fix(security): authorize site uploads and harden tokens, media and request identity
This commit is contained in:
1 parent
52f6d1491f
commit
8abfe352ef
70 files changed
+1609
-204
No files matched your search
+25
-5
@@ -1,6 +1,13 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { and, eq, gt, isNull, or } from "drizzle-orm";
|
||||
import { personalTokenScope } from "@/lib/auth/personal-token-scope";
|
||||
import {
|
||||
type PersonalTokenAbility,
|
||||
tokenAllowsAbilities,
|
||||
} from "@/lib/auth/personal-token-abilities";
|
||||
import {
|
||||
personalTokenScope,
|
||||
USER_TOKENABLE_TYPE,
|
||||
} from "@/lib/auth/personal-token-scope";
|
||||
import { databaseUserId } from "@/lib/auth/session-user";
|
||||
import { db, PersonalAccessTokens } from "@/lib/db";
|
||||
|
||||
@@ -14,8 +21,11 @@ function hashToken(raw: string): string {
|
||||
return createHash("sha256").update(raw).digest("hex");
|
||||
}
|
||||
|
||||
/** Resolve the user id behind a Bearer token, or null. */
|
||||
export async function bearerUserId(req: Request): Promise<number | null> {
|
||||
/** Resolve an authorized user token; callers without a declared scope require full access. */
|
||||
export async function bearerUserId(
|
||||
req: Request,
|
||||
requiredAbilities: readonly PersonalTokenAbility[] = [],
|
||||
): Promise<number | null> {
|
||||
const header = req.headers.get("authorization") ?? "";
|
||||
const m = header.match(/^Bearer\s+(.+)$/i);
|
||||
if (!m) return null;
|
||||
@@ -29,11 +39,14 @@ export async function bearerUserId(req: Request): Promise<number | null> {
|
||||
.select({
|
||||
id: PersonalAccessTokens.id,
|
||||
tokenableId: PersonalAccessTokens.tokenableId,
|
||||
tokenableType: PersonalAccessTokens.tokenableType,
|
||||
abilities: PersonalAccessTokens.abilities,
|
||||
})
|
||||
.from(PersonalAccessTokens)
|
||||
.where(
|
||||
and(
|
||||
eq(PersonalAccessTokens.token, hashToken(raw)),
|
||||
eq(PersonalAccessTokens.tokenableType, USER_TOKENABLE_TYPE),
|
||||
or(
|
||||
isNull(PersonalAccessTokens.expiresAt),
|
||||
gt(PersonalAccessTokens.expiresAt, new Date()),
|
||||
@@ -41,14 +54,21 @@ export async function bearerUserId(req: Request): Promise<number | null> {
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (!row) return null;
|
||||
if (
|
||||
!row ||
|
||||
row.tokenableType !== USER_TOKENABLE_TYPE ||
|
||||
!tokenAllowsAbilities(row.abilities, requiredAbilities)
|
||||
)
|
||||
return null;
|
||||
const userId = databaseUserId(row.tokenableId);
|
||||
if (userId === null) return null;
|
||||
// Best-effort last-used stamp (don't fail the request if it errors).
|
||||
void db
|
||||
.update(PersonalAccessTokens)
|
||||
.set({ lastUsedAt: new Date() })
|
||||
.where(eq(PersonalAccessTokens.id, row.id))
|
||||
.catch(() => {});
|
||||
return databaseUserId(row.tokenableId);
|
||||
return userId;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user