Template
1
0

refactor: identity -> iam

This commit is contained in:
2025-10-03 16:07:10 +02:00
parent fe50394ec0
commit 7504361d88
46 changed files with 16 additions and 10 deletions

View File

@@ -0,0 +1,17 @@
import { HTTP } from "@cerbos/http";
import { getEnvironmentVariable } from "@platform/config/environment.ts";
import z from "zod";
export const cerbos = new HTTP(
getEnvironmentVariable({
key: "CERBOS_URL",
type: z.string(),
fallback: "http://localhost:3592",
}),
{
adminCredentials: {
username: "cerbos",
password: "cerbosAdmin",
},
},
);

View File

@@ -0,0 +1,23 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: identity
version: default
rules:
# Admins can read any identity with limited fields
- actions: ["read", "update"]
effect: EFFECT_ALLOW
roles: ["admin"]
# Users can fully read, update, or delete their own identity
- actions: ["read", "update", "delete"]
effect: EFFECT_ALLOW
roles: ["user"]
condition:
match:
expr: request.resource.id == request.principal.id

View File

@@ -0,0 +1,14 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: role
version: default
rules:
# Admin can manage roles
- actions: ["manage"]
effect: EFFECT_ALLOW
roles: ["super"]

View File

@@ -0,0 +1,11 @@
/*
export const resources = new ResourceRegistry([
{
kind: "identity",
actions: ["read", "update", "delete"],
attr: {},
},
] as const);
export type Resource = typeof resources.$resource;
*/

163
modules/iam/client.ts Normal file
View File

@@ -0,0 +1,163 @@
import { CheckResourcesResponse } from "@cerbos/core";
import { HttpAdapter, makeClient } from "@platform/relay";
import { config } from "./config.ts";
import checkResource from "./routes/access/check-resource/spec.ts";
import checkResources from "./routes/access/check-resources/spec.ts";
import isAllowed from "./routes/access/is-allowed/spec.ts";
import getById from "./routes/identities/get/spec.ts";
import loginByPassword from "./routes/login/code/spec.ts";
import loginByEmail from "./routes/login/email/spec.ts";
import loginByCode from "./routes/login/password/spec.ts";
import me from "./routes/me/spec.ts";
const adapter = new HttpAdapter({
url: config.url,
});
const access = makeClient(
{
adapter,
},
{
isAllowed,
checkResource,
checkResources,
},
);
export const identity = makeClient(
{
adapter,
},
{
/**
* TODO ...
*/
getById,
/**
* TODO ...
*/
me,
/**
* TODO ...
*/
login: {
/**
* TODO ...
*/
email: loginByEmail,
/**
* TODO ...
*/
password: loginByPassword,
/**
* TODO ...
*/
code: loginByCode,
},
access: {
/**
* Check if a principal is allowed to perform an action on a resource.
*
* @param resource - Resource which we are validating.
* @param action - Action which we are validating.
*
* @example
*
* await access.isAllowed(
* {
* kind: "document",
* id: "1",
* attr: { owner: "user@example.com" },
* },
* "view"
* ); // => true
*/
isAllowed: async (resource: Resource, action: string) => {
const response = await access.isAllowed({ body: { resource, action } });
if ("error" in response) {
throw response.error;
}
return response.data;
},
/**
* Check a principal's permissions on a resource.
*
* @param resource - Resource which we are validating.
* @param actions - Actions which we are validating.
*
* @example
*
* const decision = await access.checkResource(
* {
* kind: "document",
* id: "1",
* attr: { owner: "user@example.com" },
* },
* ["view", "edit"],
* );
*
* decision.isAllowed("view"); // => true
*/
checkResource: async (resource: Resource, actions: string[]) => {
const response = await access.checkResource({ body: { resource, actions } });
if ("error" in response) {
throw response.error;
}
return new CheckResourcesResponse(response.data);
},
/**
* Check a principal's permissions on a set of resources.
*
* @param resources - Resources which we are validating.
*
* @example
*
* const decision = await access.checkResources([
* {
* resource: {
* kind: "document",
* id: "1",
* attr: { owner: "user@example.com" },
* },
* actions: ["view", "edit"],
* },
* {
* resource: {
* kind: "image",
* id: "1",
* attr: { owner: "user@example.com" },
* },
* actions: ["delete"],
* },
* ]);
*
* decision.isAllowed({
* resource: { kind: "document", id: "1" },
* action: "view",
* }); // => true
*/
checkResources: async (resources: { resource: Resource; actions: string[] }[]) => {
const response = await access.checkResources({ body: resources });
if ("error" in response) {
throw response.error;
}
return new CheckResourcesResponse(response.data);
},
},
},
);
type Resource = {
kind: string;
id: string;
attr: Record<string, any>;
};

