feat: encapsulate identity with better-auth
This commit is contained in:
88
modules/identity/services/access.ts
Normal file
88
modules/identity/services/access.ts
Normal 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>;
|
||||
29
modules/identity/services/auth.ts
Normal file
29
modules/identity/services/auth.ts
Normal 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
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
61
modules/identity/services/database.ts
Normal file
61
modules/identity/services/database.ts
Normal 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);
|
||||
}
|
||||
3
modules/identity/services/logger.ts
Normal file
3
modules/identity/services/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { logger as platformLogger } from "@platform/logger";
|
||||
|
||||
export const logger = platformLogger.prefix("Modules/Identity");
|
||||
34
modules/identity/services/session.ts
Normal file
34
modules/identity/services/session.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user