feat(auth): implement portal SSO and local admin authentication
Added support for portal SSO with JWT authentication and local admin login. Updated environment configuration to include AUTH_REQUIRED, AUTH_JWT_SECRET, AUTH_ISSUER, and AUTH_PORTAL_URL. Enhanced the auth plugin to handle JWT verification based on the new configuration. Introduced new routes for authentication and updated the API client to manage token handling and redirects. Improved user experience by integrating authentication checks across various routes and components.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Portal JWT RBAC helpers (mirrors @authportal/shared hasPermission).
|
||||
* Format: cfdm:<section>:<read|write|admin>
|
||||
*/
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
apps: string[];
|
||||
permissions: string[];
|
||||
isAdmin?: boolean;
|
||||
};
|
||||
|
||||
export function hasPermission(
|
||||
granted: readonly string[],
|
||||
required: string,
|
||||
): boolean {
|
||||
if (granted.includes(required)) return true;
|
||||
const parts = required.split(":");
|
||||
if (parts.length !== 3) return false;
|
||||
const [app, section, action] = parts;
|
||||
if (action === "read") {
|
||||
return (
|
||||
granted.includes(`${app}:${section}:write`) ||
|
||||
granted.includes(`${app}:${section}:admin`)
|
||||
);
|
||||
}
|
||||
if (action === "write") {
|
||||
return granted.includes(`${app}:${section}:admin`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
type Rule = {
|
||||
methods: string[];
|
||||
match: (path: string) => boolean;
|
||||
permission: string;
|
||||
};
|
||||
|
||||
const RULES: Rule[] = [
|
||||
{
|
||||
methods: ["GET"],
|
||||
match: (p) =>
|
||||
p.startsWith("/api/v1/domains") ||
|
||||
p.startsWith("/api/v1/domain-monitors") ||
|
||||
p === "/api/v1/domain-monitors",
|
||||
permission: "cfdm:domains:read",
|
||||
},
|
||||
{
|
||||
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
||||
match: (p) =>
|
||||
p.startsWith("/api/v1/domains") ||
|
||||
p.startsWith("/api/v1/domain-monitors"),
|
||||
permission: "cfdm:domains:write",
|
||||
},
|
||||
{
|
||||
methods: ["GET"],
|
||||
match: (p) =>
|
||||
p.startsWith("/api/v1/dns") || p.startsWith("/api/v1/subdomains"),
|
||||
permission: "cfdm:dns:read",
|
||||
},
|
||||
{
|
||||
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
||||
match: (p) =>
|
||||
p.startsWith("/api/v1/dns") || p.startsWith("/api/v1/subdomains"),
|
||||
permission: "cfdm:dns:write",
|
||||
},
|
||||
{
|
||||
methods: ["GET"],
|
||||
match: (p) => p.startsWith("/api/v1/certificates"),
|
||||
permission: "cfdm:certificates:read",
|
||||
},
|
||||
{
|
||||
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
||||
match: (p) => p.startsWith("/api/v1/certificates"),
|
||||
permission: "cfdm:certificates:write",
|
||||
},
|
||||
{
|
||||
methods: ["GET"],
|
||||
match: (p) =>
|
||||
p.startsWith("/api/v1/groups") ||
|
||||
p.startsWith("/api/v1/service-groups"),
|
||||
permission: "cfdm:groups:read",
|
||||
},
|
||||
{
|
||||
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
||||
match: (p) =>
|
||||
p.startsWith("/api/v1/groups") ||
|
||||
p.startsWith("/api/v1/service-groups"),
|
||||
permission: "cfdm:groups:write",
|
||||
},
|
||||
{
|
||||
methods: ["GET"],
|
||||
match: (p) =>
|
||||
p.startsWith("/api/v1/services") ||
|
||||
p.startsWith("/api/v1/service-bindings"),
|
||||
permission: "cfdm:services:read",
|
||||
},
|
||||
{
|
||||
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
||||
match: (p) =>
|
||||
p.startsWith("/api/v1/services") ||
|
||||
p.startsWith("/api/v1/service-bindings"),
|
||||
permission: "cfdm:services:write",
|
||||
},
|
||||
{
|
||||
methods: ["GET", "POST"],
|
||||
match: (p) => p.startsWith("/api/v1/sync"),
|
||||
permission: "cfdm:domains:write",
|
||||
},
|
||||
{
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
||||
match: (p) =>
|
||||
p.startsWith("/api/v1/settings") ||
|
||||
p.startsWith("/api/v1/notifications") ||
|
||||
p.startsWith("/api/v1/health-check") ||
|
||||
p.startsWith("/api/v1/health-checks"),
|
||||
permission: "cfdm:settings:admin",
|
||||
},
|
||||
];
|
||||
|
||||
/** Resolve required permission for method+path, or null if public / unknown. */
|
||||
export function permissionForRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
): string | null {
|
||||
const m = method.toUpperCase();
|
||||
const pathname = path.split("?")[0] ?? path;
|
||||
for (const rule of RULES) {
|
||||
if (!rule.methods.includes(m)) continue;
|
||||
if (rule.match(pathname)) return rule.permission;
|
||||
}
|
||||
// Default: any authenticated cfdm user for unmatched /api/v1/*
|
||||
if (pathname.startsWith("/api/v1/")) return "cfdm:domains:read";
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user