Template
1
0

feat: encapsulate identity with better-auth

This commit is contained in:
2025-09-25 13:24:32 +02:00
parent 99111b69eb
commit f2ba21a7e3
48 changed files with 718 additions and 766 deletions

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCy5ZoXkKP9mZTk
sKbQdSwspHZqyMH33Gby23+9ycNHMIww7djcWFfPRW4s7tu3SNaac6qVg9OI43+Z
6BPXxuh4nhQ4LX5No9iVEmcWvZtKE4ghwzsoU0llT7+aKl9UYvgqU1YX4zyfiyo2
bW0nVPasEHTyjLCVPK5BKlq+UmuyJTVcduALDnVETpUefu5Vca6tIRXsOovvAf5b
zmcxPccaXIatR/AeipxT0YWoInn8dxD3kyFgTPXtinuBZxvp6MUeSs5IE8OJRJRP
PEo1MQ9HFw9aYRIn9uIkbARbNZMGz77zB1+0TrPGyKOB5lLReWGMUFAJhjLrnTsY
z19se4kNAgMBAAECgf9QkG6A6ViiHIMnUskIDeP5Xir19d9kbGwrcn0F2OXYaX+l
Oot9w3KM6loRJx380/zk/e0Uch1MeZ2fyqQRUmAGQIzkXUm6LUWIekYQN6vZ3JlP
YA2/M+otdd8Tpws9hFSDMUlx0SP3GAi0cE48xdBkVAT0NjZ3Jjor7Wv6GLe//Kzg
1OVrbPAA/+RrPB+BQn5nmZFT0aLuLpyxB4f4ArHG/8DEBY49Syy7/3Ke0kfHMnhl
5Eg5Yau89wSLqEoUSuQvNixu/5nTTQ6v1VYPVG8D1hn773SbNoY9o5vZOPRl1P0q
9YC/qpzPJkm/A5TZLsoalIxuGTdwts+DaEeoKmECgYEA5CddLQbMNu9kYElxpSA3
xXoTL71ZBCQsWExmJrcGe2lQhGO40lF8jE6QnEvMt0mp8Dg9n2ih4J87+2Ozb0fp
2G2ilNeMxM7keywA/+Cwg71QyImppU0lQ5PYLv+pllfxN8FPpLBluy7rDahzphkn
1rijqI5d4bHNG6IgD2ynteECgYEAyLs2eBWxX39Jff3OdpSVmHf7NtacbtsUf1qM
RJSvLsiSwKn39n1+Y6ebzftxm/XD/j8FbN8XvMZMI4OrlfzP+YJaTybIbHrLzCE2
B5E9j0GbJRhJ/D3l9FQBGdY4g5yC4mgbncXURQqqQTtKk2d+ixZSrw8iyDGN+aMJ
ybqZoK0CgYALb6GvARk5Y7R/Uw8cPMou3tiZWv9cQsfqQSIZrLDpfLTpfeokuKrq
iYGcI/yF725SOS91jxQWI0Upa6zx1gP1skEk/szyjIBNYD5IlSWj5NhoxOW5AG3u
vjlm2a/RdmUD62+njKP8xvRHQftSBw7FJ4okh8ZS6suiJ/U9cK/TYQKBgFg+jTyP
dNGhuKJN0NUqjvVfUa4S/ORzJXizStTfdIAhpvpR/nN7SfPvfDw6nQBOM+JyvCTX
kqznlBNM0EL4yElNN/xx9UxTU4Ki2wjKngB7fAP7wJLGd3BI+c7s8R1S0etMj091
59KOVLimoytYJTZqEuFoywatWlfzh9sKUH1lAoGBAID6mqGL3SZhh+i2/kAytfzw
UswTQqA0CCBTzN/Eo1QozmUVTLQPj8rBchNSoiSc92y+lPIL8ePdU7imRB77i+9D
9MSmc5u3ACACOSkwF0JCEGN+Rju4HR5wwm3h6Kvf/FQ3yvSEOKAWhqXIY95qtYTU
j3O+iJbY32pbQsawIAkw
-----END PRIVATE KEY-----

View File

