From ddc0763074b5a709005391f9ae4f990f04c9d2aa Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Tue, 29 Jul 2025 14:39:38 +0100 Subject: [PATCH 1/8] Initial reauthenticate flow --- packages/frontpage-oauth/src/index.ts | 5 +- packages/frontpage/app/(auth)/layout.tsx | 9 + .../frontpage/app/(auth)/login/_lib/form.tsx | 106 +- packages/frontpage/app/(auth)/login/page.tsx | 39 +- .../_lib/reauthenticate-action.ts | 21 + .../_lib/reauthenticate-form.tsx | 34 + .../app/(auth)/reauthenticate/page.tsx | 55 + .../drizzle/0007_slimy_mephistopheles.sql | 1 + .../frontpage/drizzle/meta/0007_snapshot.json | 1159 +++++++++++++++++ packages/frontpage/drizzle/meta/_journal.json | 9 +- packages/frontpage/lib/schema.ts | 2 + packages/frontpage/middleware.ts | 20 + 12 files changed, 1382 insertions(+), 78 deletions(-) create mode 100644 packages/frontpage/app/(auth)/layout.tsx create mode 100644 packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-action.ts create mode 100644 packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-form.tsx create mode 100644 packages/frontpage/app/(auth)/reauthenticate/page.tsx create mode 100644 packages/frontpage/drizzle/0007_slimy_mephistopheles.sql create mode 100644 packages/frontpage/drizzle/meta/0007_snapshot.json diff --git a/packages/frontpage-oauth/src/index.ts b/packages/frontpage-oauth/src/index.ts index c2d1606a..d06f7694 100644 --- a/packages/frontpage-oauth/src/index.ts +++ b/packages/frontpage-oauth/src/index.ts @@ -6,6 +6,8 @@ type GetClientMetadataOptions = { appUrl: string; }; +export const AUTH_SCOPES = "atproto transition:generic"; + export function getClientMetadata({ redirectUri, appUrl, @@ -20,8 +22,7 @@ export function getClientMetadata({ subject_type: "public", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], // TODO: "code id_token"? - // TODO: Tweak these? - scope: "atproto transition:generic", + scope: AUTH_SCOPES, client_name: "Frontpage", token_endpoint_auth_method: "private_key_jwt", token_endpoint_auth_signing_alg: "ES256", diff --git a/packages/frontpage/app/(auth)/layout.tsx b/packages/frontpage/app/(auth)/layout.tsx new file mode 100644 index 00000000..56772458 --- /dev/null +++ b/packages/frontpage/app/(auth)/layout.tsx @@ -0,0 +1,9 @@ +import { Card } from "@/lib/components/ui/card"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/packages/frontpage/app/(auth)/login/_lib/form.tsx b/packages/frontpage/app/(auth)/login/_lib/form.tsx index 28228198..55723cff 100644 --- a/packages/frontpage/app/(auth)/login/_lib/form.tsx +++ b/packages/frontpage/app/(auth)/login/_lib/form.tsx @@ -26,61 +26,59 @@ export function LoginForm() { ); return ( - <> -
-
- +
+ + + + - - - - - - - - - Login with another PDS - - Enter the URL of your PDS to login. - - - - - - - - - - - - - - - Login with handle - - Enter your Bluesky/AT Protocol handle to login. - - - - - - - - -
- + + + + Login with another PDS + + Enter the URL of your PDS to login. + + + + + + + + + + + + + + + Login with handle + + Enter your Bluesky/AT Protocol handle to login. + + + + + + + + + ); } diff --git a/packages/frontpage/app/(auth)/login/page.tsx b/packages/frontpage/app/(auth)/login/page.tsx index 9aa300b4..f8c39e00 100644 --- a/packages/frontpage/app/(auth)/login/page.tsx +++ b/packages/frontpage/app/(auth)/login/page.tsx @@ -3,7 +3,6 @@ import { LoginForm } from "./_lib/form"; import { getUser } from "@/lib/data/user"; import { Alert, AlertDescription, AlertTitle } from "@/lib/components/ui/alert"; import { CrossCircledIcon } from "@radix-ui/react-icons"; -import { Card } from "@/lib/components/ui/card"; export default async function LoginPage({ searchParams, @@ -19,25 +18,23 @@ export default async function LoginPage({ const error = (await searchParams).error; return ( -
- -
-

- Sign in to Frontpage -

-

- Sign in or create an account in the Atmosphere to get started. -

-
- - {error ? ( - - - Login error, please try again - {error} - - ) : null} -
-
+ <> +
+

+ Sign in to Frontpage +

+

+ Sign in or create an account in the Atmosphere to get started. +

+
+ + {error ? ( + + + Login error, please try again + {error} + + ) : null} + ); } diff --git a/packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-action.ts b/packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-action.ts new file mode 100644 index 00000000..dbed9a63 --- /dev/null +++ b/packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-action.ts @@ -0,0 +1,21 @@ +"use server"; + +import { getSession } from "@/lib/auth"; +import { signIn } from "@/lib/auth-sign-in"; +import { redirect } from "next/navigation"; + +export async function reauthenticateAction() { + const session = await getSession(); + if (!session) { + redirect("/login?error=You've been logged out. Please log in again."); + } + const result = await signIn({ + identifier: session.user.did, + }); + + if (result && "error" in result) { + return { + error: `An error occurred while re-authenticating (${result.error}), please try again.`, + }; + } +} diff --git a/packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-form.tsx b/packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-form.tsx new file mode 100644 index 00000000..5dae11b5 --- /dev/null +++ b/packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-form.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useActionState } from "react"; +import { reauthenticateAction } from "./reauthenticate-action"; +import { Button } from "@/lib/components/ui/button"; +import { Alert, AlertDescription, AlertTitle } from "@/lib/components/ui/alert"; +import { CrossCircledIcon } from "@radix-ui/react-icons"; + +export function ReauthenticateForm({ + // TODO: Use this prop to redirect after re-authentication, requires changes in signIn method + // eslint-disable-next-line @typescript-eslint/no-unused-vars + redirectPath, +}: { + redirectPath?: string; +}) { + const [state, action, isPending] = useActionState(reauthenticateAction, null); + + return ( +
+
+ +
+ {state?.error ? ( + + + Error + {state?.error} + + ) : null} +
+ ); +} diff --git a/packages/frontpage/app/(auth)/reauthenticate/page.tsx b/packages/frontpage/app/(auth)/reauthenticate/page.tsx new file mode 100644 index 00000000..e2284a94 --- /dev/null +++ b/packages/frontpage/app/(auth)/reauthenticate/page.tsx @@ -0,0 +1,55 @@ +import { getSession, signOut } from "@/lib/auth"; +import { AUTH_SCOPES } from "@repo/frontpage-oauth"; +import { redirect } from "next/navigation"; +import { ReauthenticateForm } from "./_lib/reauthenticate-form"; +import { Button } from "@/lib/components/ui/button"; +import { revalidatePath } from "next/cache"; + +export default async function LoginPage({ + searchParams, +}: { + searchParams: Promise<{ redirect?: string }>; +}) { + const session = await getSession(); + if (!session) { + redirect("/login?error=You've been logged out. Please log in again."); + } + + const redirectParam = (await searchParams).redirect; + + // TODO: Test this doesnt allow you to redirect to an external URL + const redirectPath = redirectParam?.startsWith("/") ? redirectParam : "/"; + + if (session.user.scope === AUTH_SCOPES) { + console.warn( + "User has AUTH_SCOPES, redirecting to the specified path or defaulting to /", + ); + redirect(redirectPath); + } + + return ( + <> +
+

+ Re-authenticate to Frontpage +

+

+ You need to re-authenticate to continue using Frontpage so that we + have the latest permissions to access your data. +

+
+ +
{ + "use server"; + await signOut(); + revalidatePath("/", "layout"); + }} + > + +
+ + ); +} diff --git a/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql b/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql new file mode 100644 index 00000000..a3b63fd9 --- /dev/null +++ b/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql @@ -0,0 +1 @@ +ALTER TABLE `oauth_sessions` ADD `scope` text DEFAULT 'atproto transition:generic' NOT NULL; diff --git a/packages/frontpage/drizzle/meta/0007_snapshot.json b/packages/frontpage/drizzle/meta/0007_snapshot.json new file mode 100644 index 00000000..f06d7eeb --- /dev/null +++ b/packages/frontpage/drizzle/meta/0007_snapshot.json @@ -0,0 +1,1159 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "55a55ad1-0a89-47f7-aaca-3679bb5ea145", + "prevId": "e428846a-761a-4c51-86c2-b064a994370e", + "tables": { + "admin_users": { + "name": "admin_users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "admin_users_did_unique": { + "name": "admin_users_did_unique", + "columns": [ + "did" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "beta_users": { + "name": "beta_users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "beta_users_did_unique": { + "name": "beta_users_did_unique", + "columns": [ + "did" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comments": { + "name": "comments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text(10000)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'live'" + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "comments_author_did_rkey_unique": { + "name": "comments_author_did_rkey_unique", + "columns": [ + "author_did", + "rkey" + ], + "isUnique": true + } + }, + "foreignKeys": { + "comments_post_id_posts_id_fk": { + "name": "comments_post_id_posts_id_fk", + "tableFrom": "comments", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "parent_comment_id_fkey": { + "name": "parent_comment_id_fkey", + "tableFrom": "comments", + "tableTo": "comments", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comment_aggregates": { + "name": "comment_aggregates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vote_count": { + "name": "vote_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CAST(1 AS REAL) / (pow(2,1.8)))" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "comment_aggregates_comment_id_unique": { + "name": "comment_aggregates_comment_id_unique", + "columns": [ + "comment_id" + ], + "isUnique": true + }, + "comment_id_idx": { + "name": "comment_id_idx", + "columns": [ + "comment_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "comment_aggregates_comment_id_comments_id_fk": { + "name": "comment_aggregates_comment_id_comments_id_fk", + "tableFrom": "comment_aggregates", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comment_votes": { + "name": "comment_votes", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'live'" + } + }, + "indexes": { + "comment_votes_author_did_rkey_unique": { + "name": "comment_votes_author_did_rkey_unique", + "columns": [ + "author_did", + "rkey" + ], + "isUnique": true + }, + "comment_votes_author_did_comment_id_unique": { + "name": "comment_votes_author_did_comment_id_unique", + "columns": [ + "author_did", + "comment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "comment_votes_comment_id_comments_id_fk": { + "name": "comment_votes_comment_id_comments_id_fk", + "tableFrom": "comment_votes", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "consumed_offsets": { + "name": "consumed_offsets", + "columns": { + "offset": { + "name": "offset", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "labelled_profiles": { + "name": "labelled_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_hidden": { + "name": "is_hidden", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "labelled_profiles_did_unique": { + "name": "labelled_profiles_did_unique", + "columns": [ + "did" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "moderation_events": { + "name": "moderation_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "subject_uri": { + "name": "subject_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_did": { + "name": "subject_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_collection": { + "name": "subject_collection", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subject_rkey": { + "name": "subject_rkey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subject_cid": { + "name": "subject_cid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "labels_added": { + "name": "labels_added", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels_removed": { + "name": "labels_removed", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications": { + "name": "notifications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "read_at": { + "name": "read_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_comment_id_comments_id_fk": { + "name": "notifications_comment_id_comments_id_fk", + "tableFrom": "notifications", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth_auth_requests": { + "name": "oauth_auth_requests", + "columns": { + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "iss": { + "name": "iss", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pkce_verifier": { + "name": "pkce_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dpop_private_jwk": { + "name": "dpop_private_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dpop_public_jwk": { + "name": "dpop_public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauth_auth_requests_state_unique": { + "name": "oauth_auth_requests_state_unique", + "columns": [ + "state" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth_sessions": { + "name": "oauth_sessions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "iss": { + "name": "iss", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dpop_nonce": { + "name": "dpop_nonce", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dpop_private_jwk": { + "name": "dpop_private_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dpop_public_jwk": { + "name": "dpop_public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'atproto transition:generic'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "posts": { + "name": "posts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "title": { + "name": "title", + "type": "text(300)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'live'" + } + }, + "indexes": { + "posts_author_did_rkey_unique": { + "name": "posts_author_did_rkey_unique", + "columns": [ + "author_did", + "rkey" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "post_aggregates": { + "name": "post_aggregates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment_count": { + "name": "comment_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "vote_count": { + "name": "vote_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CAST(1 AS REAL) / (pow(2,1.8)))" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "post_id_idx": { + "name": "post_id_idx", + "columns": [ + "post_id" + ], + "isUnique": false + }, + "rank_idx": { + "name": "rank_idx", + "columns": [ + "rank" + ], + "isUnique": false + }, + "post_aggregates_post_id_unique": { + "name": "post_aggregates_post_id_unique", + "columns": [ + "post_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "post_aggregates_post_id_posts_id_fk": { + "name": "post_aggregates_post_id_posts_id_fk", + "tableFrom": "post_aggregates", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "post_votes": { + "name": "post_votes", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'live'" + } + }, + "indexes": { + "post_votes_author_did_rkey_unique": { + "name": "post_votes_author_did_rkey_unique", + "columns": [ + "author_did", + "rkey" + ], + "isUnique": true + }, + "post_votes_author_did_post_id_unique": { + "name": "post_votes_author_did_post_id_unique", + "columns": [ + "author_did", + "post_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "post_votes_post_id_posts_id_fk": { + "name": "post_votes_post_id_posts_id_fk", + "tableFrom": "post_votes", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "actioned_at": { + "name": "actioned_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actioned_by": { + "name": "actioned_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subject_uri": { + "name": "subject_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_did": { + "name": "subject_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_collection": { + "name": "subject_collection", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subject_rkey": { + "name": "subject_rkey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subject_cid": { + "name": "subject_cid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "creator_comment": { + "name": "creator_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_reason": { + "name": "report_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/frontpage/drizzle/meta/_journal.json b/packages/frontpage/drizzle/meta/_journal.json index 78475313..a5dd266c 100644 --- a/packages/frontpage/drizzle/meta/_journal.json +++ b/packages/frontpage/drizzle/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1744475828986, "tag": "0006_thankful_namorita", "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1753794880462, + "tag": "0007_slimy_mephistopheles", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/frontpage/lib/schema.ts b/packages/frontpage/lib/schema.ts index 0dd60922..1fbed698 100644 --- a/packages/frontpage/lib/schema.ts +++ b/packages/frontpage/lib/schema.ts @@ -15,6 +15,7 @@ import { MAX_POST_URL_LENGTH, } from "./data/db/constants"; import { type ColumnBaseConfig, sql } from "drizzle-orm"; +import { AUTH_SCOPES } from "@repo/frontpage-oauth"; const did = customType<{ data: DID }>({ dataType() { @@ -225,6 +226,7 @@ export const OauthSession = sqliteTable("oauth_sessions", { dpopPublicJwk: text("dpop_public_jwk").notNull(), expiresAt: dateIsoText("expires_at").notNull(), createdAt: dateIsoText("created_at").notNull(), + scope: text("scope").notNull().default(AUTH_SCOPES), }); export const ModerationEvent = sqliteTable("moderation_events", { diff --git a/packages/frontpage/middleware.ts b/packages/frontpage/middleware.ts index ca763308..a1dbd545 100644 --- a/packages/frontpage/middleware.ts +++ b/packages/frontpage/middleware.ts @@ -17,6 +17,7 @@ import { getAuthCookie, setAuthCookie, } from "./lib/auth"; +import { AUTH_SCOPES } from "@repo/frontpage-oauth"; export async function middleware(request: NextRequest) { const cookieJwt = await getAuthCookie(); @@ -37,6 +38,25 @@ export async function middleware(request: NextRequest) { if (!session) { return NextResponse.next(); } + + const requestUrl = new URL(request.url); + // If the current session has different scopes than the AUTH_SCOPES, redirect to reauthenticate + // Don't redirect if the request is for the reauthenticate page or oauth callback + if ( + requestUrl.pathname !== "/reauthenticate" && + requestUrl.pathname.startsWith("/oauth/") && + session.user.scope !== AUTH_SCOPES + ) { + // Redirect to page to ask for re-authentication + const redirectUrl = new URL("/reauthenticate", request.url); + redirectUrl.searchParams.set("redirect", request.nextUrl.pathname); + console.log(redirectUrl); + return NextResponse.redirect(redirectUrl, { + status: 307, + headers: NextResponse.next().headers, + }); + } + const authServer = await processDiscoveryResponse( new URL(session.user.iss), await oauthDiscoveryRequest(new URL(session.user.iss)), From d42c5d0e9fa9731e36c25e58be20ec130bd5f9b1 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Tue, 29 Jul 2025 14:56:19 +0100 Subject: [PATCH 2/8] Add avatar --- .../_lib/reauthenticate-form.tsx | 9 ++++-- .../app/(auth)/reauthenticate/page.tsx | 29 +++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-form.tsx b/packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-form.tsx index 5dae11b5..38789930 100644 --- a/packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-form.tsx +++ b/packages/frontpage/app/(auth)/reauthenticate/_lib/reauthenticate-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useActionState } from "react"; +import { type ReactNode, useActionState } from "react"; import { reauthenticateAction } from "./reauthenticate-action"; import { Button } from "@/lib/components/ui/button"; import { Alert, AlertDescription, AlertTitle } from "@/lib/components/ui/alert"; @@ -10,15 +10,18 @@ export function ReauthenticateForm({ // TODO: Use this prop to redirect after re-authentication, requires changes in signIn method // eslint-disable-next-line @typescript-eslint/no-unused-vars redirectPath, + avatar, }: { redirectPath?: string; + avatar: ReactNode; }) { const [state, action, isPending] = useActionState(reauthenticateAction, null); return (
-
-
diff --git a/packages/frontpage/app/(auth)/reauthenticate/page.tsx b/packages/frontpage/app/(auth)/reauthenticate/page.tsx index e2284a94..2a2f463d 100644 --- a/packages/frontpage/app/(auth)/reauthenticate/page.tsx +++ b/packages/frontpage/app/(auth)/reauthenticate/page.tsx @@ -4,6 +4,7 @@ import { redirect } from "next/navigation"; import { ReauthenticateForm } from "./_lib/reauthenticate-form"; import { Button } from "@/lib/components/ui/button"; import { revalidatePath } from "next/cache"; +import { UserAvatar } from "@/lib/components/user-avatar"; export default async function LoginPage({ searchParams, @@ -38,18 +39,22 @@ export default async function LoginPage({ have the latest permissions to access your data.

- -
{ - "use server"; - await signOut(); - revalidatePath("/", "layout"); - }} - > - -
+
+ } + /> +
{ + "use server"; + await signOut(); + revalidatePath("/", "layout"); + }} + > + +
+
); } From 4091d9867590186c8673fe9a4a7e7332f66e2355 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 30 Jul 2025 12:37:15 +0100 Subject: [PATCH 3/8] Move auth scope check to layout --- packages/frontpage/app/(app)/layout.tsx | 9 +++++++++ packages/frontpage/middleware.ts | 18 ------------------ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/packages/frontpage/app/(app)/layout.tsx b/packages/frontpage/app/(app)/layout.tsx index ad7cbfea..70927d21 100644 --- a/packages/frontpage/app/(app)/layout.tsx +++ b/packages/frontpage/app/(app)/layout.tsx @@ -30,6 +30,8 @@ import { DialogTrigger, } from "@/lib/components/ui/dialog"; import { NewPostForm } from "./post/new/_client"; +import { AUTH_SCOPES } from "@repo/frontpage-oauth"; +import { redirect } from "next/navigation"; export default async function Layout({ children, @@ -37,6 +39,13 @@ export default async function Layout({ children: React.ReactNode; }) { const session = await getSession(); + + // If the current session has different scopes than the AUTH_SCOPES, redirect to reauthenticate + // Don't redirect if the request is for the reauthenticate page or oauth callback + if (session && session.user.scope !== AUTH_SCOPES) { + redirect("/reauthenticate"); + } + return (
diff --git a/packages/frontpage/middleware.ts b/packages/frontpage/middleware.ts index a1dbd545..3aa217e2 100644 --- a/packages/frontpage/middleware.ts +++ b/packages/frontpage/middleware.ts @@ -39,24 +39,6 @@ export async function middleware(request: NextRequest) { return NextResponse.next(); } - const requestUrl = new URL(request.url); - // If the current session has different scopes than the AUTH_SCOPES, redirect to reauthenticate - // Don't redirect if the request is for the reauthenticate page or oauth callback - if ( - requestUrl.pathname !== "/reauthenticate" && - requestUrl.pathname.startsWith("/oauth/") && - session.user.scope !== AUTH_SCOPES - ) { - // Redirect to page to ask for re-authentication - const redirectUrl = new URL("/reauthenticate", request.url); - redirectUrl.searchParams.set("redirect", request.nextUrl.pathname); - console.log(redirectUrl); - return NextResponse.redirect(redirectUrl, { - status: 307, - headers: NextResponse.next().headers, - }); - } - const authServer = await processDiscoveryResponse( new URL(session.user.iss), await oauthDiscoveryRequest(new URL(session.user.iss)), From f1fc6401cd85e7bf9a66d2442b0af622bb4d4518 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 30 Jul 2025 12:42:09 +0100 Subject: [PATCH 4/8] Remove unused import --- packages/frontpage/middleware.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/frontpage/middleware.ts b/packages/frontpage/middleware.ts index 3aa217e2..60ad3a0c 100644 --- a/packages/frontpage/middleware.ts +++ b/packages/frontpage/middleware.ts @@ -17,7 +17,6 @@ import { getAuthCookie, setAuthCookie, } from "./lib/auth"; -import { AUTH_SCOPES } from "@repo/frontpage-oauth"; export async function middleware(request: NextRequest) { const cookieJwt = await getAuthCookie(); From 34e8effd3f6f2debe082b43ec37891c36c2ab5f4 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Sun, 3 Aug 2025 21:49:18 +0100 Subject: [PATCH 5/8] Move default auth scopes to application code --- packages/frontpage/drizzle/0007_slimy_mephistopheles.sql | 2 +- packages/frontpage/drizzle/meta/0007_snapshot.json | 3 +-- packages/frontpage/lib/auth.ts | 6 +++++- packages/frontpage/lib/schema.ts | 3 +-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql b/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql index a3b63fd9..a7dfcc17 100644 --- a/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql +++ b/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql @@ -1 +1 @@ -ALTER TABLE `oauth_sessions` ADD `scope` text DEFAULT 'atproto transition:generic' NOT NULL; +ALTER TABLE `oauth_sessions` ADD `scope` text NOT NULL; diff --git a/packages/frontpage/drizzle/meta/0007_snapshot.json b/packages/frontpage/drizzle/meta/0007_snapshot.json index f06d7eeb..fc67fe3b 100644 --- a/packages/frontpage/drizzle/meta/0007_snapshot.json +++ b/packages/frontpage/drizzle/meta/0007_snapshot.json @@ -771,8 +771,7 @@ "type": "text", "primaryKey": false, "notNull": true, - "autoincrement": false, - "default": "'atproto transition:generic'" + "autoincrement": false } }, "indexes": {}, diff --git a/packages/frontpage/lib/auth.ts b/packages/frontpage/lib/auth.ts index 8293e694..99e9e1d4 100644 --- a/packages/frontpage/lib/auth.ts +++ b/packages/frontpage/lib/auth.ts @@ -33,7 +33,10 @@ import { getDidFromHandleOrDid, getVerifiedHandle, } from "./data/atproto/identity"; -import { getClientMetadata as createClientMetadata } from "@repo/frontpage-oauth"; +import { + AUTH_SCOPES, + getClientMetadata as createClientMetadata, +} from "@repo/frontpage-oauth"; import { getRootHost } from "./navigation"; import { invariant } from "./utils"; @@ -332,6 +335,7 @@ export const handlers = { dpopNonce, dpopPrivateJwk: row.dpopPrivateJwk, dpopPublicJwk: row.dpopPublicJwk, + scope: AUTH_SCOPES, }); if (!lastInsertRowid) { diff --git a/packages/frontpage/lib/schema.ts b/packages/frontpage/lib/schema.ts index 1fbed698..e3d26dc4 100644 --- a/packages/frontpage/lib/schema.ts +++ b/packages/frontpage/lib/schema.ts @@ -15,7 +15,6 @@ import { MAX_POST_URL_LENGTH, } from "./data/db/constants"; import { type ColumnBaseConfig, sql } from "drizzle-orm"; -import { AUTH_SCOPES } from "@repo/frontpage-oauth"; const did = customType<{ data: DID }>({ dataType() { @@ -226,7 +225,7 @@ export const OauthSession = sqliteTable("oauth_sessions", { dpopPublicJwk: text("dpop_public_jwk").notNull(), expiresAt: dateIsoText("expires_at").notNull(), createdAt: dateIsoText("created_at").notNull(), - scope: text("scope").notNull().default(AUTH_SCOPES), + scope: text("scope").notNull(), }); export const ModerationEvent = sqliteTable("moderation_events", { From 5465f6707014a576805bdcb07b516875e8247191 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Sat, 6 Sep 2025 10:40:37 +0100 Subject: [PATCH 6/8] Add default --- packages/frontpage/drizzle/0007_slimy_mephistopheles.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql b/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql index a7dfcc17..6ac2e353 100644 --- a/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql +++ b/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql @@ -1 +1 @@ -ALTER TABLE `oauth_sessions` ADD `scope` text NOT NULL; +ALTER TABLE `oauth_sessions` ADD `scope` text NOT NULL DEFAULT 'atproto transition:generic'; From 659251d847de89c66fbc0083e99156c12ab10b67 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Fri, 3 Oct 2025 22:29:49 +0100 Subject: [PATCH 7/8] Better migration --- packages/frontpage/drizzle/0007_silent_franklin_storm.sql | 1 + packages/frontpage/drizzle/0007_slimy_mephistopheles.sql | 1 - packages/frontpage/drizzle/meta/0007_snapshot.json | 5 +++-- packages/frontpage/drizzle/meta/_journal.json | 4 ++-- packages/frontpage/lib/schema.ts | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 packages/frontpage/drizzle/0007_silent_franklin_storm.sql delete mode 100644 packages/frontpage/drizzle/0007_slimy_mephistopheles.sql diff --git a/packages/frontpage/drizzle/0007_silent_franklin_storm.sql b/packages/frontpage/drizzle/0007_silent_franklin_storm.sql new file mode 100644 index 00000000..03d6d381 --- /dev/null +++ b/packages/frontpage/drizzle/0007_silent_franklin_storm.sql @@ -0,0 +1 @@ +ALTER TABLE `oauth_sessions` ADD `scope` text DEFAULT 'atproto transition:generic' NOT NULL; \ No newline at end of file diff --git a/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql b/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql deleted file mode 100644 index 6ac2e353..00000000 --- a/packages/frontpage/drizzle/0007_slimy_mephistopheles.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `oauth_sessions` ADD `scope` text NOT NULL DEFAULT 'atproto transition:generic'; diff --git a/packages/frontpage/drizzle/meta/0007_snapshot.json b/packages/frontpage/drizzle/meta/0007_snapshot.json index fc67fe3b..bd8164e2 100644 --- a/packages/frontpage/drizzle/meta/0007_snapshot.json +++ b/packages/frontpage/drizzle/meta/0007_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "55a55ad1-0a89-47f7-aaca-3679bb5ea145", + "id": "313ce997-54c0-48dd-b9cd-dd4038931f34", "prevId": "e428846a-761a-4c51-86c2-b064a994370e", "tables": { "admin_users": { @@ -771,7 +771,8 @@ "type": "text", "primaryKey": false, "notNull": true, - "autoincrement": false + "autoincrement": false, + "default": "'atproto transition:generic'" } }, "indexes": {}, diff --git a/packages/frontpage/drizzle/meta/_journal.json b/packages/frontpage/drizzle/meta/_journal.json index a5dd266c..0e681ea8 100644 --- a/packages/frontpage/drizzle/meta/_journal.json +++ b/packages/frontpage/drizzle/meta/_journal.json @@ -54,8 +54,8 @@ { "idx": 7, "version": "6", - "when": 1753794880462, - "tag": "0007_slimy_mephistopheles", + "when": 1759526589132, + "tag": "0007_silent_franklin_storm", "breakpoints": true } ] diff --git a/packages/frontpage/lib/schema.ts b/packages/frontpage/lib/schema.ts index e3d26dc4..f2e874f2 100644 --- a/packages/frontpage/lib/schema.ts +++ b/packages/frontpage/lib/schema.ts @@ -225,7 +225,7 @@ export const OauthSession = sqliteTable("oauth_sessions", { dpopPublicJwk: text("dpop_public_jwk").notNull(), expiresAt: dateIsoText("expires_at").notNull(), createdAt: dateIsoText("created_at").notNull(), - scope: text("scope").notNull(), + scope: text("scope").notNull().default("atproto transition:generic"), }); export const ModerationEvent = sqliteTable("moderation_events", { From 5ec1593fa1c41b0af7b309d26e90bb552f902337 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Fri, 3 Oct 2025 22:34:02 +0100 Subject: [PATCH 8/8] Use const as default scope --- packages/frontpage/lib/schema.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontpage/lib/schema.ts b/packages/frontpage/lib/schema.ts index f2e874f2..1fbed698 100644 --- a/packages/frontpage/lib/schema.ts +++ b/packages/frontpage/lib/schema.ts @@ -15,6 +15,7 @@ import { MAX_POST_URL_LENGTH, } from "./data/db/constants"; import { type ColumnBaseConfig, sql } from "drizzle-orm"; +import { AUTH_SCOPES } from "@repo/frontpage-oauth"; const did = customType<{ data: DID }>({ dataType() { @@ -225,7 +226,7 @@ export const OauthSession = sqliteTable("oauth_sessions", { dpopPublicJwk: text("dpop_public_jwk").notNull(), expiresAt: dateIsoText("expires_at").notNull(), createdAt: dateIsoText("created_at").notNull(), - scope: text("scope").notNull().default("atproto transition:generic"), + scope: text("scope").notNull().default(AUTH_SCOPES), }); export const ModerationEvent = sqliteTable("moderation_events", {