@antelopejs/dms-frontend 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +131 -0
- package/dist/commands/build.js +66 -0
- package/dist/commands/clean.js +42 -0
- package/dist/commands/dev.js +116 -0
- package/dist/commands/prepare.js +60 -0
- package/dist/commands/start.js +49 -0
- package/dist/commands/verify-source.js +39 -0
- package/dist/common.js +24 -0
- package/dist/config.js +142 -0
- package/dist/discovery.js +123 -0
- package/dist/fs-sync.js +142 -0
- package/dist/index.js +44 -0
- package/dist/layer-watch.js +154 -0
- package/dist/layers.js +120 -0
- package/dist/manifest.js +109 -0
- package/dist/materialize.js +249 -0
- package/dist/ports.js +30 -0
- package/dist/update-check.js +173 -0
- package/dist/utils/cli-ui.js +178 -0
- package/dist/verify-source-runner.js +228 -0
- package/dist/workspace-setup.js +109 -0
- package/dist/workspace.js +76 -0
- package/package.json +97 -0
- package/templates/vue/DmsDynamicPage.vue +89 -0
- package/templates/vue/app-config-stub.mjs +1 -0
- package/templates/vue/app-runtime.ts +240 -0
- package/templates/vue/compress-assets.mjs +48 -0
- package/templates/vue/email-locales.ts +32 -0
- package/templates/vue/email-renderer.ts +159 -0
- package/templates/vue/email-runtime.ts +23 -0
- package/templates/vue/frontend-module.ts +1418 -0
- package/templates/vue/globals.d.ts +1 -0
- package/templates/vue/index.html +24 -0
- package/templates/vue/main.ts +33 -0
- package/templates/vue/npmrc +2 -0
- package/templates/vue/package.json +35 -0
- package/templates/vue/pnpm-workspace.yaml +4 -0
- package/templates/vue/server/auth/backend.mjs +83 -0
- package/templates/vue/server/auth/client-ip.mjs +52 -0
- package/templates/vue/server/auth/oauth.mjs +213 -0
- package/templates/vue/server/auth/routes.mjs +254 -0
- package/templates/vue/server/auth/session.mjs +180 -0
- package/templates/vue/server/client-manifest.mjs +116 -0
- package/templates/vue/server/email.mjs +36 -0
- package/templates/vue/server/inertia.mjs +79 -0
- package/templates/vue/server/render-token.mjs +81 -0
- package/templates/vue/server/tester.mjs +228 -0
- package/templates/vue/server.mjs +526 -0
- package/templates/vue/ssr-renderer.ts +146 -0
- package/templates/vue/tsconfig.json +31 -0
- package/templates/vue/typecheck-loader.mjs +13 -0
- package/templates/vue/vite.config.ts +161 -0
- package/templates/vue/vite.email.config.ts +77 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare const defineAppConfig: typeof import("./frontend-module").defineAppConfig;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="data:," />
|
|
7
|
+
<script>
|
|
8
|
+
const preference = localStorage.getItem("dms-color-mode") || "system";
|
|
9
|
+
const mode =
|
|
10
|
+
preference === "system"
|
|
11
|
+
? matchMedia("(prefers-color-scheme: dark)").matches
|
|
12
|
+
? "dark"
|
|
13
|
+
: "light"
|
|
14
|
+
: preference;
|
|
15
|
+
document.documentElement.classList.add(mode);
|
|
16
|
+
</script>
|
|
17
|
+
<title>Antelope DMS</title>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div class="isolate">__DMS_APP__</div>
|
|
21
|
+
<div id="dms-overlays" class="isolate"></div>
|
|
22
|
+
<script type="module" src="/main.ts" fetchpriority="low"></script>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import "./dms-main.css";
|
|
2
|
+
import { createInertiaApp } from "@inertiajs/vue3";
|
|
3
|
+
import { createHead } from "@unhead/vue/client";
|
|
4
|
+
import { createApp, createSSRApp, h } from "vue";
|
|
5
|
+
import { configureDmsApp, resolveDmsInertiaPage } from "./app-runtime";
|
|
6
|
+
import { setupFrontendModules } from "./frontend-module";
|
|
7
|
+
import { frontendModules } from "./frontend-modules.generated";
|
|
8
|
+
|
|
9
|
+
function markDmsReady(): void {
|
|
10
|
+
document.documentElement.dataset.dmsReady = "true";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
await setupFrontendModules(frontendModules);
|
|
14
|
+
|
|
15
|
+
createInertiaApp({
|
|
16
|
+
resolve: resolveDmsInertiaPage,
|
|
17
|
+
async setup({ App, el, props, plugin }) {
|
|
18
|
+
const root = { render: () => h(App, props) };
|
|
19
|
+
const app = el.hasAttribute("data-server-rendered")
|
|
20
|
+
? createSSRApp(root)
|
|
21
|
+
: createApp(root);
|
|
22
|
+
const configured = await configureDmsApp({
|
|
23
|
+
app,
|
|
24
|
+
head: createHead(),
|
|
25
|
+
initialPageProps: props.initialPage.props,
|
|
26
|
+
initialPageUrl: props.initialPage.url,
|
|
27
|
+
inertiaPlugin: plugin,
|
|
28
|
+
});
|
|
29
|
+
app.mount(el);
|
|
30
|
+
markDmsReady();
|
|
31
|
+
await configured.mounted();
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dms-frontend-workspace",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build && node compress-assets.mjs && vite build --ssr ssr-renderer.ts --outDir dist/ssr && vite build --config vite.email.config.ts",
|
|
8
|
+
"typecheck": "node --import ./typecheck-loader.mjs ./node_modules/vue-tsc/bin/vue-tsc.js --noEmit",
|
|
9
|
+
"start": "node server.mjs"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@inertiajs/vue3": "^3.7.0",
|
|
13
|
+
"@nuxt/ui": "4.10.0",
|
|
14
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
15
|
+
"@unhead/vue": "^2.0.14",
|
|
16
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
17
|
+
"@vue/server-renderer": "^3.5.0",
|
|
18
|
+
"@vueuse/core": "^13.0.0",
|
|
19
|
+
"defu": "^6.1.4",
|
|
20
|
+
"ofetch": "^1.4.1",
|
|
21
|
+
"reka-ui": "^2.6.0",
|
|
22
|
+
"tailwindcss": "^4.0.0",
|
|
23
|
+
"ufo": "^1.6.1",
|
|
24
|
+
"unplugin-auto-import": "^20.2.0",
|
|
25
|
+
"unplugin-vue-components": "^29.0.0",
|
|
26
|
+
"vite": "^7.0.0",
|
|
27
|
+
"vue": "^3.5.0",
|
|
28
|
+
"vue-i18n": "^11.1.12"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typescript": "^5.8.2",
|
|
32
|
+
"vue-tsc": "3.1.1"
|
|
33
|
+
},
|
|
34
|
+
"packageManager": "pnpm@10.6.5"
|
|
35
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { clientIp } from "./client-ip.mjs";
|
|
2
|
+
|
|
3
|
+
const FORWARDED_HEADERS = [
|
|
4
|
+
"user-agent",
|
|
5
|
+
"x-content-language",
|
|
6
|
+
"x-realtime-session",
|
|
7
|
+
];
|
|
8
|
+
const BODY_LIMIT = 64 * 1024;
|
|
9
|
+
|
|
10
|
+
export class UpstreamError extends Error {
|
|
11
|
+
constructor(status, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PUBLIC_AUTH_MESSAGE = /^error\.[a-z0-9_.-]{1,100}$/;
|
|
18
|
+
|
|
19
|
+
export function publicBackendMessage(data) {
|
|
20
|
+
const message = typeof data === "string" ? data : data?.message;
|
|
21
|
+
return typeof message === "string" && PUBLIC_AUTH_MESSAGE.test(message)
|
|
22
|
+
? message
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function backend(path, request, options = {}) {
|
|
27
|
+
const headers = Object.fromEntries(
|
|
28
|
+
FORWARDED_HEADERS.flatMap((name) =>
|
|
29
|
+
request.headers[name] ? [[name, request.headers[name]]] : [],
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
headers["x-forwarded-for"] = clientIp(request);
|
|
33
|
+
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
34
|
+
if (options.relay)
|
|
35
|
+
headers["x-dms-oauth-relay"] = process.env.DMS_OAUTH_RELAY_SECRET ?? "";
|
|
36
|
+
if (options.body !== undefined) headers["content-type"] = "application/json";
|
|
37
|
+
const response = await fetch(new URL(path, process.env.DMS_BACKEND_URL), {
|
|
38
|
+
method: options.method ?? "GET",
|
|
39
|
+
headers,
|
|
40
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
41
|
+
});
|
|
42
|
+
const text = await response.text();
|
|
43
|
+
let data;
|
|
44
|
+
try {
|
|
45
|
+
data = JSON.parse(text);
|
|
46
|
+
} catch {
|
|
47
|
+
data = response.ok ? {} : text;
|
|
48
|
+
}
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const message = publicBackendMessage(data) ?? "DMS backend request failed";
|
|
51
|
+
throw new UpstreamError(response.status, message);
|
|
52
|
+
}
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function body(request) {
|
|
57
|
+
const chunks = [];
|
|
58
|
+
let size = 0;
|
|
59
|
+
for await (const chunk of request) {
|
|
60
|
+
size += chunk.length;
|
|
61
|
+
if (size > BODY_LIMIT)
|
|
62
|
+
throw new RequestBodyError(413, "Request body too large");
|
|
63
|
+
chunks.push(chunk);
|
|
64
|
+
}
|
|
65
|
+
if (!chunks.length) return {};
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
68
|
+
} catch {
|
|
69
|
+
throw new RequestBodyError(400, "Invalid request body");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class RequestBodyError extends Error {
|
|
74
|
+
constructor(status, message) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.status = status;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function json(response, status, value) {
|
|
81
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
82
|
+
response.end(JSON.stringify(value));
|
|
83
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function trustedHops() {
|
|
2
|
+
const value = Number.parseInt(process.env.DMS_TRUSTED_PROXY_HOPS ?? "0", 10);
|
|
3
|
+
return Number.isSafeInteger(value) && value > 0 ? value : 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function requestScheme(request) {
|
|
7
|
+
if (trustedHops()) {
|
|
8
|
+
const forwarded = String(request.headers["x-forwarded-proto"] ?? "")
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((value) => value.trim().toLowerCase())
|
|
11
|
+
.filter((value) => value === "http" || value === "https")
|
|
12
|
+
.at(-1);
|
|
13
|
+
if (forwarded) return forwarded;
|
|
14
|
+
}
|
|
15
|
+
return request.socket?.encrypted ? "https" : "http";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requestHost(request) {
|
|
19
|
+
if (!trustedHops()) return request.headers.host;
|
|
20
|
+
return (
|
|
21
|
+
String(request.headers["x-forwarded-host"] ?? "")
|
|
22
|
+
.split(",")
|
|
23
|
+
.map((value) => value.trim())
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.at(-1) || request.headers.host
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isSameOrigin(request) {
|
|
30
|
+
const origin = request.headers.origin;
|
|
31
|
+
if (!origin) return false;
|
|
32
|
+
try {
|
|
33
|
+
return (
|
|
34
|
+
new URL(origin).origin ===
|
|
35
|
+
new URL(`${requestScheme(request)}://${requestHost(request)}`).origin
|
|
36
|
+
);
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function clientIp(request) {
|
|
43
|
+
const socketIp = request.socket?.remoteAddress || "unknown";
|
|
44
|
+
const hops = trustedHops();
|
|
45
|
+
if (!hops) return socketIp;
|
|
46
|
+
const forwarded = String(request.headers["x-forwarded-for"] ?? "")
|
|
47
|
+
.split(",")
|
|
48
|
+
.map((value) => value.trim())
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
const chain = [...forwarded, socketIp];
|
|
51
|
+
return chain[Math.max(0, chain.length - hops - 1)] ?? socketIp;
|
|
52
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { backend, json } from "./backend.mjs";
|
|
3
|
+
import { clientIp } from "./client-ip.mjs";
|
|
4
|
+
import {
|
|
5
|
+
clearSealedCookie,
|
|
6
|
+
persistAccountSession,
|
|
7
|
+
readSealedCookie,
|
|
8
|
+
writeSealedCookie,
|
|
9
|
+
} from "./session.mjs";
|
|
10
|
+
|
|
11
|
+
const FLOW = "dms_oauth_flow";
|
|
12
|
+
const HANDOFF = "dms_oauth_handoff";
|
|
13
|
+
const COMPLETE = "/auth/oauth/complete";
|
|
14
|
+
const FLOW_LIFETIME_MS = 10 * 60 * 1000;
|
|
15
|
+
const RATE_WINDOW_MS = 60 * 1000;
|
|
16
|
+
const RATE_LIMIT = 10;
|
|
17
|
+
const MAX_RATE_LIMIT_CLIENTS = 10_000;
|
|
18
|
+
const MAX_CALLBACK_PARAMETER_LENGTH = 2048;
|
|
19
|
+
const attempts = new Map();
|
|
20
|
+
|
|
21
|
+
export function resolveOAuthClientKey(request) {
|
|
22
|
+
return clientIp(request);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sweepAttempts(now) {
|
|
26
|
+
for (const [key, window] of attempts) {
|
|
27
|
+
if (now - window.startedAt >= RATE_WINDOW_MS) attempts.delete(key);
|
|
28
|
+
}
|
|
29
|
+
while (attempts.size >= MAX_RATE_LIMIT_CLIENTS)
|
|
30
|
+
attempts.delete(attempts.keys().next().value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function hitOAuthRateLimitKey(key, now = Date.now()) {
|
|
34
|
+
sweepAttempts(now);
|
|
35
|
+
const window = attempts.get(key);
|
|
36
|
+
if (!window || now - window.startedAt >= RATE_WINDOW_MS) {
|
|
37
|
+
attempts.set(key, { count: 1, startedAt: now });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
window.count += 1;
|
|
41
|
+
return window.count <= RATE_LIMIT;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rateLimited(request, scope) {
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
return !hitOAuthRateLimitKey(
|
|
47
|
+
`${scope}:${resolveOAuthClientKey(request)}`,
|
|
48
|
+
now,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isValidOAuthProvider(provider) {
|
|
53
|
+
return typeof provider === "string" && /^[a-z0-9-]{1,32}$/.test(provider);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safePath(value) {
|
|
57
|
+
if (
|
|
58
|
+
typeof value !== "string" ||
|
|
59
|
+
!value.startsWith("/") ||
|
|
60
|
+
value.startsWith("//") ||
|
|
61
|
+
value.includes("\\")
|
|
62
|
+
)
|
|
63
|
+
return "";
|
|
64
|
+
try {
|
|
65
|
+
const decoded = decodeURIComponent(value);
|
|
66
|
+
return decoded.startsWith("//") || decoded.includes("\\") ? "" : value;
|
|
67
|
+
} catch {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function redirect(response, location) {
|
|
73
|
+
response.writeHead(302, { location });
|
|
74
|
+
response.end();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function errorRedirect(response, key) {
|
|
78
|
+
redirect(response, `/auth?oauth_error=${encodeURIComponent(key)}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function oauthStart(request, response, provider, url) {
|
|
82
|
+
if (
|
|
83
|
+
!isValidOAuthProvider(provider) ||
|
|
84
|
+
request.headers["sec-fetch-site"] === "cross-site" ||
|
|
85
|
+
rateLimited(request, "start")
|
|
86
|
+
)
|
|
87
|
+
return errorRedirect(response, "error.oauth.failed");
|
|
88
|
+
try {
|
|
89
|
+
const result = await backend(
|
|
90
|
+
`/api/auth/oauth/${provider}/authorize-url`,
|
|
91
|
+
request,
|
|
92
|
+
{ relay: true },
|
|
93
|
+
);
|
|
94
|
+
writeSealedCookie(
|
|
95
|
+
response,
|
|
96
|
+
FLOW,
|
|
97
|
+
{
|
|
98
|
+
state: result.state,
|
|
99
|
+
expiresAt: Date.now() + FLOW_LIFETIME_MS,
|
|
100
|
+
redirect: safePath(url.searchParams.get("redirect")),
|
|
101
|
+
language: url.searchParams.get("language") ?? "",
|
|
102
|
+
invite: url.searchParams.get("invite") ?? "",
|
|
103
|
+
},
|
|
104
|
+
FLOW_LIFETIME_MS / 1000,
|
|
105
|
+
);
|
|
106
|
+
redirect(response, result.authorizeUrl);
|
|
107
|
+
} catch {
|
|
108
|
+
errorRedirect(response, "error.oauth.failed");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sameState(left, right) {
|
|
113
|
+
if (typeof left !== "string" || typeof right !== "string") return false;
|
|
114
|
+
const a = Buffer.from(left);
|
|
115
|
+
const b = Buffer.from(right);
|
|
116
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validCallback(flow, url) {
|
|
120
|
+
const code = url.searchParams.get("code");
|
|
121
|
+
const state = url.searchParams.get("state");
|
|
122
|
+
return !(
|
|
123
|
+
!flow ||
|
|
124
|
+
flow.expiresAt < Date.now() ||
|
|
125
|
+
typeof code !== "string" ||
|
|
126
|
+
code.length > MAX_CALLBACK_PARAMETER_LENGTH ||
|
|
127
|
+
typeof state !== "string" ||
|
|
128
|
+
state.length > MAX_CALLBACK_PARAMETER_LENGTH ||
|
|
129
|
+
!sameState(state, flow.state)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function storeOAuthResult(request, response, provider, result) {
|
|
134
|
+
if (result.requires_2fa)
|
|
135
|
+
writeSealedCookie(
|
|
136
|
+
response,
|
|
137
|
+
HANDOFF,
|
|
138
|
+
{ kind: "2fa", token: result.two_factor_token, methods: result.methods },
|
|
139
|
+
300,
|
|
140
|
+
);
|
|
141
|
+
else if (result.requires_tenant_assignment)
|
|
142
|
+
writeSealedCookie(
|
|
143
|
+
response,
|
|
144
|
+
HANDOFF,
|
|
145
|
+
{ kind: "no-workspace", token: result.tenant_assignment_token, provider },
|
|
146
|
+
300,
|
|
147
|
+
);
|
|
148
|
+
else persistAccountSession(request, response, sessionFrom(result));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function completeOAuth(request, response, provider, url, flow) {
|
|
152
|
+
try {
|
|
153
|
+
const result = await backend(
|
|
154
|
+
`/api/auth/oauth/${provider}/callback`,
|
|
155
|
+
request,
|
|
156
|
+
{
|
|
157
|
+
method: "POST",
|
|
158
|
+
relay: true,
|
|
159
|
+
body: {
|
|
160
|
+
code: url.searchParams.get("code"),
|
|
161
|
+
state: url.searchParams.get("state"),
|
|
162
|
+
state_cookie: flow.state,
|
|
163
|
+
language: flow.language || undefined,
|
|
164
|
+
invite: flow.invite || undefined,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
storeOAuthResult(request, response, provider, result);
|
|
169
|
+
redirect(
|
|
170
|
+
response,
|
|
171
|
+
flow.redirect &&
|
|
172
|
+
!result.requires_2fa &&
|
|
173
|
+
!result.requires_tenant_assignment
|
|
174
|
+
? `${COMPLETE}?redirect=${encodeURIComponent(flow.redirect)}`
|
|
175
|
+
: COMPLETE,
|
|
176
|
+
);
|
|
177
|
+
} catch {
|
|
178
|
+
errorRedirect(response, "error.oauth.failed");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function oauthCallback(request, response, provider, url) {
|
|
183
|
+
if (!isValidOAuthProvider(provider) || rateLimited(request, "callback"))
|
|
184
|
+
return errorRedirect(response, "error.oauth.failed");
|
|
185
|
+
const flow = readSealedCookie(request, FLOW);
|
|
186
|
+
clearSealedCookie(response, FLOW);
|
|
187
|
+
if (url.searchParams.has("error"))
|
|
188
|
+
return errorRedirect(response, "error.oauth.cancelled");
|
|
189
|
+
if (!validCallback(flow, url))
|
|
190
|
+
return errorRedirect(response, "error.oauth.invalid_state");
|
|
191
|
+
return completeOAuth(request, response, provider, url, flow);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function oauthHandoff(request, response) {
|
|
195
|
+
const handoff = readSealedCookie(request, HANDOFF) ?? { kind: "none" };
|
|
196
|
+
clearSealedCookie(response, HANDOFF);
|
|
197
|
+
json(response, 200, handoff);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function sessionFrom(result) {
|
|
201
|
+
const payload = JSON.parse(
|
|
202
|
+
Buffer.from(
|
|
203
|
+
result.access_token.split(".")[1] ?? "",
|
|
204
|
+
"base64url",
|
|
205
|
+
).toString() || "{}",
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
user: result.user,
|
|
209
|
+
accessToken: result.access_token,
|
|
210
|
+
refreshToken: result.refresh_token,
|
|
211
|
+
activeTenantId: payload.tenantId ?? "default",
|
|
212
|
+
};
|
|
213
|
+
}
|