@@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsuWaF5Cj/ZmU5LCm0HUs
LKR2asjB99xm8tt/vcnDRzCMMO3Y3FhXz0VuLO7bt0jWmnOqlYPTiON/megT18bo
eJ4UOC1+TaPYlRJnFr2bShOIIcM7KFNJZU+/mipfVGL4KlNWF+M8n4sqNm1tJ1T2
rBB08oywlTyuQSpavlJrsiU1XHbgCw51RE6VHn7uVXGurSEV7DqL7wH+W85nMT3H
GlyGrUfwHoqcU9GFqCJ5/HcQ95MhYEz17Yp7gWcb6ejFHkrOSBPDiUSUTzxKNTEP
RxcPWmESJ/biJGwEWzWTBs++8wdftE6zxsijgeZS0XlhjFBQCYYy6507GM9fbHuJ
DQIDAQAB
-----END PUBLIC KEY-----

View File

@@ -11,4 +11,4 @@ resourcePolicy:
- actions: ["manage"]
effect: EFFECT_ALLOW
roles: ["admin"]
roles: ["super"]

View File

@@ -1,4 +1,5 @@
import { getEnvironmentVariable } from "@platform/config/environment.ts";
import { SerializeOptions } from "cookie";
import z from "zod";
export const config = {
@@ -7,4 +8,37 @@ export const config = {
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>;

View File

@@ -8,10 +8,14 @@
"./server.ts": "./server.ts"
},
"dependencies": {
"@platform/cerbos": "workspace:*",
"@platform/config": "workspace:*",
"@platform/logger": "workspace:*",
"@platform/relay": "workspace:*",
"supertokens-node": "23.0.1",
"@platform/storage": "workspace:*",
"@platform/vault": "workspace:*",
"better-auth": "1.3.16",
"cookie": "1.0.2",
"zod": "4.1.11"
}
}

View File

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

View File

@@ -1,15 +1,18 @@
import { ForbiddenError } from "@platform/relay";
import { getPrincipalAttributes } from "@platform/supertoken/principal.ts";
import UserMetadata from "supertokens-node/recipe/usermetadata";
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 decision = await access.isAllowed({ kind: "identity", id, attr: {} }, "update");
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 = await getPrincipalAttributes(id);
const attr = principal.attr;
for (const op of ops) {
switch (op.type) {
case "add": {
@@ -36,5 +39,5 @@ export default route.access("session").handle(async ({ params: { id }, body: ops
}
}
}
await UserMetadata.updateUserMetadata(id, { attr });
await setPrincipalAttributesById(id, attr);
});

View File

@@ -1,25 +1,16 @@
import { logger } from "@platform/logger";
import { NotFoundError } from "@platform/relay";
import { getSessionHeaders } from "@platform/supertoken/session.ts";
import Passwordless from "supertokens-node/recipe/passwordless";
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: { preAuthSessionId, deviceId, userInputCode } }) => {
const response = await Passwordless.consumeCode({ tenantId: "public", preAuthSessionId, deviceId, userInputCode });
if (response.status !== "OK") {
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();
}
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),
headers: response.headers,
});
});

View File

