107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import type { IdentitySession, IdentityUser } from "./contracts.ts";
|
|
|
|
interface RawIdentitySession {
|
|
session: { id: string; token: string; expiresAt: Date | string };
|
|
user: {
|
|
id: string;
|
|
email: string;
|
|
emailVerified: boolean;
|
|
twoFactorEnabled?: boolean;
|
|
name: string;
|
|
image?: string | null;
|
|
role?: string | null;
|
|
};
|
|
}
|
|
|
|
export interface IdentityServerSession extends IdentitySession {
|
|
sessionId: string;
|
|
sessionToken: string;
|
|
}
|
|
|
|
export interface IdentitySessionReader {
|
|
getSession(input: { headers: Headers }): Promise<RawIdentitySession | null>;
|
|
}
|
|
|
|
export class IdentityAuthorizationError extends Error {
|
|
constructor(
|
|
message: string,
|
|
readonly status: 401 | 403,
|
|
) {
|
|
super(message);
|
|
this.name = "IdentityAuthorizationError";
|
|
}
|
|
}
|
|
|
|
function parseRoles(role: string | null | undefined): string[] {
|
|
const roles = (role ?? "user")
|
|
.split(",")
|
|
.map((value) => value.trim())
|
|
.filter(Boolean);
|
|
return [...new Set(roles.length ? roles : ["user"])];
|
|
}
|
|
|
|
export async function readIdentityServerSession(
|
|
reader: IdentitySessionReader,
|
|
requestHeaders: Headers,
|
|
): Promise<IdentityServerSession | null> {
|
|
const value = await reader.getSession({ headers: requestHeaders });
|
|
if (!value) return null;
|
|
|
|
const expiresAt = new Date(value.session.expiresAt);
|
|
if (!Number.isFinite(expiresAt.getTime())) {
|
|
throw new Error("identity session has an invalid expiry");
|
|
}
|
|
|
|
return {
|
|
sessionId: value.session.id,
|
|
sessionToken: value.session.token,
|
|
expiresAt,
|
|
user: {
|
|
id: value.user.id,
|
|
email: value.user.email.trim().toLowerCase(),
|
|
emailVerified: value.user.emailVerified,
|
|
twoFactorEnabled: value.user.twoFactorEnabled === true,
|
|
name: value.user.name,
|
|
image: value.user.image ?? null,
|
|
role: parseRoles(value.user.role),
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function readIdentitySession(
|
|
reader: IdentitySessionReader,
|
|
requestHeaders: Headers,
|
|
): Promise<IdentitySession | null> {
|
|
const session = await readIdentityServerSession(reader, requestHeaders);
|
|
return session ? { user: session.user, expiresAt: session.expiresAt } : null;
|
|
}
|
|
|
|
export async function requireIdentityServerSession(
|
|
reader: IdentitySessionReader,
|
|
requestHeaders: Headers,
|
|
): Promise<IdentityServerSession> {
|
|
const session = await readIdentityServerSession(reader, requestHeaders);
|
|
if (!session) throw new IdentityAuthorizationError("Authentication required", 401);
|
|
return session;
|
|
}
|
|
|
|
export async function requireIdentityUser(
|
|
reader: IdentitySessionReader,
|
|
requestHeaders: Headers,
|
|
): Promise<IdentityUser> {
|
|
const session = await readIdentitySession(reader, requestHeaders);
|
|
if (!session) throw new IdentityAuthorizationError("Authentication required", 401);
|
|
return session.user;
|
|
}
|
|
|
|
export async function requireIdentityAdmin(
|
|
reader: IdentitySessionReader,
|
|
requestHeaders: Headers,
|
|
): Promise<IdentityUser> {
|
|
const user = await requireIdentityUser(reader, requestHeaders);
|
|
if (!user.role.includes("admin")) {
|
|
throw new IdentityAuthorizationError("Administrator access required", 403);
|
|
}
|
|
return user;
|
|
}
|