44
modules/iam/config.ts Normal file
View File

@@ -0,0 +1,44 @@
import { getEnvironmentVariable } from "@platform/config/environment.ts";
import type { SerializeOptions } from "cookie";
import z from "zod";
export const config = {
url: getEnvironmentVariable({
key: "IDENTITY_SERVICE_URL",
type: z.url(),
fallback: "http://localhost:8370",
}),
internal: {
privateKey: getEnvironmentVariable({
key: "IDENTITY_PRIVATE_KEY",
type: z.string(),
fallback:
"-----BEGIN PRIVATE KEY-----\n" +
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2WYKMJZUWff5XOWC\n" +
"XGuU+wmsRzhQGEIzfUoL6rrGoaehRANCAATCpiGiFQxTA76EIVG0cBbj+AFt6BuJ\n" +
"t4q+zoInPUzkChCdwI+XfAYokrZwBjcyRGluC02HaN3cptrmjYSGSMSx\n" +
"-----END PRIVATE KEY-----",
}),
publicKey: getEnvironmentVariable({
key: "IDENTITY_PUBLIC_KEY",
type: z.string(),
fallback:
"-----BEGIN PUBLIC KEY-----\n" +
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwqYhohUMUwO+hCFRtHAW4/gBbegb\n" +
"ibeKvs6CJz1M5AoQncCPl3wGKJK2cAY3MkRpbgtNh2jd3Kba5o2EhkjEsQ==\n" +
"-----END PUBLIC KEY-----",
}),
},
cookie: (maxAge: number) =>
({
httpOnly: true,
secure: getEnvironmentVariable({
key: "AUTH_COOKIE_SECURE",
type: z.coerce.boolean(),
fallback: "false",
}), // Set to true for HTTPS in production
maxAge,
path: "/",
sameSite: "strict",
}) satisfies SerializeOptions,
};

View File

@@ -0,0 +1,46 @@
import { makeDocumentParser } from "@platform/database/utilities.ts";
import z from "zod";
export enum PrincipalTypeId {
User = 1,
Group = 2,
Other = 99,
}
export const PRINCIPAL_TYPE_NAMES = {
[PrincipalTypeId.User]: "User",
[PrincipalTypeId.Group]: "Group",
[PrincipalTypeId.Other]: "Other",
};
/*
|--------------------------------------------------------------------------------
| Schema
|--------------------------------------------------------------------------------
*/
export const PrincipalSchema = z.object({
id: z.string(),
type: z.strictObject({
id: z.enum(PrincipalTypeId),
name: z.string(),
}),
roles: z.array(z.string()),
attr: z.record(z.string(), z.any()),
});
/*
|--------------------------------------------------------------------------------
| Parsers
|--------------------------------------------------------------------------------
*/
export const parsePrincipal = makeDocumentParser(PrincipalSchema);
/*
|--------------------------------------------------------------------------------
| Types
|--------------------------------------------------------------------------------
*/
export type Principal = z.infer<typeof PrincipalSchema>;

View File

@@ -0,0 +1,26 @@
import z from "zod";
/*
|--------------------------------------------------------------------------------
| Schema
|--------------------------------------------------------------------------------
*/
export const SessionSchema = z.object({
id: z.string(),
userId: z.string(),
token: z.string(),
ipAddress: z.string().nullable().optional(),
userAgent: z.string().nullable().optional(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
expiresAt: z.coerce.date(),
});
/*
|--------------------------------------------------------------------------------
| Types
|--------------------------------------------------------------------------------
*/
export type Session = z.infer<typeof SessionSchema>;

23
modules/iam/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "@modules/identity",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
"./client.ts": "./client.ts",
"./server.ts": "./server.ts",
"./types.ts": "./types.ts"
},
"dependencies": {
"@cerbos/core": "0.24.1",
"@cerbos/http": "0.23.1",
"@platform/config": "workspace:*",
"@platform/logger": "workspace:*",
"@platform/relay": "workspace:*",
"@platform/storage": "workspace:*",
"@platform/vault": "workspace:*",
"better-auth": "1.3.16",
"cookie": "1.0.2",
"zod": "4.1.11"
}
}

