session state simplification

This commit is contained in:
Michael Freno
2026-01-12 09:24:58 -05:00
parent ed16b277f7
commit f68f1f462a
32 changed files with 132 additions and 381 deletions

View File

@@ -7,14 +7,14 @@ const DeletePostButton = lazy(() => import("./DeletePostButton"));
export interface CardProps { export interface CardProps {
post: PostCardData; post: PostCardData;
privilegeLevel: "anonymous" | "admin" | "user"; isAdmin: boolean;
index?: number; index?: number;
} }
export default function Card(props: CardProps) { export default function Card(props: CardProps) {
return ( return (
<div class="bg-base border-text relative z-0 mx-auto h-96 w-full overflow-hidden rounded-lg border shadow-lg lg:w-5/6 xl:w-3/4"> <div class="bg-base border-text relative z-0 mx-auto h-96 w-full overflow-hidden rounded-lg border shadow-lg lg:w-5/6 xl:w-3/4">
<Show when={props.privilegeLevel === "admin"}> <Show when={props.isAdmin}>
<div class="border-opacity-20 bg-opacity-40 border-text bg-text absolute top-0 w-full border-b px-2 py-4 backdrop-blur-md md:px-6"> <div class="border-opacity-20 bg-opacity-40 border-text bg-text absolute top-0 w-full border-b px-2 py-4 backdrop-blur-md md:px-6">
<div class="flex justify-between"> <div class="flex justify-between">
<Show when={!props.post.published}> <Show when={!props.post.published}>
@@ -60,7 +60,7 @@ export default function Card(props: CardProps) {
</div> </div>
<CardLinks <CardLinks
postTitle={props.post.title} postTitle={props.post.title}
privilegeLevel={props.privilegeLevel} isAdmin={props.isAdmin}
postID={props.post.id} postID={props.post.id}
/> />
</div> </div>

View File

@@ -5,7 +5,7 @@ import { Spinner } from "~/components/Spinner";
export interface CardLinksProps { export interface CardLinksProps {
postTitle: string; postTitle: string;
postID: number; postID: number;
privilegeLevel: string; isAdmin: boolean;
} }
export default function CardLinks(props: CardLinksProps) { export default function CardLinks(props: CardLinksProps) {
@@ -25,7 +25,7 @@ export default function CardLinks(props: CardLinksProps) {
<Spinner size={24} /> <Spinner size={24} />
</Show> </Show>
</A> </A>
<Show when={props.privilegeLevel === "admin"}> <Show when={props.isAdmin}>
<A <A
href={`/blog/edit/${props.postID}`} href={`/blog/edit/${props.postID}`}
onClick={() => setEditLoading(true)} onClick={() => setEditLoading(true)}

View File

@@ -122,13 +122,10 @@ export default function CommentBlock(props: CommentBlockProps) {
); );
const canDelete = () => const canDelete = () =>
props.currentUserID === props.comment.commenter_id || props.currentUserID === props.comment.commenter_id || props.isAdmin;
props.privilegeLevel === "admin";
const canEdit = () => props.currentUserID === props.comment.commenter_id; const canEdit = () => props.currentUserID === props.comment.commenter_id;
const isAnonymous = () => props.privilegeLevel === "anonymous";
const replyIconColor = () => "var(--color-peach)"; const replyIconColor = () => "var(--color-peach)";
return ( return (
@@ -163,12 +160,12 @@ export default function CommentBlock(props: CommentBlockProps) {
hasUpvoted() hasUpvoted()
? "fill-green" ? "fill-green"
: `fill-text hover:fill-green ${ : `fill-text hover:fill-green ${
isAnonymous() ? "tooltip z-50" : "" !props.isAuthenticated ? "tooltip z-50" : ""
}` }`
}`} }`}
> >
<ThumbsUpEmoji /> <ThumbsUpEmoji />
<Show when={isAnonymous()}> <Show when={!props.isAuthenticated}>
<div class="tooltip-text -ml-16 w-32 text-white"> <div class="tooltip-text -ml-16 w-32 text-white">
You must be logged in You must be logged in
</div> </div>
@@ -190,14 +187,14 @@ export default function CommentBlock(props: CommentBlockProps) {
hasDownvoted() hasDownvoted()
? "fill-red" ? "fill-red"
: `fill-text hover:fill-red ${ : `fill-text hover:fill-red ${
isAnonymous() ? "tooltip z-50" : "" !props.isAuthenticated ? "tooltip z-50" : ""
}` }`
}`} }`}
> >
<div class="rotate-180"> <div class="rotate-180">
<ThumbsUpEmoji /> <ThumbsUpEmoji />
</div> </div>
<Show when={isAnonymous()}> <Show when={!props.isAuthenticated}>
<div class="tooltip-text -ml-16 w-32"> <div class="tooltip-text -ml-16 w-32">
You must be logged in You must be logged in
</div> </div>
@@ -309,7 +306,7 @@ export default function CommentBlock(props: CommentBlockProps) {
currentUserID={props.currentUserID} currentUserID={props.currentUserID}
reactions={reactions()} reactions={reactions()}
showingReactionOptions={showingReactionOptions()} showingReactionOptions={showingReactionOptions()}
privilegeLevel={props.privilegeLevel} isAuthenticated={props.isAuthenticated}
commentReaction={props.commentReaction} commentReaction={props.commentReaction}
/> />
</div> </div>
@@ -325,7 +322,7 @@ export default function CommentBlock(props: CommentBlockProps) {
> >
<CommentInputBlock <CommentInputBlock
isReply={true} isReply={true}
privilegeLevel={props.privilegeLevel} isAuthenticated={props.isAuthenticated}
parent_id={props.comment.id} parent_id={props.comment.id}
post_id={props.projectID} post_id={props.projectID}
currentUserID={props.currentUserID} currentUserID={props.currentUserID}
@@ -348,7 +345,8 @@ export default function CommentBlock(props: CommentBlockProps) {
child_comments={props.allComments?.filter( child_comments={props.allComments?.filter(
(comment) => comment.parent_comment_id === childComment.id (comment) => comment.parent_comment_id === childComment.id
)} )}
privilegeLevel={props.privilegeLevel} isAuthenticated={props.isAuthenticated}
isAdmin={props.isAdmin}
currentUserID={props.currentUserID} currentUserID={props.currentUserID}
reactionMap={props.reactionMap} reactionMap={props.reactionMap}
level={props.level + 1} level={props.level + 1}

View File

@@ -84,13 +84,11 @@ export default function CommentDeletionPrompt(
onChange={handleNormalDeleteCheckbox} onChange={handleNormalDeleteCheckbox}
/> />
<div class="my-auto px-2 text-sm font-normal"> <div class="my-auto px-2 text-sm font-normal">
{props.privilegeLevel === "admin" {props.isAdmin ? "Confirm User Delete?" : "Confirm Delete?"}
? "Confirm User Delete?"
: "Confirm Delete?"}
</div> </div>
</div> </div>
</div> </div>
<Show when={props.privilegeLevel === "admin"}> <Show when={props.isAdmin}>
<div class="flex w-full justify-center"> <div class="flex w-full justify-center">
<div class="flex pt-4"> <div class="flex pt-4">
<input <input

View File

@@ -18,7 +18,7 @@ export default function CommentInputBlock(props: CommentInputBlockProps) {
} }
}; };
if (props.privilegeLevel === "user" || props.privilegeLevel === "admin") { if (props.isAuthenticated) {
return ( return (
<div class="flex w-full justify-center select-none"> <div class="flex w-full justify-center select-none">
<div class="h-fit w-3/4"> <div class="h-fit w-3/4">

View File

@@ -6,7 +6,6 @@ import type {
UserPublicData, UserPublicData,
ReactionType, ReactionType,
ModificationType, ModificationType,
PrivilegeLevel,
SortingMode SortingMode
} from "~/types/comment"; } from "~/types/comment";
import CommentInputBlock from "./CommentInputBlock"; import CommentInputBlock from "./CommentInputBlock";
@@ -20,28 +19,6 @@ const COMMENT_SORTING_OPTIONS: { val: SortingMode }[] = [
{ val: "hot" } { val: "hot" }
]; ];
interface CommentSectionProps {
privilegeLevel: PrivilegeLevel;
allComments: Comment[];
topLevelComments: Comment[];
postID: number;
reactionMap: Map<number, CommentReaction[]>;
currentUserID: string;
userCommentMap: Map<UserPublicData, number[]> | undefined;
newComment: (commentBody: string, parentCommentID?: number) => Promise<void>;
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) { export default function CommentSection(props: CommentSectionProps) {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -66,7 +43,7 @@ export default function CommentSection(props: CommentSectionProps) {
<div class="mb-1"> <div class="mb-1">
<CommentInputBlock <CommentInputBlock
isReply={false} isReply={false}
privilegeLevel={props.privilegeLevel} isAuthenticated={props.isAuthenticated}
post_id={props.postID} post_id={props.postID}
socket={undefined} socket={undefined}
currentUserID={props.currentUserID} currentUserID={props.currentUserID}
@@ -90,7 +67,8 @@ export default function CommentSection(props: CommentSectionProps) {
<div id="comments"> <div id="comments">
<CommentSorting <CommentSorting
topLevelComments={props.topLevelComments} topLevelComments={props.topLevelComments}
privilegeLevel={props.privilegeLevel} isAuthenticated={props.isAuthenticated}
isAdmin={props.isAdmin}
postID={props.postID} postID={props.postID}
allComments={props.allComments} allComments={props.allComments}
reactionMap={props.reactionMap} reactionMap={props.reactionMap}

View File

@@ -855,7 +855,8 @@ export default function CommentSectionWrapper(
</Show> </Show>
<CommentSection <CommentSection
privilegeLevel={props.privilegeLevel} isAuthenticated={props.isAuthenticated}
isAdmin={props.isAdmin}
allComments={allComments()} allComments={allComments()}
topLevelComments={topLevelComments()} topLevelComments={topLevelComments()}
postID={props.id} postID={props.id}
@@ -875,7 +876,7 @@ export default function CommentSectionWrapper(
commenterImage={commenterImageForModification()} commenterImage={commenterImageForModification()}
commenterEmail={commenterEmailForModification()} commenterEmail={commenterEmailForModification()}
commenterDisplayName={commenterDisplayNameForModification()} commenterDisplayName={commenterDisplayNameForModification()}
privilegeLevel={props.privilegeLevel} isAdmin={props.isAdmin}
commentDeletionLoading={commentDeletionLoading()} commentDeletionLoading={commentDeletionLoading()}
deleteComment={deleteComment} deleteComment={deleteComment}
onClose={() => { onClose={() => {

View File

@@ -51,7 +51,8 @@ export default function CommentSorting(props: CommentSortingProps) {
child_comments={props.allComments?.filter( child_comments={props.allComments?.filter(
(comment) => comment.parent_comment_id === topLevelComment.id (comment) => comment.parent_comment_id === topLevelComment.id
)} )}
privilegeLevel={props.privilegeLevel} isAuthenticated={props.isAuthenticated}
isAdmin={props.isAdmin}
currentUserID={props.currentUserID} currentUserID={props.currentUserID}
reactionMap={props.reactionMap} reactionMap={props.reactionMap}
level={0} level={0}

View File

@@ -10,7 +10,7 @@ export interface Tag {
export interface PostSortingProps { export interface PostSortingProps {
posts: PostCardData[]; posts: PostCardData[];
tags: Tag[]; tags: Tag[];
privilegeLevel: "anonymous" | "admin" | "user"; isAdmin: boolean;
filters?: string; filters?: string;
sort?: string; sort?: string;
include?: string; include?: string;
@@ -36,7 +36,7 @@ export default function PostSorting(props: PostSortingProps) {
const filteredPosts = createMemo(() => { const filteredPosts = createMemo(() => {
let filtered = props.posts; let filtered = props.posts;
if (props.privilegeLevel === "admin" && props.status) { if (props.isAdmin && props.status) {
if (props.status === "published") { if (props.status === "published") {
filtered = filtered.filter((post) => post.published === 1); filtered = filtered.filter((post) => post.published === 1);
} else if (props.status === "unpublished") { } else if (props.status === "unpublished") {
@@ -141,11 +141,7 @@ export default function PostSorting(props: PostSortingProps) {
<For each={sortedPosts()}> <For each={sortedPosts()}>
{(post, index) => ( {(post, index) => (
<div class="my-4"> <div class="my-4">
<Card <Card post={post} isAdmin={props.isAdmin} index={index()} />
post={post}
privilegeLevel={props.privilegeLevel}
index={index()}
/>
</div> </div>
)} )}
</For> </For>

View File

@@ -10,7 +10,7 @@ export interface PostLike {
export interface SessionDependantLikeProps { export interface SessionDependantLikeProps {
currentUserID: string | undefined | null; currentUserID: string | undefined | null;
privilegeLevel: "admin" | "user" | "anonymous"; isAuthenticated: boolean;
likes: PostLike[]; likes: PostLike[];
projectID: number; projectID: number;
} }
@@ -61,7 +61,7 @@ export default function SessionDependantLike(props: SessionDependantLikeProps) {
return ( return (
<Show <Show
when={props.privilegeLevel !== "anonymous"} when={props.isAuthenticated}
fallback={ fallback={
<button class="tooltip flex flex-col"> <button class="tooltip flex flex-col">
<div class="mx-auto"> <div class="mx-auto">

View File

@@ -65,7 +65,7 @@ export const AuthProvider: ParentComponent = (props) => {
const email = () => serverAuth()?.email ?? null; const email = () => serverAuth()?.email ?? null;
const displayName = () => serverAuth()?.displayName ?? null; const displayName = () => serverAuth()?.displayName ?? null;
const userId = () => serverAuth()?.userId ?? null; const userId = () => serverAuth()?.userId ?? null;
const isAdmin = () => serverAuth()?.privilegeLevel === "admin"; const isAdmin = () => serverAuth()?.isAdmin ?? false;
const isEmailVerified = () => serverAuth()?.emailVerified ?? false; const isEmailVerified = () => serverAuth()?.emailVerified ?? false;
// Server handles all token refresh logic // Server handles all token refresh logic

View File

@@ -9,6 +9,7 @@ export const model: { [key: string]: string } = {
display_name TEXT, display_name TEXT,
provider TEXT, provider TEXT,
image TEXT, image TEXT,
is_admin INTEGER DEFAULT 0,
registered_at TEXT NOT NULL DEFAULT (datetime('now')), registered_at TEXT NOT NULL DEFAULT (datetime('now')),
failed_attempts INTEGER DEFAULT 0, failed_attempts INTEGER DEFAULT 0,
locked_until TEXT locked_until TEXT

4
src/env/server.ts vendored
View File

@@ -2,8 +2,6 @@ import { z } from "zod";
const serverEnvSchema = z.object({ const serverEnvSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]), 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), JWT_SECRET_KEY: z.string().min(1),
AWS_REGION: z.string().min(1), AWS_REGION: z.string().min(1),
AWS_S3_BUCKET_NAME: 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[] => { export const getMissingEnvVars = (): string[] => {
const requiredServerVars = [ const requiredServerVars = [
"NODE_ENV", "NODE_ENV",
"ADMIN_EMAIL",
"ADMIN_ID",
"JWT_SECRET_KEY", "JWT_SECRET_KEY",
"AWS_REGION", "AWS_REGION",
"AWS_S3_BUCKET_NAME", "AWS_S3_BUCKET_NAME",

View File

@@ -7,7 +7,7 @@ export interface UserState {
email: string | null; email: string | null;
displayName: string | null; displayName: string | null;
emailVerified: boolean; emailVerified: boolean;
privilegeLevel: "admin" | "user" | "anonymous"; isAdmin: boolean;
} }
/** /**
@@ -29,7 +29,7 @@ export const getUserState = query(async (): Promise<UserState> => {
email: null, email: null,
displayName: null, displayName: null,
emailVerified: false, emailVerified: false,
privilegeLevel: "anonymous" isAdmin: false
}; };
} }
@@ -42,7 +42,7 @@ export const getUserState = query(async (): Promise<UserState> => {
email: null, email: null,
displayName: null, displayName: null,
emailVerified: false, emailVerified: false,
privilegeLevel: "anonymous" isAdmin: false
}; };
} }
@@ -59,7 +59,7 @@ export const getUserState = query(async (): Promise<UserState> => {
email: null, email: null,
displayName: null, displayName: null,
emailVerified: false, emailVerified: false,
privilegeLevel: "anonymous" isAdmin: false
}; };
} }
@@ -71,7 +71,7 @@ export const getUserState = query(async (): Promise<UserState> => {
email: user.email ?? null, email: user.email ?? null,
displayName: user.display_name ?? null, displayName: user.display_name ?? null,
emailVerified: user.email_verified === 1, emailVerified: user.email_verified === 1,
privilegeLevel: auth.isAdmin ? "admin" : "user" isAdmin: auth.isAdmin
}; };
}, "user-auth-state"); }, "user-auth-state");

View File

@@ -50,15 +50,14 @@ export function isValidCommentBody(body: string): boolean {
export function canModifyComment( export function canModifyComment(
userID: string, userID: string,
commenterID: string, commenterID: string,
privilegeLevel: "admin" | "user" | "anonymous" isAuthenticated: boolean,
isAdmin: boolean
): boolean { ): boolean {
if (privilegeLevel === "admin") return true; if (isAdmin) return true;
if (privilegeLevel === "anonymous") return false; if (!isAuthenticated) return false;
return userID === commenterID; return userID === commenterID;
} }
export function canDatabaseDelete( export function canDatabaseDelete(isAdmin: boolean): boolean {
privilegeLevel: "admin" | "user" | "anonymous" return isAdmin;
): boolean {
return privilegeLevel === "admin";
} }

View File

@@ -8,7 +8,7 @@ const checkAdmin = query(async (): Promise<boolean> => {
const { getUserState } = await import("~/lib/auth-query"); const { getUserState } = await import("~/lib/auth-query");
const userState = await getUserState(); const userState = await getUserState();
if (userState.privilegeLevel !== "admin") { if (!userState.isAdmin) {
console.log("redirect"); console.log("redirect");
throw redirect("/"); throw redirect("/");
} }

View File

@@ -34,7 +34,8 @@ const getPostByTitle = query(
const { getFeatureFlags } = await import("~/server/feature-flags"); const { getFeatureFlags } = await import("~/server/feature-flags");
const event = getRequestEvent()!; const event = getRequestEvent()!;
const userState = await getUserState(); const userState = await getUserState();
const privilegeLevel = userState.privilegeLevel; const isAuthenticated = userState.isAuthenticated;
const isAdmin = userState.isAdmin;
const userID = userState.userId; const userID = userState.userId;
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -51,7 +52,8 @@ const getPostByTitle = query(
tags: [], tags: [],
userCommentArray: [], userCommentArray: [],
reactionArray: [], reactionArray: [],
privilegeLevel: "anonymous" as const, isAuthenticated: false,
isAdmin: false,
userID: null userID: null
}; };
} }
@@ -77,13 +79,14 @@ const getPostByTitle = query(
tags: [], tags: [],
userCommentArray: [], userCommentArray: [],
reactionArray: [], reactionArray: [],
privilegeLevel: "anonymous" as const, isAuthenticated: false,
isAdmin: false,
userID: null userID: null
}; };
} }
let query = "SELECT * FROM Post WHERE title = ?"; let query = "SELECT * FROM Post WHERE title = ?";
if (privilegeLevel !== "admin") { if (!isAdmin) {
query += ` AND published = TRUE`; query += ` AND published = TRUE`;
} }
@@ -110,7 +113,8 @@ const getPostByTitle = query(
tags: [], tags: [],
userCommentArray: [], userCommentArray: [],
reactionArray: [], reactionArray: [],
privilegeLevel: "anonymous" as const, isAuthenticated: false,
isAdmin: false,
userID: null userID: null
}; };
} }
@@ -123,14 +127,15 @@ const getPostByTitle = query(
tags: [], tags: [],
userCommentArray: [], userCommentArray: [],
reactionArray: [], reactionArray: [],
privilegeLevel: "anonymous" as const, isAuthenticated: false,
isAdmin: false,
userID: null userID: null
}; };
} }
const conditionalContext = { const conditionalContext = {
isAuthenticated: userID !== null, isAuthenticated: userID !== null,
privilegeLevel: privilegeLevel, isAdmin: isAdmin,
userId: userID, userId: userID,
currentDate: new Date(), currentDate: new Date(),
featureFlags: getFeatureFlags(), featureFlags: getFeatureFlags(),
@@ -245,7 +250,8 @@ const getPostByTitle = query(
topLevelComments, topLevelComments,
userCommentArray, userCommentArray,
reactionArray, reactionArray,
privilegeLevel, isAuthenticated,
isAdmin,
userID, userID,
sortBy, sortBy,
reads: post.reads || 0 reads: post.reads || 0
@@ -429,7 +435,7 @@ export default function PostPage() {
<div> <div>
<SessionDependantLike <SessionDependantLike
currentUserID={postData.userID} currentUserID={postData.userID}
privilegeLevel={postData.privilegeLevel} isAuthenticated={postData.isAuthenticated}
likes={postData.likes as any[]} likes={postData.likes as any[]}
projectID={p().id} projectID={p().id}
/> />
@@ -455,7 +461,8 @@ export default function PostPage() {
class="mx-4 pt-12 pb-12 md:mx-8 lg:mx-12" class="mx-4 pt-12 pb-12 md:mx-8 lg:mx-12"
> >
<CommentSectionWrapper <CommentSectionWrapper
privilegeLevel={postData.privilegeLevel} isAuthenticated={postData.isAuthenticated}
isAdmin={postData.isAdmin}
allComments={postData.comments as Comment[]} allComments={postData.comments as Comment[]}
topLevelComments={ topLevelComments={
postData.topLevelComments as Comment[] postData.topLevelComments as Comment[]

View File

@@ -13,7 +13,7 @@ const checkAdminAccess = query(async () => {
// Reuse shared auth query for consistency // Reuse shared auth query for consistency
const userState = await getUserState(); const userState = await getUserState();
if (userState.privilegeLevel !== "admin") { if (!userState.isAdmin) {
throw redirect("/401"); throw redirect("/401");
} }

View File

@@ -12,7 +12,7 @@ const getPostForEdit = query(async (id: string) => {
const { ConnectionFactory } = await import("~/server/utils"); const { ConnectionFactory } = await import("~/server/utils");
const userState = await getUserState(); const userState = await getUserState();
if (userState.privilegeLevel !== "admin") { if (!userState.isAdmin) {
throw redirect("/401"); throw redirect("/401");
} }
@@ -35,7 +35,7 @@ const getPostForEdit = query(async (id: string) => {
return { return {
post, post,
tags, tags,
privilegeLevel: userState.privilegeLevel, isAdmin: userState.isAdmin,
userID: userState.userId userID: userState.userId
}; };
}, "post-for-edit"); }, "post-for-edit");

View File

@@ -15,10 +15,10 @@ const getPosts = query(async () => {
const { ConnectionFactory } = await import("~/server/utils"); const { ConnectionFactory } = await import("~/server/utils");
const { withCacheAndStale } = await import("~/server/cache"); const { withCacheAndStale } = await import("~/server/cache");
const userState = await getUserState(); const userState = await getUserState();
const privilegeLevel = userState.privilegeLevel; const isAdmin = userState.isAdmin;
return withCacheAndStale( return withCacheAndStale(
`posts-${privilegeLevel}`, `posts-${isAdmin ? "admin" : "user"}`,
CACHE_CONFIG.BLOG_POSTS_LIST_CACHE_TTL_MS, CACHE_CONFIG.BLOG_POSTS_LIST_CACHE_TTL_MS,
async () => { async () => {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -43,7 +43,7 @@ const getPosts = query(async () => {
LEFT JOIN Comment c ON p.id = c.post_id LEFT JOIN Comment c ON p.id = c.post_id
`; `;
if (privilegeLevel !== "admin") { if (!isAdmin) {
postsQuery += ` WHERE p.published = TRUE`; postsQuery += ` WHERE p.published = TRUE`;
} }
@@ -57,7 +57,7 @@ const getPosts = query(async () => {
SELECT t.value, t.post_id SELECT t.value, t.post_id
FROM Tag t FROM Tag t
JOIN Post p ON t.post_id = p.id 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 ORDER BY t.value ASC
`; `;
@@ -70,7 +70,7 @@ const getPosts = query(async () => {
tagMap[key] = (tagMap[key] || 0) + 1; tagMap[key] = (tagMap[key] || 0) + 1;
}); });
return { posts, tags, tagMap, privilegeLevel }; return { posts, tags, tagMap, isAdmin };
} }
); );
}, "posts"); }, "posts");
@@ -106,11 +106,11 @@ export default function BlogIndex() {
<TagSelector tagMap={loadedData().tagMap} /> <TagSelector tagMap={loadedData().tagMap} />
</Show> </Show>
<Show when={loadedData().privilegeLevel === "admin"}> <Show when={loadedData().isAdmin}>
<PublishStatusToggle /> <PublishStatusToggle />
</Show> </Show>
<Show when={loadedData().privilegeLevel === "admin"}> <Show when={loadedData().isAdmin}>
<div class="mt-2 flex justify-center md:mt-0 md:justify-end"> <div class="mt-2 flex justify-center md:mt-0 md:justify-end">
<A <A
href="/blog/create" href="/blog/create"
@@ -130,7 +130,7 @@ export default function BlogIndex() {
<PostSorting <PostSorting
posts={loadedData().posts} posts={loadedData().posts}
tags={loadedData().tags} tags={loadedData().tags}
privilegeLevel={loadedData().privilegeLevel} isAdmin={loadedData().isAdmin}
filters={filters()} filters={filters()}
sort={sort()} sort={sort()}
include={include()} include={include()}

View File

@@ -7,7 +7,7 @@ import { getUserState } from "~/lib/auth-query";
const checkAdminAccess = query(async () => { const checkAdminAccess = query(async () => {
"use server"; "use server";
const userState = await getUserState(); const userState = await getUserState();
return { privilegeLevel: userState.privilegeLevel }; return { isAdmin: userState.isAdmin };
}, "test-auth-state"); }, "test-auth-state");
type EndpointTest = { type EndpointTest = {
@@ -916,7 +916,7 @@ export default function TestPage() {
description="tRPC API testing dashboard for developers to test endpoints and verify functionality." description="tRPC API testing dashboard for developers to test endpoints and verify functionality."
/> />
<Show <Show
when={authState()?.privilegeLevel === "admin"} when={authState()?.isAdmin}
fallback={ fallback={
<div class="w-full pt-[30vh] text-center"> <div class="w-full pt-[30vh] text-center">
<div class="text-text text-2xl">Unauthorized</div> <div class="text-text text-2xl">Unauthorized</div>

View File

@@ -306,14 +306,11 @@ export const authRouter = createTRPCRouter({
} }
} }
const isAdmin = userId === env.ADMIN_ID;
const clientIP = getClientIP(getH3Event(ctx)); const clientIP = getClientIP(getH3Event(ctx));
const userAgent = getUserAgent(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx));
await createAuthSession( await createAuthSession(
getH3Event(ctx), getH3Event(ctx),
userId, userId,
isAdmin,
true, // OAuth defaults to remember true, // OAuth defaults to remember
clientIP, clientIP,
userAgent userAgent
@@ -521,15 +518,12 @@ export const authRouter = createTRPCRouter({
} }
} }
const isAdmin = userId === env.ADMIN_ID;
// Create session with Vinxi (OAuth defaults to remember me) // Create session with Vinxi (OAuth defaults to remember me)
const clientIP = getClientIP(getH3Event(ctx)); const clientIP = getClientIP(getH3Event(ctx));
const userAgent = getUserAgent(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx));
await createAuthSession( await createAuthSession(
getH3Event(ctx), getH3Event(ctx),
userId, userId,
isAdmin,
true, // OAuth defaults to remember true, // OAuth defaults to remember
clientIP, clientIP,
userAgent userAgent
@@ -647,7 +641,6 @@ export const authRouter = createTRPCRouter({
} }
const userId = (res.rows[0] as unknown as User).id; const userId = (res.rows[0] as unknown as User).id;
const isAdmin = userId === env.ADMIN_ID;
const clientIP = getClientIP(getH3Event(ctx)); const clientIP = getClientIP(getH3Event(ctx));
const userAgent = getUserAgent(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx));
@@ -655,7 +648,6 @@ export const authRouter = createTRPCRouter({
await createAuthSession( await createAuthSession(
getH3Event(ctx), getH3Event(ctx),
userId, userId,
isAdmin,
rememberMe, rememberMe,
clientIP, clientIP,
userAgent userAgent
@@ -780,7 +772,6 @@ export const authRouter = createTRPCRouter({
} }
const userId = (res.rows[0] as unknown as User).id; 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 // Use rememberMe from JWT if not provided in input, default to false
const shouldRemember = const shouldRemember =
@@ -791,7 +782,6 @@ export const authRouter = createTRPCRouter({
await createAuthSession( await createAuthSession(
getH3Event(ctx), getH3Event(ctx),
userId, userId,
isAdmin,
shouldRemember, shouldRemember,
clientIP, clientIP,
userAgent userAgent
@@ -983,12 +973,10 @@ export const authRouter = createTRPCRouter({
// Create session with client info // Create session with client info
const clientIP = getClientIP(getH3Event(ctx)); const clientIP = getClientIP(getH3Event(ctx));
const userAgent = getUserAgent(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx));
const isAdmin = userId === env.ADMIN_ID;
await createAuthSession( await createAuthSession(
getH3Event(ctx), getH3Event(ctx),
userId, userId,
isAdmin,
true, // Always use persistent sessions true, // Always use persistent sessions
clientIP, clientIP,
userAgent userAgent
@@ -1150,14 +1138,11 @@ export const authRouter = createTRPCRouter({
// Reset rate limits on successful login // Reset rate limits on successful login
await resetLoginRateLimits(email, clientIP); await resetLoginRateLimits(email, clientIP);
const isAdmin = user.id === env.ADMIN_ID;
// Create session with Vinxi // Create session with Vinxi
const userAgent = getUserAgent(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx));
await createAuthSession( await createAuthSession(
getH3Event(ctx), getH3Event(ctx),
user.id, user.id,
isAdmin,
rememberMe ?? false, // Default to session cookie (expires on browser close) rememberMe ?? false, // Default to session cookie (expires on browser close)
clientIP, clientIP,
userAgent userAgent

View File

@@ -7,9 +7,9 @@ import { CACHE_CONFIG } from "~/config";
const BLOG_CACHE_TTL = CACHE_CONFIG.BLOG_CACHE_TTL_MS; const BLOG_CACHE_TTL = CACHE_CONFIG.BLOG_CACHE_TTL_MS;
const getAllPostsData = async (privilegeLevel: string) => { const getAllPostsData = async (isAdmin: boolean) => {
return withCacheAndStale( return withCacheAndStale(
`blog-posts-${privilegeLevel}`, `blog-posts-${isAdmin ? "admin" : "public"}`,
BLOG_CACHE_TTL, BLOG_CACHE_TTL,
async () => { async () => {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -34,7 +34,7 @@ const getAllPostsData = async (privilegeLevel: string) => {
LEFT JOIN Comment c ON p.id = c.post_id LEFT JOIN Comment c ON p.id = c.post_id
`; `;
if (privilegeLevel !== "admin") { if (!isAdmin) {
postsQuery += ` WHERE p.published = TRUE`; postsQuery += ` WHERE p.published = TRUE`;
} }
@@ -48,7 +48,7 @@ const getAllPostsData = async (privilegeLevel: string) => {
SELECT t.value, t.post_id SELECT t.value, t.post_id
FROM Tag t FROM Tag t
JOIN Post p ON t.post_id = p.id 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 ORDER BY t.value ASC
`; `;
@@ -64,21 +64,21 @@ const getAllPostsData = async (privilegeLevel: string) => {
tagMap[key] = (tagMap[key] || 0) + 1; tagMap[key] = (tagMap[key] || 0) + 1;
}); });
return { posts, tags, tagMap, privilegeLevel }; return { posts, tags, tagMap, isAdmin };
} }
); );
}; };
export const blogRouter = createTRPCRouter({ export const blogRouter = createTRPCRouter({
getRecentPosts: publicProcedure.query(async ({ ctx }) => { getRecentPosts: publicProcedure.query(async ({ ctx }) => {
const allPostsData = await getAllPostsData("public"); const allPostsData = await getAllPostsData(false);
return allPostsData.posts.slice(0, 3); return allPostsData.posts.slice(0, 3);
}), }),
getPosts: publicProcedure.query(async ({ ctx }) => { getPosts: publicProcedure.query(async ({ ctx }) => {
const privilegeLevel = ctx.privilegeLevel; const isAdmin = ctx.isAdmin;
return getAllPostsData(privilegeLevel); return getAllPostsData(isAdmin);
}), }),
incrementPostRead: publicProcedure incrementPostRead: publicProcedure

View File

@@ -144,7 +144,7 @@ export const databaseRouter = createTRPCRouter({
commentID: input.commentID, commentID: input.commentID,
deletionType: input.deletionType, deletionType: input.deletionType,
userId: ctx.userId, userId: ctx.userId,
privilegeLevel: ctx.privilegeLevel isAdmin: ctx.isAdmin
}); });
const commentQuery = await conn.execute({ const commentQuery = await conn.execute({
@@ -161,7 +161,7 @@ export const databaseRouter = createTRPCRouter({
} }
const isOwner = comment.commenter_id === ctx.userId; const isOwner = comment.commenter_id === ctx.userId;
const isAdmin = ctx.privilegeLevel === "admin"; const isAdmin = ctx.isAdmin;
console.log("[deleteComment] Authorization check:", { console.log("[deleteComment] Authorization check:", {
isOwner, isOwner,

View File

@@ -3,7 +3,7 @@ import { env } from "~/env/server";
export const infillRouter = createTRPCRouter({ export const infillRouter = createTRPCRouter({
getConfig: publicProcedure.query(({ ctx }) => { getConfig: publicProcedure.query(({ ctx }) => {
if (ctx.privilegeLevel !== "admin") { if (!ctx.isAdmin) {
return { endpoint: null, token: null }; return { endpoint: null, token: null };
} }

View File

@@ -8,7 +8,7 @@ import { getAuthSession } from "~/server/session-helpers";
export type Context = { export type Context = {
event: APIEvent; event: APIEvent;
userId: string | null; userId: string | null;
privilegeLevel: "anonymous" | "user" | "admin"; isAdmin: boolean;
}; };
async function createContextInner(event: APIEvent): Promise<Context> { async function createContextInner(event: APIEvent): Promise<Context> {
@@ -16,11 +16,11 @@ async function createContextInner(event: APIEvent): Promise<Context> {
const session = await getAuthSession(event.nativeEvent); const session = await getAuthSession(event.nativeEvent);
let userId: string | null = null; let userId: string | null = null;
let privilegeLevel: "anonymous" | "user" | "admin" = "anonymous"; let isAdmin = false;
if (session && session.userId) { if (session && session.userId) {
userId = session.userId; userId = session.userId;
privilegeLevel = session.isAdmin ? "admin" : "user"; isAdmin = session.isAdmin;
} }
const req = event.nativeEvent.node?.req || event.nativeEvent; const req = event.nativeEvent.node?.req || event.nativeEvent;
@@ -56,7 +56,7 @@ async function createContextInner(event: APIEvent): Promise<Context> {
return { return {
event, event,
userId, userId,
privilegeLevel isAdmin
}; };
} }
@@ -70,7 +70,7 @@ export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure; export const publicProcedure = t.procedure;
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.userId || ctx.privilegeLevel === "anonymous") { if (!ctx.userId) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" });
} }
return next({ return next({
@@ -82,7 +82,7 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
}); });
const enforceUserIsAdmin = t.middleware(({ ctx, next }) => { const enforceUserIsAdmin = t.middleware(({ ctx, next }) => {
if (ctx.privilegeLevel !== "admin") { if (!ctx.isAdmin) {
throw new TRPCError({ throw new TRPCError({
code: "FORBIDDEN", code: "FORBIDDEN",
message: "Admin access required" message: "Admin access required"

View File

@@ -6,7 +6,6 @@ import { getAuthSession } from "./session-helpers";
/** /**
* Check authentication status * Check authentication status
* Consolidates getUserID, getPrivilegeLevel, and checkAuthStatus into single function
* @param event - H3Event * @param event - H3Event
* @returns Object with isAuthenticated, userId, and isAdmin flags * @returns Object with isAuthenticated, userId, and isAdmin flags
*/ */

View File

@@ -7,7 +7,7 @@ import {
describe("parseConditionals", () => { describe("parseConditionals", () => {
const baseContext: ConditionalContext = { const baseContext: ConditionalContext = {
isAuthenticated: true, isAuthenticated: true,
privilegeLevel: "user", isAdmin: false,
userId: "test-user", userId: "test-user",
currentDate: new Date("2025-06-01"), currentDate: new Date("2025-06-01"),
featureFlags: { "beta-feature": true }, featureFlags: { "beta-feature": true },
@@ -34,7 +34,7 @@ describe("parseConditionals", () => {
const anonContext: ConditionalContext = { const anonContext: ConditionalContext = {
...baseContext, ...baseContext,
isAuthenticated: false, isAuthenticated: false,
privilegeLevel: "anonymous" isAdmin: false
}; };
const result = parseConditionals(html, anonContext); const result = parseConditionals(html, anonContext);
expect(result).not.toContain("Secret content"); expect(result).not.toContain("Secret content");
@@ -51,7 +51,7 @@ describe("parseConditionals", () => {
const adminContext: ConditionalContext = { const adminContext: ConditionalContext = {
...baseContext, ...baseContext,
privilegeLevel: "admin" isAdmin: true
}; };
const adminResult = parseConditionals(html, adminContext); const adminResult = parseConditionals(html, adminContext);
expect(adminResult).toContain("Admin panel"); expect(adminResult).toContain("Admin panel");
@@ -119,7 +119,7 @@ describe("parseConditionals", () => {
const anonContext: ConditionalContext = { const anonContext: ConditionalContext = {
...baseContext, ...baseContext,
isAuthenticated: false, isAuthenticated: false,
privilegeLevel: "anonymous" isAdmin: false
}; };
const anonResult = parseConditionals(html, anonContext); const anonResult = parseConditionals(html, anonContext);
expect(anonResult).toContain("Not authenticated content"); expect(anonResult).toContain("Not authenticated content");

View File

@@ -11,7 +11,7 @@ export function getSafeEnvVariables(): Record<string, string | undefined> {
export interface ConditionalContext { export interface ConditionalContext {
isAuthenticated: boolean; isAuthenticated: boolean;
privilegeLevel: "admin" | "user" | "anonymous"; isAdmin: boolean;
userId: string | null; userId: string | null;
currentDate: Date; currentDate: Date;
featureFlags: Record<string, boolean>; featureFlags: Record<string, boolean>;
@@ -194,7 +194,16 @@ function evaluatePrivilegeCondition(
value: string, value: string,
context: ConditionalContext context: ConditionalContext
): boolean { ): 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;
}
} }
/** /**

View File

@@ -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);
});
}

View File

@@ -90,7 +90,6 @@ export function hashRefreshToken(token: string): string {
* Create a new session in database and Vinxi session * Create a new session in database and Vinxi session
* @param event - H3Event * @param event - H3Event
* @param userId - User ID * @param userId - User ID
* @param isAdmin - Whether user is admin
* @param rememberMe - Whether to use extended session duration * @param rememberMe - Whether to use extended session duration
* @param ipAddress - Client IP address * @param ipAddress - Client IP address
* @param userAgent - Client user agent string * @param userAgent - Client user agent string
@@ -101,7 +100,6 @@ export function hashRefreshToken(token: string): string {
export async function createAuthSession( export async function createAuthSession(
event: H3Event, event: H3Event,
userId: string, userId: string,
isAdmin: boolean,
rememberMe: boolean, rememberMe: boolean,
ipAddress: string, ipAddress: string,
userAgent: string, userAgent: string,
@@ -109,6 +107,19 @@ export async function createAuthSession(
tokenFamily: string | null = null tokenFamily: string | null = null
): Promise<SessionData> { ): Promise<SessionData> {
const conn = ConnectionFactory(); 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 sessionId = uuidV4();
const family = tokenFamily || uuidV4(); const family = tokenFamily || uuidV4();
const refreshToken = generateRefreshToken(); const refreshToken = generateRefreshToken();
@@ -374,10 +385,10 @@ async function restoreSessionFromDB(
try { try {
const conn = ConnectionFactory(); 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({ const result = await conn.execute({
sql: `SELECT s.id, s.user_id, s.token_family, s.refresh_token_hash, 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 FROM Session s
JOIN User u ON s.user_id = u.id JOIN User u ON s.user_id = u.id
WHERE s.id = ?`, WHERE s.id = ?`,
@@ -412,7 +423,6 @@ async function restoreSessionFromDB(
const newSession = await createAuthSession( const newSession = await createAuthSession(
event, event,
dbSession.user_id as string, dbSession.user_id as string,
dbSession.isAdmin === 1,
true, // Assume rememberMe=true for restoration true, // Assume rememberMe=true for restoration
ipAddress, ipAddress,
userAgent, userAgent,
@@ -678,7 +688,6 @@ export async function rotateAuthSession(
const newSessionData = await createAuthSession( const newSessionData = await createAuthSession(
event, event,
oldSessionData.userId, oldSessionData.userId,
oldSessionData.isAdmin,
oldSessionData.rememberMe, oldSessionData.rememberMe,
ipAddress, ipAddress,
userAgent, userAgent,

View File

@@ -55,8 +55,6 @@ export interface BackupResponse {
commentParent?: number | null; commentParent?: number | null;
} }
export type PrivilegeLevel = "admin" | "user" | "anonymous";
export type SortingMode = "newest" | "oldest" | "highest_rated" | "hot"; export type SortingMode = "newest" | "oldest" | "highest_rated" | "hot";
export type DeletionType = "user" | "admin" | "database"; export type DeletionType = "user" | "admin" | "database";
@@ -64,7 +62,8 @@ export type DeletionType = "user" | "admin" | "database";
export type ModificationType = "delete" | "edit"; export type ModificationType = "delete" | "edit";
export interface CommentSectionWrapperProps { export interface CommentSectionWrapperProps {
privilegeLevel: PrivilegeLevel; isAuthenticated: boolean;
isAdmin: boolean;
allComments: Comment[]; allComments: Comment[];
topLevelComments: Comment[]; topLevelComments: Comment[];
id: number; id: number;
@@ -74,7 +73,8 @@ export interface CommentSectionWrapperProps {
} }
export interface CommentSectionProps { export interface CommentSectionProps {
privilegeLevel: PrivilegeLevel; isAuthenticated: boolean;
isAdmin: boolean;
postID: number; postID: number;
allComments: Comment[]; allComments: Comment[];
topLevelComments: Comment[]; topLevelComments: Comment[];
@@ -101,7 +101,8 @@ export interface CommentBlockProps {
recursionCount: number; recursionCount: number;
allComments: Comment[] | undefined; allComments: Comment[] | undefined;
child_comments: Comment[] | undefined; child_comments: Comment[] | undefined;
privilegeLevel: PrivilegeLevel; isAuthenticated: boolean;
isAdmin: boolean;
currentUserID: string; currentUserID: string;
reactionMap: Map<number, CommentReaction[]>; reactionMap: Map<number, CommentReaction[]>;
level: number; level: number;
@@ -124,7 +125,7 @@ export interface CommentBlockProps {
export interface CommentInputBlockProps { export interface CommentInputBlockProps {
isReply: boolean; isReply: boolean;
parent_id?: number; parent_id?: number;
privilegeLevel: PrivilegeLevel; isAuthenticated: boolean;
post_id: number; post_id: number;
socket: WebSocket | undefined; socket: WebSocket | undefined;
currentUserID: string; currentUserID: string;
@@ -134,7 +135,8 @@ export interface CommentInputBlockProps {
export interface CommentSortingProps { export interface CommentSortingProps {
topLevelComments: Comment[]; topLevelComments: Comment[];
privilegeLevel: PrivilegeLevel; isAuthenticated: boolean;
isAdmin: boolean;
postID: number; postID: number;
allComments: Comment[]; allComments: Comment[];
reactionMap: Map<number, CommentReaction[]>; reactionMap: Map<number, CommentReaction[]>;
@@ -170,14 +172,14 @@ export interface ReactionBarProps {
currentUserID: string; currentUserID: string;
commentID: number; commentID: number;
reactions: CommentReaction[]; reactions: CommentReaction[];
privilegeLevel: PrivilegeLevel; isAuthenticated: boolean;
showingReactionOptions: boolean; showingReactionOptions: boolean;
commentReaction: (reactionType: ReactionType, commentID: number) => void; commentReaction: (reactionType: ReactionType, commentID: number) => void;
} }
export interface CommentDeletionPromptProps { export interface CommentDeletionPromptProps {
isOpen: boolean; isOpen: boolean;
privilegeLevel: PrivilegeLevel; isAdmin: boolean;
commentID: number; commentID: number;
commenterID: string; commenterID: string;
deleteComment: ( deleteComment: (