From f68f1f462a2ff2f4ac4b393a671b2969076814c1 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Mon, 12 Jan 2026 09:24:58 -0500 Subject: [PATCH] session state simplification --- src/components/blog/Card.tsx | 6 +- src/components/blog/CardLinks.tsx | 4 +- src/components/blog/CommentBlock.tsx | 20 +- src/components/blog/CommentDeletionPrompt.tsx | 6 +- src/components/blog/CommentInputBlock.tsx | 2 +- src/components/blog/CommentSection.tsx | 28 +-- src/components/blog/CommentSectionWrapper.tsx | 5 +- src/components/blog/CommentSorting.tsx | 3 +- src/components/blog/PostSorting.tsx | 10 +- src/components/blog/SessionDependantLike.tsx | 4 +- src/context/auth.tsx | 2 +- src/db/create.ts | 1 + src/env/server.ts | 4 - src/lib/auth-query.ts | 10 +- src/lib/comment-utils.ts | 13 +- src/routes/analytics.tsx | 2 +- src/routes/blog/[title]/index.tsx | 27 ++- src/routes/blog/create/index.tsx | 2 +- src/routes/blog/edit/[id].tsx | 4 +- src/routes/blog/index.tsx | 16 +- src/routes/test.tsx | 4 +- src/server/api/routers/auth.ts | 15 -- src/server/api/routers/blog.ts | 16 +- src/server/api/routers/database.ts | 4 +- src/server/api/routers/infill.ts | 2 +- src/server/api/utils.ts | 12 +- src/server/auth.ts | 1 - src/server/conditional-parser.test.ts | 8 +- src/server/conditional-parser.ts | 13 +- src/server/migrate-multi-auth.ts | 228 ------------------ src/server/session-helpers.ts | 21 +- src/types/comment.ts | 20 +- 32 files changed, 132 insertions(+), 381 deletions(-) delete mode 100644 src/server/migrate-multi-auth.ts diff --git a/src/components/blog/Card.tsx b/src/components/blog/Card.tsx index 4665eaf..651a851 100644 --- a/src/components/blog/Card.tsx +++ b/src/components/blog/Card.tsx @@ -7,14 +7,14 @@ const DeletePostButton = lazy(() => import("./DeletePostButton")); export interface CardProps { post: PostCardData; - privilegeLevel: "anonymous" | "admin" | "user"; + isAdmin: boolean; index?: number; } export default function Card(props: CardProps) { return (
- +
@@ -60,7 +60,7 @@ export default function Card(props: CardProps) {
diff --git a/src/components/blog/CardLinks.tsx b/src/components/blog/CardLinks.tsx index 889ca8c..2aafc9f 100644 --- a/src/components/blog/CardLinks.tsx +++ b/src/components/blog/CardLinks.tsx @@ -5,7 +5,7 @@ import { Spinner } from "~/components/Spinner"; export interface CardLinksProps { postTitle: string; postID: number; - privilegeLevel: string; + isAdmin: boolean; } export default function CardLinks(props: CardLinksProps) { @@ -25,7 +25,7 @@ export default function CardLinks(props: CardLinksProps) {
- + setEditLoading(true)} diff --git a/src/components/blog/CommentBlock.tsx b/src/components/blog/CommentBlock.tsx index b242d51..d826ebc 100644 --- a/src/components/blog/CommentBlock.tsx +++ b/src/components/blog/CommentBlock.tsx @@ -122,13 +122,10 @@ export default function CommentBlock(props: CommentBlockProps) { ); const canDelete = () => - props.currentUserID === props.comment.commenter_id || - props.privilegeLevel === "admin"; + props.currentUserID === props.comment.commenter_id || props.isAdmin; const canEdit = () => props.currentUserID === props.comment.commenter_id; - const isAnonymous = () => props.privilegeLevel === "anonymous"; - const replyIconColor = () => "var(--color-peach)"; return ( @@ -163,12 +160,12 @@ export default function CommentBlock(props: CommentBlockProps) { hasUpvoted() ? "fill-green" : `fill-text hover:fill-green ${ - isAnonymous() ? "tooltip z-50" : "" + !props.isAuthenticated ? "tooltip z-50" : "" }` }`} > - +
You must be logged in
@@ -190,14 +187,14 @@ export default function CommentBlock(props: CommentBlockProps) { hasDownvoted() ? "fill-red" : `fill-text hover:fill-red ${ - isAnonymous() ? "tooltip z-50" : "" + !props.isAuthenticated ? "tooltip z-50" : "" }` }`} >
- +
You must be logged in
@@ -309,7 +306,7 @@ export default function CommentBlock(props: CommentBlockProps) { currentUserID={props.currentUserID} reactions={reactions()} showingReactionOptions={showingReactionOptions()} - privilegeLevel={props.privilegeLevel} + isAuthenticated={props.isAuthenticated} commentReaction={props.commentReaction} />
@@ -325,7 +322,7 @@ export default function CommentBlock(props: CommentBlockProps) { > comment.parent_comment_id === childComment.id )} - privilegeLevel={props.privilegeLevel} + isAuthenticated={props.isAuthenticated} + isAdmin={props.isAdmin} currentUserID={props.currentUserID} reactionMap={props.reactionMap} level={props.level + 1} diff --git a/src/components/blog/CommentDeletionPrompt.tsx b/src/components/blog/CommentDeletionPrompt.tsx index 4b66271..29311b3 100644 --- a/src/components/blog/CommentDeletionPrompt.tsx +++ b/src/components/blog/CommentDeletionPrompt.tsx @@ -84,13 +84,11 @@ export default function CommentDeletionPrompt( onChange={handleNormalDeleteCheckbox} />
- {props.privilegeLevel === "admin" - ? "Confirm User Delete?" - : "Confirm Delete?"} + {props.isAdmin ? "Confirm User Delete?" : "Confirm Delete?"}
- +
diff --git a/src/components/blog/CommentSection.tsx b/src/components/blog/CommentSection.tsx index f12cc17..a1ffc07 100644 --- a/src/components/blog/CommentSection.tsx +++ b/src/components/blog/CommentSection.tsx @@ -6,7 +6,6 @@ import type { UserPublicData, ReactionType, ModificationType, - PrivilegeLevel, SortingMode } from "~/types/comment"; import CommentInputBlock from "./CommentInputBlock"; @@ -20,28 +19,6 @@ const COMMENT_SORTING_OPTIONS: { val: SortingMode }[] = [ { val: "hot" } ]; -interface CommentSectionProps { - privilegeLevel: PrivilegeLevel; - allComments: Comment[]; - topLevelComments: Comment[]; - postID: number; - reactionMap: Map; - currentUserID: string; - userCommentMap: Map | undefined; - newComment: (commentBody: string, parentCommentID?: number) => Promise; - commentSubmitLoading: boolean; - toggleModification: ( - commentID: number, - commenterID: string, - commentBody: string, - modificationType: ModificationType, - commenterImage?: string, - commenterEmail?: string, - commenterDisplayName?: string - ) => void; - commentReaction: (reactionType: ReactionType, commentID: number) => void; -} - export default function CommentSection(props: CommentSectionProps) { const [searchParams] = useSearchParams(); @@ -66,7 +43,7 @@ export default function CommentSection(props: CommentSectionProps) {
{ diff --git a/src/components/blog/CommentSorting.tsx b/src/components/blog/CommentSorting.tsx index be1ad06..2716760 100644 --- a/src/components/blog/CommentSorting.tsx +++ b/src/components/blog/CommentSorting.tsx @@ -51,7 +51,8 @@ export default function CommentSorting(props: CommentSortingProps) { child_comments={props.allComments?.filter( (comment) => comment.parent_comment_id === topLevelComment.id )} - privilegeLevel={props.privilegeLevel} + isAuthenticated={props.isAuthenticated} + isAdmin={props.isAdmin} currentUserID={props.currentUserID} reactionMap={props.reactionMap} level={0} diff --git a/src/components/blog/PostSorting.tsx b/src/components/blog/PostSorting.tsx index 87d912e..eda3a02 100644 --- a/src/components/blog/PostSorting.tsx +++ b/src/components/blog/PostSorting.tsx @@ -10,7 +10,7 @@ export interface Tag { export interface PostSortingProps { posts: PostCardData[]; tags: Tag[]; - privilegeLevel: "anonymous" | "admin" | "user"; + isAdmin: boolean; filters?: string; sort?: string; include?: string; @@ -36,7 +36,7 @@ export default function PostSorting(props: PostSortingProps) { const filteredPosts = createMemo(() => { let filtered = props.posts; - if (props.privilegeLevel === "admin" && props.status) { + if (props.isAdmin && props.status) { if (props.status === "published") { filtered = filtered.filter((post) => post.published === 1); } else if (props.status === "unpublished") { @@ -141,11 +141,7 @@ export default function PostSorting(props: PostSortingProps) { {(post, index) => (
- +
)}
diff --git a/src/components/blog/SessionDependantLike.tsx b/src/components/blog/SessionDependantLike.tsx index cf61c0e..12c7e84 100644 --- a/src/components/blog/SessionDependantLike.tsx +++ b/src/components/blog/SessionDependantLike.tsx @@ -10,7 +10,7 @@ export interface PostLike { export interface SessionDependantLikeProps { currentUserID: string | undefined | null; - privilegeLevel: "admin" | "user" | "anonymous"; + isAuthenticated: boolean; likes: PostLike[]; projectID: number; } @@ -61,7 +61,7 @@ export default function SessionDependantLike(props: SessionDependantLikeProps) { return (
diff --git a/src/context/auth.tsx b/src/context/auth.tsx index b592221..6068dbc 100644 --- a/src/context/auth.tsx +++ b/src/context/auth.tsx @@ -65,7 +65,7 @@ export const AuthProvider: ParentComponent = (props) => { const email = () => serverAuth()?.email ?? null; const displayName = () => serverAuth()?.displayName ?? null; const userId = () => serverAuth()?.userId ?? null; - const isAdmin = () => serverAuth()?.privilegeLevel === "admin"; + const isAdmin = () => serverAuth()?.isAdmin ?? false; const isEmailVerified = () => serverAuth()?.emailVerified ?? false; // Server handles all token refresh logic diff --git a/src/db/create.ts b/src/db/create.ts index 32ff508..45363a6 100644 --- a/src/db/create.ts +++ b/src/db/create.ts @@ -9,6 +9,7 @@ export const model: { [key: string]: string } = { display_name TEXT, provider TEXT, image TEXT, + is_admin INTEGER DEFAULT 0, registered_at TEXT NOT NULL DEFAULT (datetime('now')), failed_attempts INTEGER DEFAULT 0, locked_until TEXT diff --git a/src/env/server.ts b/src/env/server.ts index 5b19e59..5c620aa 100644 --- a/src/env/server.ts +++ b/src/env/server.ts @@ -2,8 +2,6 @@ import { z } from "zod"; const serverEnvSchema = z.object({ NODE_ENV: z.enum(["development", "test", "production"]), - ADMIN_EMAIL: z.string().min(1), - ADMIN_ID: z.string().min(1), JWT_SECRET_KEY: z.string().min(1), AWS_REGION: z.string().min(1), AWS_S3_BUCKET_NAME: z.string().min(1), @@ -109,8 +107,6 @@ export const isMissingEnvVar = (varName: string): boolean => { export const getMissingEnvVars = (): string[] => { const requiredServerVars = [ "NODE_ENV", - "ADMIN_EMAIL", - "ADMIN_ID", "JWT_SECRET_KEY", "AWS_REGION", "AWS_S3_BUCKET_NAME", diff --git a/src/lib/auth-query.ts b/src/lib/auth-query.ts index e235791..2ba2fd2 100644 --- a/src/lib/auth-query.ts +++ b/src/lib/auth-query.ts @@ -7,7 +7,7 @@ export interface UserState { email: string | null; displayName: string | null; emailVerified: boolean; - privilegeLevel: "admin" | "user" | "anonymous"; + isAdmin: boolean; } /** @@ -29,7 +29,7 @@ export const getUserState = query(async (): Promise => { email: null, displayName: null, emailVerified: false, - privilegeLevel: "anonymous" + isAdmin: false }; } @@ -42,7 +42,7 @@ export const getUserState = query(async (): Promise => { email: null, displayName: null, emailVerified: false, - privilegeLevel: "anonymous" + isAdmin: false }; } @@ -59,7 +59,7 @@ export const getUserState = query(async (): Promise => { email: null, displayName: null, emailVerified: false, - privilegeLevel: "anonymous" + isAdmin: false }; } @@ -71,7 +71,7 @@ export const getUserState = query(async (): Promise => { email: user.email ?? null, displayName: user.display_name ?? null, emailVerified: user.email_verified === 1, - privilegeLevel: auth.isAdmin ? "admin" : "user" + isAdmin: auth.isAdmin }; }, "user-auth-state"); diff --git a/src/lib/comment-utils.ts b/src/lib/comment-utils.ts index 6922da7..4c3d101 100644 --- a/src/lib/comment-utils.ts +++ b/src/lib/comment-utils.ts @@ -50,15 +50,14 @@ export function isValidCommentBody(body: string): boolean { export function canModifyComment( userID: string, commenterID: string, - privilegeLevel: "admin" | "user" | "anonymous" + isAuthenticated: boolean, + isAdmin: boolean ): boolean { - if (privilegeLevel === "admin") return true; - if (privilegeLevel === "anonymous") return false; + if (isAdmin) return true; + if (!isAuthenticated) return false; return userID === commenterID; } -export function canDatabaseDelete( - privilegeLevel: "admin" | "user" | "anonymous" -): boolean { - return privilegeLevel === "admin"; +export function canDatabaseDelete(isAdmin: boolean): boolean { + return isAdmin; } diff --git a/src/routes/analytics.tsx b/src/routes/analytics.tsx index e1a44a1..701c14f 100644 --- a/src/routes/analytics.tsx +++ b/src/routes/analytics.tsx @@ -8,7 +8,7 @@ const checkAdmin = query(async (): Promise => { const { getUserState } = await import("~/lib/auth-query"); const userState = await getUserState(); - if (userState.privilegeLevel !== "admin") { + if (!userState.isAdmin) { console.log("redirect"); throw redirect("/"); } diff --git a/src/routes/blog/[title]/index.tsx b/src/routes/blog/[title]/index.tsx index 0dd38db..4ab7047 100644 --- a/src/routes/blog/[title]/index.tsx +++ b/src/routes/blog/[title]/index.tsx @@ -34,7 +34,8 @@ const getPostByTitle = query( const { getFeatureFlags } = await import("~/server/feature-flags"); const event = getRequestEvent()!; const userState = await getUserState(); - const privilegeLevel = userState.privilegeLevel; + const isAuthenticated = userState.isAuthenticated; + const isAdmin = userState.isAdmin; const userID = userState.userId; const conn = ConnectionFactory(); @@ -51,7 +52,8 @@ const getPostByTitle = query( tags: [], userCommentArray: [], reactionArray: [], - privilegeLevel: "anonymous" as const, + isAuthenticated: false, + isAdmin: false, userID: null }; } @@ -77,13 +79,14 @@ const getPostByTitle = query( tags: [], userCommentArray: [], reactionArray: [], - privilegeLevel: "anonymous" as const, + isAuthenticated: false, + isAdmin: false, userID: null }; } let query = "SELECT * FROM Post WHERE title = ?"; - if (privilegeLevel !== "admin") { + if (!isAdmin) { query += ` AND published = TRUE`; } @@ -110,7 +113,8 @@ const getPostByTitle = query( tags: [], userCommentArray: [], reactionArray: [], - privilegeLevel: "anonymous" as const, + isAuthenticated: false, + isAdmin: false, userID: null }; } @@ -123,14 +127,15 @@ const getPostByTitle = query( tags: [], userCommentArray: [], reactionArray: [], - privilegeLevel: "anonymous" as const, + isAuthenticated: false, + isAdmin: false, userID: null }; } const conditionalContext = { isAuthenticated: userID !== null, - privilegeLevel: privilegeLevel, + isAdmin: isAdmin, userId: userID, currentDate: new Date(), featureFlags: getFeatureFlags(), @@ -245,7 +250,8 @@ const getPostByTitle = query( topLevelComments, userCommentArray, reactionArray, - privilegeLevel, + isAuthenticated, + isAdmin, userID, sortBy, reads: post.reads || 0 @@ -429,7 +435,7 @@ export default function PostPage() {
@@ -455,7 +461,8 @@ export default function PostPage() { class="mx-4 pt-12 pb-12 md:mx-8 lg:mx-12" > { // Reuse shared auth query for consistency const userState = await getUserState(); - if (userState.privilegeLevel !== "admin") { + if (!userState.isAdmin) { throw redirect("/401"); } diff --git a/src/routes/blog/edit/[id].tsx b/src/routes/blog/edit/[id].tsx index f1b3e1d..d870e48 100644 --- a/src/routes/blog/edit/[id].tsx +++ b/src/routes/blog/edit/[id].tsx @@ -12,7 +12,7 @@ const getPostForEdit = query(async (id: string) => { const { ConnectionFactory } = await import("~/server/utils"); const userState = await getUserState(); - if (userState.privilegeLevel !== "admin") { + if (!userState.isAdmin) { throw redirect("/401"); } @@ -35,7 +35,7 @@ const getPostForEdit = query(async (id: string) => { return { post, tags, - privilegeLevel: userState.privilegeLevel, + isAdmin: userState.isAdmin, userID: userState.userId }; }, "post-for-edit"); diff --git a/src/routes/blog/index.tsx b/src/routes/blog/index.tsx index 31811c7..9a2e42c 100644 --- a/src/routes/blog/index.tsx +++ b/src/routes/blog/index.tsx @@ -15,10 +15,10 @@ const getPosts = query(async () => { const { ConnectionFactory } = await import("~/server/utils"); const { withCacheAndStale } = await import("~/server/cache"); const userState = await getUserState(); - const privilegeLevel = userState.privilegeLevel; + const isAdmin = userState.isAdmin; return withCacheAndStale( - `posts-${privilegeLevel}`, + `posts-${isAdmin ? "admin" : "user"}`, CACHE_CONFIG.BLOG_POSTS_LIST_CACHE_TTL_MS, async () => { const conn = ConnectionFactory(); @@ -43,7 +43,7 @@ const getPosts = query(async () => { LEFT JOIN Comment c ON p.id = c.post_id `; - if (privilegeLevel !== "admin") { + if (!isAdmin) { postsQuery += ` WHERE p.published = TRUE`; } @@ -57,7 +57,7 @@ const getPosts = query(async () => { SELECT t.value, t.post_id FROM Tag t JOIN Post p ON t.post_id = p.id - ${privilegeLevel !== "admin" ? "WHERE p.published = TRUE" : ""} + ${!isAdmin ? "WHERE p.published = TRUE" : ""} ORDER BY t.value ASC `; @@ -70,7 +70,7 @@ const getPosts = query(async () => { tagMap[key] = (tagMap[key] || 0) + 1; }); - return { posts, tags, tagMap, privilegeLevel }; + return { posts, tags, tagMap, isAdmin }; } ); }, "posts"); @@ -106,11 +106,11 @@ export default function BlogIndex() { - + - +
{ "use server"; const userState = await getUserState(); - return { privilegeLevel: userState.privilegeLevel }; + return { isAdmin: userState.isAdmin }; }, "test-auth-state"); type EndpointTest = { @@ -916,7 +916,7 @@ export default function TestPage() { description="tRPC API testing dashboard for developers to test endpoints and verify functionality." />
Unauthorized
diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index 63d7c18..80c9c74 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -306,14 +306,11 @@ export const authRouter = createTRPCRouter({ } } - const isAdmin = userId === env.ADMIN_ID; - const clientIP = getClientIP(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx)); await createAuthSession( getH3Event(ctx), userId, - isAdmin, true, // OAuth defaults to remember clientIP, userAgent @@ -521,15 +518,12 @@ export const authRouter = createTRPCRouter({ } } - const isAdmin = userId === env.ADMIN_ID; - // Create session with Vinxi (OAuth defaults to remember me) const clientIP = getClientIP(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx)); await createAuthSession( getH3Event(ctx), userId, - isAdmin, true, // OAuth defaults to remember clientIP, userAgent @@ -647,7 +641,6 @@ export const authRouter = createTRPCRouter({ } const userId = (res.rows[0] as unknown as User).id; - const isAdmin = userId === env.ADMIN_ID; const clientIP = getClientIP(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx)); @@ -655,7 +648,6 @@ export const authRouter = createTRPCRouter({ await createAuthSession( getH3Event(ctx), userId, - isAdmin, rememberMe, clientIP, userAgent @@ -780,7 +772,6 @@ export const authRouter = createTRPCRouter({ } const userId = (res.rows[0] as unknown as User).id; - const isAdmin = userId === env.ADMIN_ID; // Use rememberMe from JWT if not provided in input, default to false const shouldRemember = @@ -791,7 +782,6 @@ export const authRouter = createTRPCRouter({ await createAuthSession( getH3Event(ctx), userId, - isAdmin, shouldRemember, clientIP, userAgent @@ -983,12 +973,10 @@ export const authRouter = createTRPCRouter({ // Create session with client info const clientIP = getClientIP(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx)); - const isAdmin = userId === env.ADMIN_ID; await createAuthSession( getH3Event(ctx), userId, - isAdmin, true, // Always use persistent sessions clientIP, userAgent @@ -1150,14 +1138,11 @@ export const authRouter = createTRPCRouter({ // Reset rate limits on successful login await resetLoginRateLimits(email, clientIP); - const isAdmin = user.id === env.ADMIN_ID; - // Create session with Vinxi const userAgent = getUserAgent(getH3Event(ctx)); await createAuthSession( getH3Event(ctx), user.id, - isAdmin, rememberMe ?? false, // Default to session cookie (expires on browser close) clientIP, userAgent diff --git a/src/server/api/routers/blog.ts b/src/server/api/routers/blog.ts index 77a036f..dd6e2b2 100644 --- a/src/server/api/routers/blog.ts +++ b/src/server/api/routers/blog.ts @@ -7,9 +7,9 @@ import { CACHE_CONFIG } from "~/config"; const BLOG_CACHE_TTL = CACHE_CONFIG.BLOG_CACHE_TTL_MS; -const getAllPostsData = async (privilegeLevel: string) => { +const getAllPostsData = async (isAdmin: boolean) => { return withCacheAndStale( - `blog-posts-${privilegeLevel}`, + `blog-posts-${isAdmin ? "admin" : "public"}`, BLOG_CACHE_TTL, async () => { const conn = ConnectionFactory(); @@ -34,7 +34,7 @@ const getAllPostsData = async (privilegeLevel: string) => { LEFT JOIN Comment c ON p.id = c.post_id `; - if (privilegeLevel !== "admin") { + if (!isAdmin) { postsQuery += ` WHERE p.published = TRUE`; } @@ -48,7 +48,7 @@ const getAllPostsData = async (privilegeLevel: string) => { SELECT t.value, t.post_id FROM Tag t JOIN Post p ON t.post_id = p.id - ${privilegeLevel !== "admin" ? "WHERE p.published = TRUE" : ""} + ${!isAdmin ? "WHERE p.published = TRUE" : ""} ORDER BY t.value ASC `; @@ -64,21 +64,21 @@ const getAllPostsData = async (privilegeLevel: string) => { tagMap[key] = (tagMap[key] || 0) + 1; }); - return { posts, tags, tagMap, privilegeLevel }; + return { posts, tags, tagMap, isAdmin }; } ); }; export const blogRouter = createTRPCRouter({ getRecentPosts: publicProcedure.query(async ({ ctx }) => { - const allPostsData = await getAllPostsData("public"); + const allPostsData = await getAllPostsData(false); return allPostsData.posts.slice(0, 3); }), getPosts: publicProcedure.query(async ({ ctx }) => { - const privilegeLevel = ctx.privilegeLevel; - return getAllPostsData(privilegeLevel); + const isAdmin = ctx.isAdmin; + return getAllPostsData(isAdmin); }), incrementPostRead: publicProcedure diff --git a/src/server/api/routers/database.ts b/src/server/api/routers/database.ts index 54bb1a3..ae88798 100644 --- a/src/server/api/routers/database.ts +++ b/src/server/api/routers/database.ts @@ -144,7 +144,7 @@ export const databaseRouter = createTRPCRouter({ commentID: input.commentID, deletionType: input.deletionType, userId: ctx.userId, - privilegeLevel: ctx.privilegeLevel + isAdmin: ctx.isAdmin }); const commentQuery = await conn.execute({ @@ -161,7 +161,7 @@ export const databaseRouter = createTRPCRouter({ } const isOwner = comment.commenter_id === ctx.userId; - const isAdmin = ctx.privilegeLevel === "admin"; + const isAdmin = ctx.isAdmin; console.log("[deleteComment] Authorization check:", { isOwner, diff --git a/src/server/api/routers/infill.ts b/src/server/api/routers/infill.ts index b996678..ff0ed1a 100644 --- a/src/server/api/routers/infill.ts +++ b/src/server/api/routers/infill.ts @@ -3,7 +3,7 @@ import { env } from "~/env/server"; export const infillRouter = createTRPCRouter({ getConfig: publicProcedure.query(({ ctx }) => { - if (ctx.privilegeLevel !== "admin") { + if (!ctx.isAdmin) { return { endpoint: null, token: null }; } diff --git a/src/server/api/utils.ts b/src/server/api/utils.ts index 27b4efe..c3f1bd6 100644 --- a/src/server/api/utils.ts +++ b/src/server/api/utils.ts @@ -8,7 +8,7 @@ import { getAuthSession } from "~/server/session-helpers"; export type Context = { event: APIEvent; userId: string | null; - privilegeLevel: "anonymous" | "user" | "admin"; + isAdmin: boolean; }; async function createContextInner(event: APIEvent): Promise { @@ -16,11 +16,11 @@ async function createContextInner(event: APIEvent): Promise { const session = await getAuthSession(event.nativeEvent); let userId: string | null = null; - let privilegeLevel: "anonymous" | "user" | "admin" = "anonymous"; + let isAdmin = false; if (session && session.userId) { userId = session.userId; - privilegeLevel = session.isAdmin ? "admin" : "user"; + isAdmin = session.isAdmin; } const req = event.nativeEvent.node?.req || event.nativeEvent; @@ -56,7 +56,7 @@ async function createContextInner(event: APIEvent): Promise { return { event, userId, - privilegeLevel + isAdmin }; } @@ -70,7 +70,7 @@ export const createTRPCRouter = t.router; export const publicProcedure = t.procedure; const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { - if (!ctx.userId || ctx.privilegeLevel === "anonymous") { + if (!ctx.userId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" }); } return next({ @@ -82,7 +82,7 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { }); const enforceUserIsAdmin = t.middleware(({ ctx, next }) => { - if (ctx.privilegeLevel !== "admin") { + if (!ctx.isAdmin) { throw new TRPCError({ code: "FORBIDDEN", message: "Admin access required" diff --git a/src/server/auth.ts b/src/server/auth.ts index 88295cf..43d2e80 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -6,7 +6,6 @@ import { getAuthSession } from "./session-helpers"; /** * Check authentication status - * Consolidates getUserID, getPrivilegeLevel, and checkAuthStatus into single function * @param event - H3Event * @returns Object with isAuthenticated, userId, and isAdmin flags */ diff --git a/src/server/conditional-parser.test.ts b/src/server/conditional-parser.test.ts index 9f6314b..adb5482 100644 --- a/src/server/conditional-parser.test.ts +++ b/src/server/conditional-parser.test.ts @@ -7,7 +7,7 @@ import { describe("parseConditionals", () => { const baseContext: ConditionalContext = { isAuthenticated: true, - privilegeLevel: "user", + isAdmin: false, userId: "test-user", currentDate: new Date("2025-06-01"), featureFlags: { "beta-feature": true }, @@ -34,7 +34,7 @@ describe("parseConditionals", () => { const anonContext: ConditionalContext = { ...baseContext, isAuthenticated: false, - privilegeLevel: "anonymous" + isAdmin: false }; const result = parseConditionals(html, anonContext); expect(result).not.toContain("Secret content"); @@ -51,7 +51,7 @@ describe("parseConditionals", () => { const adminContext: ConditionalContext = { ...baseContext, - privilegeLevel: "admin" + isAdmin: true }; const adminResult = parseConditionals(html, adminContext); expect(adminResult).toContain("Admin panel"); @@ -119,7 +119,7 @@ describe("parseConditionals", () => { const anonContext: ConditionalContext = { ...baseContext, isAuthenticated: false, - privilegeLevel: "anonymous" + isAdmin: false }; const anonResult = parseConditionals(html, anonContext); expect(anonResult).toContain("Not authenticated content"); diff --git a/src/server/conditional-parser.ts b/src/server/conditional-parser.ts index 7818ac4..f33c970 100644 --- a/src/server/conditional-parser.ts +++ b/src/server/conditional-parser.ts @@ -11,7 +11,7 @@ export function getSafeEnvVariables(): Record { export interface ConditionalContext { isAuthenticated: boolean; - privilegeLevel: "admin" | "user" | "anonymous"; + isAdmin: boolean; userId: string | null; currentDate: Date; featureFlags: Record; @@ -194,7 +194,16 @@ function evaluatePrivilegeCondition( value: string, context: ConditionalContext ): boolean { - return context.privilegeLevel === value; + switch (value) { + case "admin": + return context.isAdmin; + case "user": + return context.isAuthenticated && !context.isAdmin; + case "anonymous": + return !context.isAuthenticated; + default: + return false; + } } /** diff --git a/src/server/migrate-multi-auth.ts b/src/server/migrate-multi-auth.ts deleted file mode 100644 index eb2b170..0000000 --- a/src/server/migrate-multi-auth.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { ConnectionFactory } from "./database"; -import { v4 as uuidV4 } from "uuid"; - -/** - * Migration script to add multi-provider and enhanced session support - * Run this script once to migrate existing database - */ - -export async function migrateMultiAuth() { - const conn = ConnectionFactory(); - - - try { - // Step 1: Check if UserProvider table exists - const tableCheck = await conn.execute({ - sql: "SELECT name FROM sqlite_master WHERE type='table' AND name='UserProvider'" - }); - - if (tableCheck.rows.length > 0) { - - } else { - - await conn.execute(` - CREATE TABLE UserProvider ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - provider TEXT NOT NULL CHECK(provider IN ('email', 'google', 'github', 'apple')), - provider_user_id TEXT, - email TEXT, - display_name TEXT, - image TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - last_used_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE - ) - `); - - - await conn.execute( - "CREATE UNIQUE INDEX IF NOT EXISTS idx_user_provider_provider_user ON UserProvider (provider, provider_user_id)" - ); - await conn.execute( - "CREATE UNIQUE INDEX IF NOT EXISTS idx_user_provider_provider_email ON UserProvider (provider, email)" - ); - await conn.execute( - "CREATE INDEX IF NOT EXISTS idx_user_provider_user_id ON UserProvider (user_id)" - ); - await conn.execute( - "CREATE INDEX IF NOT EXISTS idx_user_provider_provider ON UserProvider (provider)" - ); - await conn.execute( - "CREATE INDEX IF NOT EXISTS idx_user_provider_email ON UserProvider (email)" - ); - } - - // Step 2: Check if Session table has device columns - const sessionColumnsCheck = await conn.execute({ - sql: "PRAGMA table_info(Session)" - }); - const hasDeviceName = sessionColumnsCheck.rows.some( - (row: any) => row.name === "device_name" - ); - - if (hasDeviceName) { - - } else { - - await conn.execute("ALTER TABLE Session ADD COLUMN device_name TEXT"); - await conn.execute("ALTER TABLE Session ADD COLUMN device_type TEXT"); - await conn.execute("ALTER TABLE Session ADD COLUMN browser TEXT"); - await conn.execute("ALTER TABLE Session ADD COLUMN os TEXT"); - - // SQLite doesn't support non-constant defaults in ALTER TABLE - // Add column with NULL default, then update existing rows - await conn.execute("ALTER TABLE Session ADD COLUMN last_active_at TEXT"); - - // Update existing rows to set last_active_at = last_used - - await conn.execute( - "UPDATE Session SET last_active_at = COALESCE(last_used, created_at) WHERE last_active_at IS NULL" - ); - - - await conn.execute( - "CREATE INDEX IF NOT EXISTS idx_session_last_active ON Session (last_active_at)" - ); - await conn.execute( - "CREATE INDEX IF NOT EXISTS idx_session_user_active ON Session (user_id, revoked, last_active_at)" - ); - } - - // Step 3: Migrate existing users to UserProvider table - - const usersResult = await conn.execute({ - sql: "SELECT id, email, provider, display_name, image, apple_user_string FROM User WHERE provider IS NOT NULL" - }); - - - - let migratedCount = 0; - for (const row of usersResult.rows) { - const user = row as any; - - // Skip apple provider users (they're for Life and Lineage mobile app, not website auth) - if (user.provider === "apple") { - - continue; - } - - // Check if already migrated - const existingProvider = await conn.execute({ - sql: "SELECT id FROM UserProvider WHERE user_id = ? AND provider = ?", - args: [user.id, user.provider || "email"] - }); - - if (existingProvider.rows.length > 0) { - - continue; - } - - // Determine provider_user_id based on provider type - let providerUserId: string | null = null; - if (user.provider === "github") { - providerUserId = user.display_name; - } else if (user.provider === "google") { - providerUserId = user.email; - } else { - providerUserId = user.email; - } - - try { - await conn.execute({ - sql: `INSERT INTO UserProvider (id, user_id, provider, provider_user_id, email, display_name, image) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - args: [ - uuidV4(), - user.id, - user.provider || "email", - providerUserId, - user.email, - user.display_name, - user.image - ] - }); - migratedCount++; - } catch (error: any) { - console.error( - `[Migration] Failed to migrate user ${user.id}:`, - error.message - ); - } - } - - // Determine provider_user_id based on provider type - let providerUserId: string | null = null; - if (user.provider === "github") { - providerUserId = user.display_name; - } else if (user.provider === "google") { - providerUserId = user.email; - } else if (user.provider === "apple") { - providerUserId = user.apple_user_string; - } else { - providerUserId = user.email; - } - - try { - await conn.execute({ - sql: `INSERT INTO UserProvider (id, user_id, provider, provider_user_id, email, display_name, image) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - args: [ - uuidV4(), - user.id, - user.provider || "email", - providerUserId, - user.email, - user.display_name, - user.image - ] - }); - migratedCount++; - } catch (error: any) { - console.error( - `[Migration] Failed to migrate user ${user.id}:`, - error.message - ); - } - } - - - - // Step 4: Verification - - const providerCount = await conn.execute({ - sql: "SELECT COUNT(*) as count FROM UserProvider" - }); - - - const multiProviderUsers = await conn.execute({ - sql: `SELECT COUNT(*) as count FROM ( - SELECT user_id FROM UserProvider GROUP BY user_id HAVING COUNT(*) > 1 - )` - }); - - - - return { - success: true, - migratedUsers: migratedCount, - totalProviders: (providerCount.rows[0] as any).count - }; - } catch (error) { - - throw error; - } -} - -// Run migration if called directly -if (require.main === module) { - migrateMultiAuth() - .then((result) => { - - process.exit(0); - }) - .catch((error) => { - - process.exit(1); - }); -} diff --git a/src/server/session-helpers.ts b/src/server/session-helpers.ts index 5b023d6..f7dfcaf 100644 --- a/src/server/session-helpers.ts +++ b/src/server/session-helpers.ts @@ -90,7 +90,6 @@ export function hashRefreshToken(token: string): string { * Create a new session in database and Vinxi session * @param event - H3Event * @param userId - User ID - * @param isAdmin - Whether user is admin * @param rememberMe - Whether to use extended session duration * @param ipAddress - Client IP address * @param userAgent - Client user agent string @@ -101,7 +100,6 @@ export function hashRefreshToken(token: string): string { export async function createAuthSession( event: H3Event, userId: string, - isAdmin: boolean, rememberMe: boolean, ipAddress: string, userAgent: string, @@ -109,6 +107,19 @@ export async function createAuthSession( tokenFamily: string | null = null ): Promise { const conn = ConnectionFactory(); + + // Fetch is_admin from database + const userResult = await conn.execute({ + sql: "SELECT is_admin FROM User WHERE id = ?", + args: [userId] + }); + + if (userResult.rows.length === 0) { + throw new Error(`User not found: ${userId}`); + } + + const isAdmin = userResult.rows[0].is_admin === 1; + const sessionId = uuidV4(); const family = tokenFamily || uuidV4(); const refreshToken = generateRefreshToken(); @@ -374,10 +385,10 @@ async function restoreSessionFromDB( try { const conn = ConnectionFactory(); - // Query DB for session with all necessary data + // Query DB for session with all necessary data including is_admin const result = await conn.execute({ sql: `SELECT s.id, s.user_id, s.token_family, s.refresh_token_hash, - s.revoked, s.expires_at, u.isAdmin + s.revoked, s.expires_at, u.is_admin FROM Session s JOIN User u ON s.user_id = u.id WHERE s.id = ?`, @@ -412,7 +423,6 @@ async function restoreSessionFromDB( const newSession = await createAuthSession( event, dbSession.user_id as string, - dbSession.isAdmin === 1, true, // Assume rememberMe=true for restoration ipAddress, userAgent, @@ -678,7 +688,6 @@ export async function rotateAuthSession( const newSessionData = await createAuthSession( event, oldSessionData.userId, - oldSessionData.isAdmin, oldSessionData.rememberMe, ipAddress, userAgent, diff --git a/src/types/comment.ts b/src/types/comment.ts index 6b4b78d..2e1818c 100644 --- a/src/types/comment.ts +++ b/src/types/comment.ts @@ -55,8 +55,6 @@ export interface BackupResponse { commentParent?: number | null; } -export type PrivilegeLevel = "admin" | "user" | "anonymous"; - export type SortingMode = "newest" | "oldest" | "highest_rated" | "hot"; export type DeletionType = "user" | "admin" | "database"; @@ -64,7 +62,8 @@ export type DeletionType = "user" | "admin" | "database"; export type ModificationType = "delete" | "edit"; export interface CommentSectionWrapperProps { - privilegeLevel: PrivilegeLevel; + isAuthenticated: boolean; + isAdmin: boolean; allComments: Comment[]; topLevelComments: Comment[]; id: number; @@ -74,7 +73,8 @@ export interface CommentSectionWrapperProps { } export interface CommentSectionProps { - privilegeLevel: PrivilegeLevel; + isAuthenticated: boolean; + isAdmin: boolean; postID: number; allComments: Comment[]; topLevelComments: Comment[]; @@ -101,7 +101,8 @@ export interface CommentBlockProps { recursionCount: number; allComments: Comment[] | undefined; child_comments: Comment[] | undefined; - privilegeLevel: PrivilegeLevel; + isAuthenticated: boolean; + isAdmin: boolean; currentUserID: string; reactionMap: Map; level: number; @@ -124,7 +125,7 @@ export interface CommentBlockProps { export interface CommentInputBlockProps { isReply: boolean; parent_id?: number; - privilegeLevel: PrivilegeLevel; + isAuthenticated: boolean; post_id: number; socket: WebSocket | undefined; currentUserID: string; @@ -134,7 +135,8 @@ export interface CommentInputBlockProps { export interface CommentSortingProps { topLevelComments: Comment[]; - privilegeLevel: PrivilegeLevel; + isAuthenticated: boolean; + isAdmin: boolean; postID: number; allComments: Comment[]; reactionMap: Map; @@ -170,14 +172,14 @@ export interface ReactionBarProps { currentUserID: string; commentID: number; reactions: CommentReaction[]; - privilegeLevel: PrivilegeLevel; + isAuthenticated: boolean; showingReactionOptions: boolean; commentReaction: (reactionType: ReactionType, commentID: number) => void; } export interface CommentDeletionPromptProps { isOpen: boolean; - privilegeLevel: PrivilegeLevel; + isAdmin: boolean; commentID: number; commenterID: string; deleteComment: (