View File

@@ -0,0 +1,6 @@
import { cerbos } from "../../../cerbos/client.ts";
import route from "./spec.ts";
export default route.access("session").handle(async ({ body: { resource, actions } }, { principal }) => {
return cerbos.checkResource({ principal, resource, actions });
});

View File

@@ -0,0 +1,16 @@
import { route } from "@platform/relay";
import z from "zod";
export default route
.post("/api/v1/identity/access/check-resource")
.body(
z.strictObject({
resource: z.strictObject({
kind: z.string(),
id: z.string(),
attr: z.record(z.string(), z.any()),
}),
actions: z.array(z.string()),
}),
)
.response(z.any());

View File

@@ -0,0 +1,6 @@
import { cerbos } from "../../../cerbos/client.ts";
import route from "./spec.ts";
export default route.access("session").handle(async ({ body: resources }, { principal }) => {
return cerbos.checkResources({ principal, resources });
});

View File

@@ -0,0 +1,18 @@
import { route } from "@platform/relay";
import z from "zod";
export default route
.post("/api/v1/identity/access/check-resources")
.body(
z.array(
z.strictObject({
resource: z.strictObject({
kind: z.string(),
id: z.string(),
attr: z.record(z.string(), z.any()),
}),
actions: z.array(z.string()),
}),
),
)
.response(z.any());

View File

@@ -0,0 +1,6 @@
import { cerbos } from "../../../cerbos/client.ts";
import route from "./spec.ts";
export default route.access("session").handle(async ({ body: { resource, action } }, { principal }) => {
return cerbos.isAllowed({ principal, resource, action });
});

View File

@@ -0,0 +1,16 @@
import { route } from "@platform/relay";
import z from "zod";
export default route
.post("/api/v1/identity/access/is-allowed")
.body(
z.strictObject({
resource: z.strictObject({
kind: z.string(),
id: z.string(),
attr: z.record(z.string(), z.any()),
}),
action: z.string(),
}),
)
.response(z.boolean());

View File

@@ -0,0 +1,16 @@
import { ForbiddenError, NotFoundError } from "@platform/relay";
import { getPrincipalById } from "../../../services/database.ts";
import route from "./spec.ts";
export default route.access("session").handle(async ({ params: { id } }, { access }) => {
const principal = await getPrincipalById(id);
if (principal === undefined) {
return new NotFoundError("Identity does not exist, or has been removed.");
}
const decision = await access.isAllowed({ kind: "identity", id, attr: {} }, "read");
if (decision === false) {
return new ForbiddenError("You do not have permission to view this identity.");
}
return principal;
});

View File

@@ -0,0 +1,10 @@
import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay";
import z from "zod";
export default route
.get("/api/v1/identity/:id")
.params({
id: z.string(),
})
.errors([UnauthorizedError, ForbiddenError, NotFoundError])
.response(z.any());

View File

@@ -0,0 +1,43 @@
import { ForbiddenError, NotFoundError } from "@platform/relay";
import { getPrincipalById, setPrincipalAttributesById } from "../../../services/database.ts";
import route from "./spec.ts";
export default route.access("session").handle(async ({ params: { id }, body: ops }, { access }) => {
const principal = await getPrincipalById(id);
if (principal === undefined) {
return new NotFoundError();
}
const decision = await access.isAllowed({ kind: "identity", id: principal.id, attr: principal.attr }, "update");
if (decision === false) {
return new ForbiddenError("You do not have permission to update this identity.");
}
const attr = principal.attr;
for (const op of ops) {
switch (op.type) {
case "add": {
attr[op.key] = op.value;
break;
}
case "push": {
if (attr[op.key] === undefined) {
attr[op.key] = op.values;
} else {
attr[op.key] = [...attr[op.key], ...op.values];
}
break;
}
case "pop": {
if (Array.isArray(attr[op.key])) {
attr[op.key] = attr[op.key].filter((value: any) => op.values.includes(value) === false);
}
break;
}
case "remove": {
delete attr[op.key];
break;
}
}
}
await setPrincipalAttributesById(id, attr);
});

View File

