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