@@ -5,9 +5,8 @@ export default route
.post("/api/v1/identity/login/code")
.body(
z.strictObject({
deviceId: z.string(),
preAuthSessionId: z.string(),
userInputCode: z.string(),
email: z.string(),
otp: z.string(),
}),
)
.query({

View File

@@ -1,23 +1,14 @@
import { logger } from "@platform/logger";
import Passwordless from "supertokens-node/recipe/passwordless";
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 Passwordless.createCode({ tenantId: "public", email });
if (response.status !== "OK") {
return logger.info({
const response = await auth.api.sendVerificationOTP({ body: { email, type: "sign-in" } });
if (response.success === false) {
logger.info({
type: "auth:passwordless",
message: "Create code failed.",
message: "OTP Email verification failed.",
received: email,
});
}
logger.info({
type: "auth:passwordless",
data: {
deviceId: response.deviceId,
preAuthSessionId: response.preAuthSessionId,
userInputCode: response.userInputCode,
},
});
});

View File

@@ -1,48 +1,39 @@
import { logger } from "@platform/logger";
import { NotFoundError } from "@platform/relay";
import { getSessionHeaders } from "@platform/supertoken/session.ts";
import Passwordless from "supertokens-node/recipe/passwordless";
import route from "./spec.ts";
export default route.access("public").handle(async ({ body: { email } }) => {
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),
});
// 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

@@ -1,15 +1,19 @@
import { ForbiddenError } from "@platform/relay";
import { getPrincipalRoles } from "@platform/supertoken/principal.ts";
import UserMetadata from "supertokens-node/recipe/usermetadata";
import { 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 decision = await access.isAllowed({ kind: "role", id, attr: {} }, "manage");
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(await getPrincipalRoles(id));
const roles: Set<string> = new Set(principal.roles);
for (const op of ops) {
switch (op.type) {
case "add": {
@@ -26,5 +30,5 @@ export default route.access("session").handle(async ({ params: { id }, body: ops
}
}
}
await UserMetadata.updateUserMetadata(id, { roles: Array.from(roles) });
await setPrincipalRolesById(id, Array.from(roles));
});

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

View File

@@ -1,3 +1,43 @@
import { HttpAdapter, makeClient } from "@platform/relay";
import { config } from "./config.ts";
import resolve from "./routes/session/resolve/spec.ts";
/*
|--------------------------------------------------------------------------------
| Internal Session Resolver
|--------------------------------------------------------------------------------
*/
export const identity = makeClient(
{
adapter: new HttpAdapter({
url: config.url,
}),
},
{
resolve: resolve.crypto({
publicKey: config.internal.publicKey,
}),
},
);
/*
|--------------------------------------------------------------------------------
| Server Exports
|--------------------------------------------------------------------------------
*/
export * from "./services/access.ts";
export * from "./services/session.ts";
export * from "./types.ts";
/*
|--------------------------------------------------------------------------------
| Module Server
|--------------------------------------------------------------------------------
*/
export default {
routes: [
(await import("./routes/identities/get/handle.ts")).default,
@@ -8,5 +48,6 @@ export 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,
],
};

View File

@@ -0,0 +1,88 @@
import { cerbos } from "@platform/cerbos";
import { Principal } from "../models/principal.ts";
export function getAccessControlMethods(principal: Principal) {
return {
/**
* 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(resource: any, action: string) {
return cerbos.isAllowed({ principal, resource, action });
},
/**
* 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(resource: any, actions: string[]) {
return cerbos.checkResource({ principal, resource, actions });
},
/**
* 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(resources: { resource: any; actions: string[] }[]) {
return cerbos.checkResources({ principal, resources });
},
};
}
export type AccessControlMethods = ReturnType<typeof getAccessControlMethods>;

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 {
parsePrincipal,
type Principal,
PRINCIPAL_TYPE_NAMES,
PrincipalSchema,
PrincipalTypeId,
} 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/identity/types.ts Normal file
View File

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

View File

@@ -16,18 +16,18 @@ resourcePolicy:
roles: ["super", "admin", "user"]
condition:
match:
expr: R.attr.id in P.attr.workspaceIds
expr: R.attr.workspaceId in P.attr.workspaceIds
- actions: ["update"]
effect: EFFECT_ALLOW
roles: ["super", "admin"]
condition:
match:
expr: R.attr.id in P.attr.workspaceIds
expr: R.attr.workspaceId in P.attr.workspaceIds
- actions: ["delete"]
effect: EFFECT_ALLOW
roles: ["super"]
condition:
match:
expr: R.attr.id in P.attr.workspaceIds
expr: R.attr.workspaceId in P.attr.workspaceIds

View File

@@ -1,4 +1,4 @@
import { container } from "@platform/database/container.ts";
import { mongo } from "@platform/database/client.ts";
import { EventFactory, EventStore, Prettify, Projector } from "@valkyr/event-store";
import { MongoAdapter } from "@valkyr/event-store/mongo";
@@ -20,7 +20,7 @@ const eventFactory = new EventFactory([
*/
export const eventStore = new EventStore({
adapter: new MongoAdapter(() => container.get("mongo"), `workspace:event-store`),
adapter: new MongoAdapter(() => mongo, `workspace:event-store`),
events: eventFactory,
snapshot: "auto",
});