@@ -0,0 +1,29 @@
import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay";
import z from "zod";
export default route
.put("/api/v1/identity/:id")
.params({
id: z.string(),
})
.body(
z.array(
z.union([
z.strictObject({
type: z.union([z.literal("add")]),
key: z.string(),
value: z.any(),
}),
z.strictObject({
type: z.union([z.literal("push"), z.literal("pop")]),
key: z.string(),
values: z.array(z.any()),
}),
z.strictObject({
type: z.union([z.literal("remove")]),
key: z.string(),
}),
]),
),
)
.errors([UnauthorizedError, ForbiddenError, NotFoundError]);

View File

@@ -0,0 +1,16 @@
import { NotFoundError } from "@platform/relay";
import { auth } from "../../../services/auth.ts";
import { logger } from "../../../services/logger.ts";
import route from "./spec.ts";
export default route.access("public").handle(async ({ body: { email, otp } }) => {
const response = await auth.api.signInEmailOTP({ body: { email, otp }, asResponse: true, returnHeaders: true });
if (response.status !== 200) {
logger.error("OTP Signin Failed", await response.json());
return new NotFoundError();
}
return new Response(null, {
headers: response.headers,
});
});

View File

@@ -0,0 +1,14 @@
import { route } from "@platform/relay";
import z from "zod";
export default route
.post("/api/v1/identity/login/code")
.body(
z.strictObject({
email: z.string(),
otp: z.string(),
}),
)
.query({
next: z.string().optional(),
});

View File

@@ -0,0 +1,14 @@
import { auth } from "../../../services/auth.ts";
import { logger } from "../../../services/logger.ts";
import route from "./spec.ts";
export default route.access("public").handle(async ({ body: { email } }) => {
const response = await auth.api.sendVerificationOTP({ body: { email, type: "sign-in" } });
if (response.success === false) {
logger.info({
type: "auth:passwordless",
message: "OTP Email verification failed.",
received: email,
});
}
});

View File

@@ -0,0 +1,8 @@
import { route } from "@platform/relay";
import z from "zod";
export default route.post("/api/v1/identity/login/email").body(
z.object({
email: z.email(),
}),
);

View File

@@ -0,0 +1,36 @@
import { logger } from "@platform/logger";
import { BadRequestError } from "@platform/relay";
import cookie from "cookie";
import { auth } from "../../../auth.ts";
import { config } from "../../../config.ts";
import { password } from "../../../crypto/password.ts";
import { getPasswordStrategyByAlias } from "../../../database.ts";
import route from "./spec.ts";
export default route.access("public").handle(async ({ body: { alias, password: userPassword } }) => {
const strategy = await getPasswordStrategyByAlias(alias);
if (strategy === undefined) {
return logger.info({
type: "auth:password",
message: "Failed to get account with 'password' strategy.",
alias,
});
}
const isValidPassword = await password.verify(userPassword, strategy.password);
if (isValidPassword === false) {
return new BadRequestError("Invalid email/password provided.");
}
return new Response(null, {
status: 204,
headers: {
"set-cookie": cookie.serialize(
"token",
await auth.generate({ id: strategy.accountId }, "1 week"),
config.cookie(1000 * 60 * 60 * 24 * 7),
),
},
});
});

View File

@@ -0,0 +1,9 @@
import { route } from "@platform/relay";
import z from "zod";
export default route.post("/api/v1/identities/login/password").body(
z.object({
alias: z.string(),
password: z.string(),
}),
);

View File

@@ -0,0 +1,39 @@
import route from "./spec.ts";
export default route.access("public").handle(async () => {
// const code = await Passwordless.createCode({ tenantId: "public", email });
// if (code.status !== "OK") {
// return logger.info({
// type: "auth:passwordless",
// message: "Create code failed.",
// received: email,
// });
// }
// logger.info({
// type: "auth:passwordless",
// data: {
// deviceId: code.deviceId,
// preAuthSessionId: code.preAuthSessionId,
// userInputCode: code.userInputCode,
// },
// });
// const response = await Passwordless.consumeCode({
// tenantId: "public",
// preAuthSessionId: code.preAuthSessionId,
// deviceId: code.deviceId,
// userInputCode: code.userInputCode,
// });
// if (response.status !== "OK") {
// return new NotFoundError();
// }
// logger.info({
// type: "code:claimed",
// session: true,
// message: "Identity resolved",
// user: response.user.toJson(),
// });
// return new Response(null, {
// status: 200,
// headers: await getSessionHeaders("public", response.recipeUserId),
// });
});

