feat: add admin accounts and image templates
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { SESSION_COOKIE_NAME, getAuthRuntimeConfig } from "@/lib/auth/config";
|
||||
import { parseSessionCookieValue, type AuthSession, type AuthUser } from "@/lib/auth/session";
|
||||
import { hasAdminAccess } from "@/lib/auth/permissions";
|
||||
import { parseSessionCookieValue, readChunkedCookieValue, type AuthSession, type AuthUser } from "@/lib/auth/session";
|
||||
import { DEFAULT_OWNER_ID } from "@/lib/server/runtime";
|
||||
|
||||
export class AuthRequiredError extends Error {
|
||||
@@ -27,38 +28,77 @@ const localUser: AuthUser = {
|
||||
username: "demo",
|
||||
displayName: "智念演示用户",
|
||||
clientId: "local-dev",
|
||||
authorities: [],
|
||||
authorities: ["zhinian_admin"],
|
||||
scope: []
|
||||
};
|
||||
|
||||
function localSession(): AuthSession {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return {
|
||||
version: 1,
|
||||
user: localUser,
|
||||
issuedAt: now,
|
||||
expiresAt: now + 24 * 60 * 60
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOptionalAuthSession(): Promise<AuthSession | null> {
|
||||
const config = getAuthRuntimeConfig();
|
||||
if (!config.sessionSecret) return null;
|
||||
const cookieStore = await cookies();
|
||||
return parseSessionCookieValue(cookieStore.get(SESSION_COOKIE_NAME)?.value, config.sessionSecret);
|
||||
return parseSessionCookieValue(
|
||||
readChunkedCookieValue(SESSION_COOKIE_NAME, (name) => cookieStore.get(name)?.value),
|
||||
config.sessionSecret
|
||||
);
|
||||
}
|
||||
|
||||
export async function requireAppUser(): Promise<AuthUser> {
|
||||
export async function requireAppSession(): Promise<AuthSession> {
|
||||
const session = await getOptionalAuthSession();
|
||||
if (session) return session.user;
|
||||
if (session) return session;
|
||||
const config = getAuthRuntimeConfig();
|
||||
if (!config.required) return localUser;
|
||||
if (!config.required) return localSession();
|
||||
if (!config.configured) {
|
||||
throw new AuthConfigurationError(`认证配置不完整:${config.missing.join(", ") || "未知配置"}`);
|
||||
}
|
||||
throw new AuthRequiredError();
|
||||
}
|
||||
|
||||
export async function requireAppUser(): Promise<AuthUser> {
|
||||
return (await requireAppSession()).user;
|
||||
}
|
||||
|
||||
export class AdminRequiredError extends Error {
|
||||
status = 403;
|
||||
|
||||
constructor(message = "需要管理员权限。") {
|
||||
super(message);
|
||||
this.name = "AdminRequiredError";
|
||||
}
|
||||
}
|
||||
|
||||
export async function requireAdminUser(): Promise<AuthUser> {
|
||||
return (await requireAdminSession()).user;
|
||||
}
|
||||
|
||||
export async function requireAdminSession(): Promise<AuthSession> {
|
||||
const session = await requireAppSession();
|
||||
if (!hasAdminAccess(session.user)) throw new AdminRequiredError();
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function getShellAuthState(): Promise<{
|
||||
user: AuthUser | null;
|
||||
authRequired: boolean;
|
||||
authConfigured: boolean;
|
||||
isAdmin: boolean;
|
||||
}> {
|
||||
const config = getAuthRuntimeConfig();
|
||||
const session = await getOptionalAuthSession();
|
||||
const user = session?.user || (!config.required ? localUser : null);
|
||||
return {
|
||||
user: session?.user || null,
|
||||
user,
|
||||
authRequired: config.required,
|
||||
authConfigured: config.configured
|
||||
authConfigured: config.configured,
|
||||
isAdmin: hasAdminAccess(user)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@ export async function verifyAuthJwt(token: string, config = getAuthRuntimeConfig
|
||||
export function createSessionFromClaims(
|
||||
claims: AuthTokenClaims,
|
||||
config: AuthRuntimeConfig,
|
||||
tokenResponseExpiresIn?: number
|
||||
tokenResponseExpiresIn?: number,
|
||||
token?: { accessToken?: string; tokenType?: string }
|
||||
): AuthSession {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const jwtExpiry = numberClaim(claims.exp);
|
||||
@@ -85,7 +86,9 @@ export function createSessionFromClaims(
|
||||
version: 1,
|
||||
user: userFromClaims(claims, config),
|
||||
issuedAt: now,
|
||||
expiresAt
|
||||
expiresAt,
|
||||
accessToken: token?.accessToken,
|
||||
tokenType: token?.tokenType
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
AUTH_STATE_COOKIE_NAME,
|
||||
SESSION_COOKIE_NAME,
|
||||
getAuthRuntimeConfig,
|
||||
safeNextPath,
|
||||
shouldUseSecureAuthCookie,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
parseSignedJsonValue
|
||||
} from "@/lib/auth/session";
|
||||
import { createSessionFromClaims, verifyAuthJwt } from "@/lib/server/auth/jwt";
|
||||
import { clearSessionCookieValues, setSessionCookieValue } from "@/lib/server/auth/session-cookie";
|
||||
import { requestOrigin } from "@/lib/server/runtime";
|
||||
|
||||
export type AuthStateCookie = {
|
||||
@@ -90,18 +90,16 @@ export async function completeAuthorizationCallback(request: Request): Promise<N
|
||||
const token = await exchangeAuthorizationCode(code, authRedirectUri(request), config);
|
||||
if (!token.access_token) throw new OAuthLoginError("认证中心没有返回 access_token。");
|
||||
const claims = await verifyAuthJwt(token.access_token, config);
|
||||
const session = createSessionFromClaims(claims, config, token.expires_in);
|
||||
const session = createSessionFromClaims(claims, config, token.expires_in, {
|
||||
accessToken: token.access_token,
|
||||
tokenType: token.token_type
|
||||
});
|
||||
const response = NextResponse.redirect(new URL(stateCookie.next, request.url));
|
||||
response.cookies.set(
|
||||
SESSION_COOKIE_NAME,
|
||||
setSessionCookieValue(
|
||||
response,
|
||||
request.url,
|
||||
await createSessionCookieValue(session, config.sessionSecret || ""),
|
||||
{
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: shouldUseSecureAuthCookie(request.url),
|
||||
path: "/",
|
||||
expires: new Date(session.expiresAt * 1000)
|
||||
}
|
||||
new Date(session.expiresAt * 1000)
|
||||
);
|
||||
response.cookies.set(AUTH_STATE_COOKIE_NAME, "", clearCookieOptions(request.url));
|
||||
return response;
|
||||
@@ -109,7 +107,7 @@ export async function completeAuthorizationCallback(request: Request): Promise<N
|
||||
|
||||
export function clearAuthCookies(request: Request, redirectTo = "/auth/login?loggedOut=1"): NextResponse {
|
||||
const response = NextResponse.redirect(new URL(redirectTo, request.url));
|
||||
response.cookies.set(SESSION_COOKIE_NAME, "", clearCookieOptions(request.url));
|
||||
clearSessionCookieValues(response, request.url);
|
||||
response.cookies.set(AUTH_STATE_COOKIE_NAME, "", clearCookieOptions(request.url));
|
||||
return response;
|
||||
}
|
||||
|
||||
45
lib/server/auth/session-cookie.ts
Normal file
45
lib/server/auth/session-cookie.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { NextResponse } from "next/server";
|
||||
import { SESSION_COOKIE_NAME, shouldUseSecureAuthCookie } from "@/lib/auth/config";
|
||||
import { chunkCookieValue, chunkedCookieName, chunkedCookieNames } from "@/lib/auth/session";
|
||||
|
||||
const MAX_SESSION_COOKIE_CHUNKS = 20;
|
||||
|
||||
type CookieResponse = Pick<NextResponse, "cookies">;
|
||||
|
||||
export function setSessionCookieValue(
|
||||
response: CookieResponse,
|
||||
requestUrl: string,
|
||||
value: string,
|
||||
expires: Date
|
||||
) {
|
||||
const chunks = chunkCookieValue(value);
|
||||
const options = {
|
||||
httpOnly: true,
|
||||
sameSite: "lax" as const,
|
||||
secure: shouldUseSecureAuthCookie(requestUrl),
|
||||
path: "/",
|
||||
expires
|
||||
};
|
||||
chunks.forEach((chunk, index) => {
|
||||
response.cookies.set(chunkedCookieName(SESSION_COOKIE_NAME, index), chunk, options);
|
||||
});
|
||||
for (let index = chunks.length; index < MAX_SESSION_COOKIE_CHUNKS; index += 1) {
|
||||
response.cookies.set(chunkedCookieName(SESSION_COOKIE_NAME, index), "", clearSessionCookieOptions(requestUrl));
|
||||
}
|
||||
}
|
||||
|
||||
export function clearSessionCookieValues(response: CookieResponse, requestUrl: string) {
|
||||
for (const name of chunkedCookieNames(SESSION_COOKIE_NAME, MAX_SESSION_COOKIE_CHUNKS)) {
|
||||
response.cookies.set(name, "", clearSessionCookieOptions(requestUrl));
|
||||
}
|
||||
}
|
||||
|
||||
function clearSessionCookieOptions(requestUrl: string) {
|
||||
return {
|
||||
httpOnly: true,
|
||||
sameSite: "lax" as const,
|
||||
secure: shouldUseSecureAuthCookie(requestUrl),
|
||||
path: "/",
|
||||
maxAge: 0
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user