View File

@@ -0,0 +1,8 @@
import { route } from "@platform/relay";
import z from "zod";
export default route.post("/api/v1/identities/login/sudo").body(
z.object({
email: z.email(),
}),
);

View File

@@ -0,0 +1,5 @@
import route from "./spec.ts";
export default route.access("session").handle(async ({ principal }) => {
return principal;
});

View File

@@ -0,0 +1,4 @@
import { NotFoundError, route, UnauthorizedError } from "@platform/relay";
import z from "zod";
export default route.get("/api/v1/identity/me").errors([UnauthorizedError, NotFoundError]).response(z.any());

View File

@@ -0,0 +1,33 @@
import { ForbiddenError, NotFoundError } from "@platform/relay";
import { getPrincipalById, setPrincipalRolesById } from "../../services/database.ts";
import route from "./spec.ts";
export default route.access("session").handle(async ({ params: { id }, body: ops }, { access }) => {
const principal = await getPrincipalById(id);
if (principal === undefined) {
return new NotFoundError();
}
const decision = await access.isAllowed({ kind: "role", id: principal.id, attr: principal.attr }, "manage");
if (decision === false) {
return new ForbiddenError("You do not have permission to modify roles for this identity.");
}
const roles: Set<string> = new Set(principal.roles);
for (const op of ops) {
switch (op.type) {
case "add": {
for (const role of op.roles) {
roles.add(role);
}
break;
}
case "remove": {
for (const role of op.roles) {
roles.delete(role);
}
break;
}
}
}
await setPrincipalRolesById(id, Array.from(roles));
});

View File

@@ -0,0 +1,19 @@
import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay";
import z from "zod";
export default route
.put("/api/v1/identity/:id/roles")
.params({
id: z.string(),
})
.body(
z.array(
z.union([
z.strictObject({
type: z.union([z.literal("add"), z.literal("remove")]),
roles: z.array(z.any()),
}),
]),
),
)
.errors([UnauthorizedError, ForbiddenError, NotFoundError]);

View File

@@ -0,0 +1,17 @@
import { NotFoundError } from "@platform/relay";
import { config } from "../../../config.ts";
import { getPrincipalByUserId } from "../../../services/database.ts";
import { getSessionByRequestHeader } from "../../../services/session.ts";
import route from "./spec.ts";
export default route.access(["internal:public", config.internal.privateKey]).handle(async ({ request }) => {
const session = await getSessionByRequestHeader(request.headers);
if (session === undefined) {
return new NotFoundError();
}
return {
session,
principal: await getPrincipalByUserId(session.userId),
};
});

View File

@@ -0,0 +1,12 @@
import { route } from "@platform/relay";
import z from "zod";
import { PrincipalSchema } from "../../../models/principal.ts";
import { SessionSchema } from "../../../models/session.ts";
export default route.get("/api/v1/identity/session").response(
z.object({
session: SessionSchema,
principal: PrincipalSchema,
}),
);

62
modules/iam/server.ts Normal file
View File

@@ -0,0 +1,62 @@
import { HttpAdapter, makeClient } from "@platform/relay";
import { config } from "./config.ts";
import resolve from "./routes/session/resolve/spec.ts";
/*
|--------------------------------------------------------------------------------
| Internal Session Resolver
|--------------------------------------------------------------------------------
*/
const identity = makeClient(
{
adapter: new HttpAdapter({
url: config.url,
}),
},
{
resolve: resolve.crypto({
publicKey: config.internal.publicKey,
}),
},
);
export async function getPrincipalSession(payload: { headers: Headers }) {
const response = await identity.resolve(payload);
if ("data" in response) {
return response.data;
}
}
/*
|--------------------------------------------------------------------------------
| Server Exports
|--------------------------------------------------------------------------------
*/
export * from "./services/session.ts";
export * from "./types.ts";
/*
|--------------------------------------------------------------------------------
| Module Server
|--------------------------------------------------------------------------------
*/
export default {
routes: [
(await import("./routes/identities/get/handle.ts")).default,
(await import("./routes/identities/update/handle.ts")).default,
(await import("./routes/login/code/handle.ts")).default,
(await import("./routes/login/email/handle.ts")).default,
// (await import("./routes/login/password/handle.ts")).default,
(await import("./routes/login/sudo/handle.ts")).default,
(await import("./routes/me/handle.ts")).default,
(await import("./routes/roles/handle.ts")).default,
(await import("./routes/session/resolve/handle.ts")).default,
(await import("./routes/access/is-allowed/handle.ts")).default,
(await import("./routes/access/check-resource/handle.ts")).default,
(await import("./routes/access/check-resources/handle.ts")).default,
],
};

View File

@@ -0,0 +1,29 @@
import { logger } from "@platform/logger";
import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import { emailOTP } from "better-auth/plugins";
import { db } from "./database.ts";
export const auth = betterAuth({
database: mongodbAdapter(db.db),
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60, // Cache duration in seconds
},
},
plugins: [
emailOTP({
async sendVerificationOTP({ email, otp, type }) {
if (type === "sign-in") {
logger.info({ email, otp, type });
} else if (type === "email-verification") {
// Send the OTP for email verification
} else {
// Send the OTP for password reset
}
},
}),
],
});

View File

@@ -0,0 +1,61 @@
import { getDatabaseAccessor } from "@platform/database/accessor.ts";
import {
PRINCIPAL_TYPE_NAMES,
type Principal,
PrincipalSchema,
PrincipalTypeId,
parsePrincipal,
} from "../models/principal.ts";
export const db = getDatabaseAccessor<{
principal: Principal;
}>("auth");
/*
|--------------------------------------------------------------------------------
| Methods
|--------------------------------------------------------------------------------
*/
export async function getPrincipalById(id: string): Promise<Principal | undefined> {
return db
.collection("principal")
.findOne({ id })
.then((value) => parsePrincipal(value));
}
export async function setPrincipalRolesById(id: string, roles: string[]): Promise<void> {
await db.collection("principal").updateOne({ id }, { $set: { roles } });
}
export async function setPrincipalAttributesById(id: string, attr: Record<string, any>): Promise<void> {
await db.collection("principal").updateOne({ id }, { $set: { attr } });
}
/**
* Retrieve a principal for a better-auth user.
*
* @param userId - User id from better-auth user list.
*/
export async function getPrincipalByUserId(userId: string): Promise<Principal> {
const principal = await db.collection("principal").findOneAndUpdate(
{ id: userId },
{
$setOnInsert: {
id: userId,
type: {
id: PrincipalTypeId.User,
name: PRINCIPAL_TYPE_NAMES[PrincipalTypeId.User],
},
roles: ["user"],
attr: {},
},
},
{ upsert: true, returnDocument: "after" },
);
if (principal === null) {
throw new Error("Failed to resolve Principal");
}
return PrincipalSchema.parse(principal);
}

View File

@@ -0,0 +1,3 @@
import { logger as platformLogger } from "@platform/logger";
export const logger = platformLogger.prefix("Modules/Identity");

View File

@@ -0,0 +1,34 @@
import cookie from "cookie";
import { config } from "../config.ts";
import { auth } from "./auth.ts";
/**
* Get session headers which can be applied on a Response object to apply
* an authenticated session to the respondent.
*
* @param accessToken - Token to apply to the cookie.
* @param maxAge - Max age of the token.
*/
export async function getSessionHeaders(accessToken: string, maxAge: number): Promise<Headers> {
return new Headers({
"set-cookie": cookie.serialize(
"better-auth.session_token",
encodeURIComponent(accessToken), // URL-encode the token
config.cookie(maxAge),
),
});
}
/**
* Get session container from request headers.
*
* @param headers - Request headers to extract session from.
*/
export async function getSessionByRequestHeader(headers: Headers) {
const response = await auth.api.getSession({ headers });
if (response === null) {
return undefined;
}
return response.session;
}

50
modules/iam/types.ts Normal file
View File

@@ -0,0 +1,50 @@
import "@platform/relay";
import "@platform/storage";
import type { Session } from "better-auth";
import type { identity } from "./client.ts";
import type { Principal } from "./models/principal.ts";
declare module "@platform/storage" {
interface StorageContext {
/**
* TODO ...
*/
session?: Session;
/**
* TODO ...
*/
principal?: Principal;
/**
* TODO ...
*/
access?: typeof identity.access;
}
}
declare module "@platform/relay" {
interface ServerContext {
/**
* TODO ...
*/
isAuthenticated: boolean;
/**
* TODO ...
*/
session: Session;
/**
* TODO ...
*/
principal: Principal;
/**
* TODO ...
*/
access: typeof identity.access;
}
}