@rimelight/security 0.0.11 → 0.0.13
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/dist/config/index.d.mts +2 -2
- package/dist/csp.d.mts +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +2 -1
- package/dist/middleware/construction.d.mts +20 -0
- package/dist/middleware/construction.mjs +192 -0
- package/dist/middleware/index.d.mts +2 -1
- package/dist/middleware/index.mjs +2 -1
- package/dist/middleware/security.d.mts +1 -1
- package/dist/{types-t69O3m_O.d.mts → types-BdTXMZBc.d.mts} +59 -1
- package/dist/types.d.mts +2 -2
- package/dist/vite.d.mts +1 -1
- package/package.json +13 -3
- package/src/components/ConstructionSignIn.astro +148 -0
package/dist/config/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as SecurityOptions, t as ConstructionOptions } from "../types-BdTXMZBc.mjs";
|
|
2
2
|
import { buildCspHeader } from "../csp.mjs";
|
|
3
|
-
export { SecurityOptions, buildCspHeader };
|
|
3
|
+
export { ConstructionOptions, SecurityOptions, buildCspHeader };
|
package/dist/csp.d.mts
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as SecurityOptions, t as ConstructionOptions } from "./types-BdTXMZBc.mjs";
|
|
2
2
|
import { buildCspHeader } from "./csp.mjs";
|
|
3
3
|
import { RimelightSecurityPlugin, SecurityPluginOptions, security } from "./vite.mjs";
|
|
4
|
-
|
|
4
|
+
import { CONSTRUCTION_GUEST_COOKIE, construction, isConstructionGuest, signInConstructionGuest } from "./middleware/construction.mjs";
|
|
5
|
+
export { CONSTRUCTION_GUEST_COOKIE, ConstructionOptions, RimelightSecurityPlugin, SecurityOptions, SecurityPluginOptions, buildCspHeader, construction, isConstructionGuest, security, security as sri, signInConstructionGuest };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "./types.mjs";
|
|
2
2
|
import { buildCspHeader } from "./csp.mjs";
|
|
3
3
|
import { security } from "./vite.mjs";
|
|
4
|
-
|
|
4
|
+
import { CONSTRUCTION_GUEST_COOKIE, construction, isConstructionGuest, signInConstructionGuest } from "./middleware/construction.mjs";
|
|
5
|
+
export { CONSTRUCTION_GUEST_COOKIE, buildCspHeader, construction, isConstructionGuest, security, security as sri, signInConstructionGuest };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { t as ConstructionOptions } from "../types-BdTXMZBc.mjs";
|
|
2
|
+
//#region src/middleware/construction.d.ts
|
|
3
|
+
export declare const CONSTRUCTION_GUEST_COOKIE = "rimelight-construction-guest";
|
|
4
|
+
/**
|
|
5
|
+
* Validates whether the incoming request carries a valid, signed construction guest cookie.
|
|
6
|
+
*/
|
|
7
|
+
export declare const isConstructionGuest: (c: any, options?: ConstructionOptions) => Promise<boolean>;
|
|
8
|
+
/**
|
|
9
|
+
* Signs and sets the construction guest cookie on the Hono or Astro context.
|
|
10
|
+
*/
|
|
11
|
+
export declare const signInConstructionGuest: (c: any, passphrase: string, rememberMe?: boolean, options?: ConstructionOptions) => Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Universal Construction Mode Middleware for Hono and Astro.
|
|
14
|
+
*
|
|
15
|
+
* Intercepts unauthenticated traffic when CONSTRUCTION_MODE is active, handles POST requests to the
|
|
16
|
+
* guest auth endpoint automatically, and passes authenticated requests or whitelisted assets
|
|
17
|
+
* cleanly.
|
|
18
|
+
*/
|
|
19
|
+
export declare function construction(options?: ConstructionOptions): (c: any, next: any) => Promise<any>;
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
2
|
+
//#region src/middleware/construction.ts
|
|
3
|
+
const CONSTRUCTION_GUEST_COOKIE = "rimelight-construction-guest";
|
|
4
|
+
const DEFAULT_WHITELISTED_PATTERNS = [
|
|
5
|
+
"/construction",
|
|
6
|
+
"/api/auth",
|
|
7
|
+
"/api/construction-guest",
|
|
8
|
+
"/_astro/",
|
|
9
|
+
"/_image",
|
|
10
|
+
"/favicon.",
|
|
11
|
+
"/robots.txt",
|
|
12
|
+
"/sitemap",
|
|
13
|
+
"/.well-known/"
|
|
14
|
+
];
|
|
15
|
+
const getPassphrase = (c, options) => {
|
|
16
|
+
if (options?.passphrase) return options.passphrase;
|
|
17
|
+
const envKey = options?.passphraseEnvKey ?? "CONSTRUCTION_PASSPHRASE";
|
|
18
|
+
return c?.env?.[envKey] ?? (typeof process !== "undefined" ? process.env?.[envKey] : void 0) ?? import.meta.env?.[envKey];
|
|
19
|
+
};
|
|
20
|
+
const isModeEnabled = (c, options) => {
|
|
21
|
+
const envKey = options?.modeEnvKey ?? "CONSTRUCTION_MODE";
|
|
22
|
+
const val = c?.env?.[envKey] ?? (typeof process !== "undefined" ? process.env?.[envKey] : void 0) ?? import.meta.env?.[envKey];
|
|
23
|
+
return val === "true" || val === true;
|
|
24
|
+
};
|
|
25
|
+
const encodeBase64Url = (value) => btoa(String.fromCharCode(...value)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
26
|
+
const decodeBase64Url = (value) => {
|
|
27
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
28
|
+
return Uint8Array.from(atob(padded), (char) => char.charCodeAt(0));
|
|
29
|
+
};
|
|
30
|
+
const signHmac = async (payload, secret) => {
|
|
31
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
|
|
32
|
+
name: "HMAC",
|
|
33
|
+
hash: "SHA-256"
|
|
34
|
+
}, false, ["sign", "verify"]);
|
|
35
|
+
return {
|
|
36
|
+
key,
|
|
37
|
+
signature: await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload))
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const getContextHelpers = (c) => {
|
|
41
|
+
const req = c.req?.raw || c.request || c.req;
|
|
42
|
+
const urlObj = c.url instanceof URL ? c.url : req?.url ? new URL(req.url) : new URL("http://localhost/");
|
|
43
|
+
const path = c.req?.path || urlObj.pathname || "/";
|
|
44
|
+
const method = (c.req?.method || req?.method || "GET").toUpperCase();
|
|
45
|
+
const search = c.req?.url ? new URL(c.req.url).search : urlObj.search || "";
|
|
46
|
+
const redirect = (target) => {
|
|
47
|
+
if (typeof c.redirect === "function") return c.redirect(target);
|
|
48
|
+
const fullTarget = target.startsWith("http") ? target : new URL(target, urlObj.origin).toString();
|
|
49
|
+
return Response.redirect(fullTarget, 302);
|
|
50
|
+
};
|
|
51
|
+
const json = (data, status = 200) => {
|
|
52
|
+
if (typeof c.json === "function") return c.json(data, status);
|
|
53
|
+
return new Response(JSON.stringify(data), {
|
|
54
|
+
status,
|
|
55
|
+
headers: { "Content-Type": "application/json" }
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const getCookieValue = (name) => {
|
|
59
|
+
try {
|
|
60
|
+
const fromHono = getCookie(c, name);
|
|
61
|
+
if (fromHono) return fromHono;
|
|
62
|
+
} catch {}
|
|
63
|
+
if (c.cookies?.get) {
|
|
64
|
+
const val = c.cookies.get(name);
|
|
65
|
+
return typeof val === "string" ? val : val?.value;
|
|
66
|
+
}
|
|
67
|
+
const match = (req?.headers?.get?.("cookie") || "").match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
|
68
|
+
return match ? decodeURIComponent(match[1]) : void 0;
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
path,
|
|
72
|
+
method,
|
|
73
|
+
search,
|
|
74
|
+
redirect,
|
|
75
|
+
json,
|
|
76
|
+
getCookieValue
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Validates whether the incoming request carries a valid, signed construction guest cookie.
|
|
81
|
+
*/
|
|
82
|
+
const isConstructionGuest = async (c, options) => {
|
|
83
|
+
const cookieName = options?.cookieName ?? "rimelight-construction-guest";
|
|
84
|
+
const token = getContextHelpers(c).getCookieValue(cookieName);
|
|
85
|
+
const passphrase = getPassphrase(c, options);
|
|
86
|
+
if (!token || !passphrase) return false;
|
|
87
|
+
try {
|
|
88
|
+
const [encodedPayload, encodedSignature] = token.split(".");
|
|
89
|
+
if (!encodedPayload || !encodedSignature) return false;
|
|
90
|
+
const payload = new TextDecoder().decode(decodeBase64Url(encodedPayload));
|
|
91
|
+
const { key } = await signHmac(payload, passphrase);
|
|
92
|
+
const signatureBuffer = decodeBase64Url(encodedSignature);
|
|
93
|
+
return await crypto.subtle.verify("HMAC", key, signatureBuffer, new TextEncoder().encode(payload)) && JSON.parse(payload).expiresAt > Date.now();
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Signs and sets the construction guest cookie on the Hono or Astro context.
|
|
100
|
+
*/
|
|
101
|
+
const signInConstructionGuest = async (c, passphrase, rememberMe = true, options) => {
|
|
102
|
+
const expectedPassphrase = getPassphrase(c, options);
|
|
103
|
+
if (!expectedPassphrase || !passphrase || passphrase !== expectedPassphrase) return false;
|
|
104
|
+
const cookieName = options?.cookieName ?? "rimelight-construction-guest";
|
|
105
|
+
const maxAge = options?.cookieMaxAge ?? 604800;
|
|
106
|
+
const payload = JSON.stringify({ expiresAt: Date.now() + maxAge * 1e3 });
|
|
107
|
+
const { signature } = await signHmac(payload, expectedPassphrase);
|
|
108
|
+
const cookieValue = encodeBase64Url(new TextEncoder().encode(payload)) + "." + encodeBase64Url(new Uint8Array(signature));
|
|
109
|
+
try {
|
|
110
|
+
setCookie(c, cookieName, cookieValue, {
|
|
111
|
+
httpOnly: true,
|
|
112
|
+
secure: true,
|
|
113
|
+
sameSite: "Lax",
|
|
114
|
+
path: "/",
|
|
115
|
+
...rememberMe ? { maxAge } : {}
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
if (c.cookies?.set) c.cookies.set(cookieName, cookieValue, {
|
|
119
|
+
httpOnly: true,
|
|
120
|
+
secure: true,
|
|
121
|
+
sameSite: "lax",
|
|
122
|
+
path: "/",
|
|
123
|
+
...rememberMe ? { maxAge } : {}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
const resolveLocale = (path, options) => {
|
|
129
|
+
if (options?.getLocale) return options.getLocale(path);
|
|
130
|
+
if (options?.locales && options.locales.length > 0) {
|
|
131
|
+
const matched = options.locales.find((loc) => path === `/${loc}` || path.startsWith(`/${loc}/`));
|
|
132
|
+
if (matched) return matched;
|
|
133
|
+
}
|
|
134
|
+
return path.match(/^\/([a-zA-Z]{2}(?:-[a-zA-Z]{2})?)(\/|$)/)?.[1]?.toLowerCase() ?? "en";
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Universal Construction Mode Middleware for Hono and Astro.
|
|
138
|
+
*
|
|
139
|
+
* Intercepts unauthenticated traffic when CONSTRUCTION_MODE is active, handles POST requests to the
|
|
140
|
+
* guest auth endpoint automatically, and passes authenticated requests or whitelisted assets
|
|
141
|
+
* cleanly.
|
|
142
|
+
*/
|
|
143
|
+
function construction(options = {}) {
|
|
144
|
+
const apiPath = options.apiPath !== void 0 ? options.apiPath : "/api/construction-guest";
|
|
145
|
+
const constructionPath = options.constructionPath ?? "/construction";
|
|
146
|
+
return async (c, next) => {
|
|
147
|
+
const { path, method, search, redirect, json } = getContextHelpers(c);
|
|
148
|
+
if (apiPath && path === apiPath && method === "POST") try {
|
|
149
|
+
let body = {};
|
|
150
|
+
if (typeof c.req?.json === "function") body = await c.req.json().catch(() => ({}));
|
|
151
|
+
else if (c.request) body = await c.request.json().catch(() => ({}));
|
|
152
|
+
if (!await signInConstructionGuest(c, body?.passphrase ?? "", body?.rememberMe ?? true, options)) return json({ error: "Invalid credentials" }, 401);
|
|
153
|
+
return json({ success: true });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return json({ error: err?.message || "Authentication failed" }, 400);
|
|
156
|
+
}
|
|
157
|
+
const enabled = isModeEnabled(c, options);
|
|
158
|
+
const locale = resolveLocale(path, options);
|
|
159
|
+
const isConstructionPage = path === constructionPath || path === `/${locale}${constructionPath}` || path.endsWith(constructionPath);
|
|
160
|
+
const isAuthorized = async () => {
|
|
161
|
+
if (options.isAuthorized) {
|
|
162
|
+
if (await options.isAuthorized(c)) return true;
|
|
163
|
+
}
|
|
164
|
+
if (c.get?.("session") || c.get?.("user") || c.locals?.session || c.locals?.user) return true;
|
|
165
|
+
return await isConstructionGuest(c, options);
|
|
166
|
+
};
|
|
167
|
+
if (isConstructionPage) {
|
|
168
|
+
if (!enabled || await isAuthorized()) return redirect(`/${locale}`);
|
|
169
|
+
return next();
|
|
170
|
+
}
|
|
171
|
+
if (!enabled) return next();
|
|
172
|
+
if (DEFAULT_WHITELISTED_PATTERNS.some((pattern) => path.includes(pattern))) return next();
|
|
173
|
+
if (options.whitelist) {
|
|
174
|
+
if (typeof options.whitelist === "function") {
|
|
175
|
+
if (options.whitelist(path)) return next();
|
|
176
|
+
} else if (Array.isArray(options.whitelist)) {
|
|
177
|
+
if (options.whitelist.some((pattern) => typeof pattern === "string" ? path.includes(pattern) : pattern.test(path))) return next();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (await isAuthorized()) return next();
|
|
181
|
+
const redirectTo = encodeURIComponent(path + search);
|
|
182
|
+
const targetUrl = `/${locale}${constructionPath}?redirect=${redirectTo}`;
|
|
183
|
+
if (options.onUnauthorized) return options.onUnauthorized(c, {
|
|
184
|
+
locale,
|
|
185
|
+
path,
|
|
186
|
+
redirectUrl: targetUrl
|
|
187
|
+
});
|
|
188
|
+
return redirect(targetUrl);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
export { CONSTRUCTION_GUEST_COOKIE, construction, isConstructionGuest, signInConstructionGuest };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CONSTRUCTION_GUEST_COOKIE, construction, isConstructionGuest, signInConstructionGuest } from "./construction.mjs";
|
|
1
2
|
import { devOnly } from "./dev-only.mjs";
|
|
2
3
|
import { security } from "./security.mjs";
|
|
3
|
-
export { devOnly, security };
|
|
4
|
+
export { CONSTRUCTION_GUEST_COOKIE, construction, devOnly, isConstructionGuest, security, signInConstructionGuest };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CONSTRUCTION_GUEST_COOKIE, construction, isConstructionGuest, signInConstructionGuest } from "./construction.mjs";
|
|
1
2
|
import { devOnly } from "./dev-only.mjs";
|
|
2
3
|
import { security } from "./security.mjs";
|
|
3
|
-
export { devOnly, security };
|
|
4
|
+
export { CONSTRUCTION_GUEST_COOKIE, construction, devOnly, isConstructionGuest, security, signInConstructionGuest };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as SecurityOptions } from "../types-BdTXMZBc.mjs";
|
|
2
2
|
//#region src/middleware/security.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Universal Security Middleware for Hono and Web Standards (Fetch API). Injects security headers,
|
|
@@ -59,5 +59,63 @@ interface SecurityOptions {
|
|
|
59
59
|
*/
|
|
60
60
|
headers?: Record<string, string>;
|
|
61
61
|
}
|
|
62
|
+
interface ConstructionOptions {
|
|
63
|
+
/**
|
|
64
|
+
* Environment variable key for construction mode flag ("true" | "false"). Defaults to
|
|
65
|
+
* "CONSTRUCTION_MODE".
|
|
66
|
+
*/
|
|
67
|
+
modeEnvKey?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Environment variable key for the passphrase secret. Defaults to "CONSTRUCTION_PASSPHRASE".
|
|
70
|
+
*/
|
|
71
|
+
passphraseEnvKey?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Explicit passphrase override (if not reading from env).
|
|
74
|
+
*/
|
|
75
|
+
passphrase?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Cookie name used for guest authorization token. Defaults to "rimelight-construction-guest".
|
|
78
|
+
*/
|
|
79
|
+
cookieName?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Cookie max age in seconds when "remember me" is enabled. Defaults to 7 days (604,800 seconds).
|
|
82
|
+
*/
|
|
83
|
+
cookieMaxAge?: number;
|
|
84
|
+
/**
|
|
85
|
+
* List of supported locales or custom resolver.
|
|
86
|
+
*/
|
|
87
|
+
locales?: string[];
|
|
88
|
+
/**
|
|
89
|
+
* Custom locale extractor from request path.
|
|
90
|
+
*/
|
|
91
|
+
getLocale?: (path: string) => string;
|
|
92
|
+
/**
|
|
93
|
+
* Custom path for the construction page. Defaults to "/construction".
|
|
94
|
+
*/
|
|
95
|
+
constructionPath?: string;
|
|
96
|
+
/**
|
|
97
|
+
* API endpoint to automatically handle guest sign-in. Set to `false` to disable automatic API
|
|
98
|
+
* interception. Defaults to "/api/construction-guest".
|
|
99
|
+
*/
|
|
100
|
+
apiPath?: string | false;
|
|
101
|
+
/**
|
|
102
|
+
* Additional routes / prefixes to whitelist and skip gating.
|
|
103
|
+
*/
|
|
104
|
+
whitelist?: (string | RegExp)[] | ((path: string) => boolean);
|
|
105
|
+
/**
|
|
106
|
+
* Custom authorization predicate. If returns true, request is allowed through. Defaults to
|
|
107
|
+
* checking `c.get("session") || await isConstructionGuest(c, options)`.
|
|
108
|
+
*/
|
|
109
|
+
isAuthorized?: (c: any) => boolean | Promise<boolean>;
|
|
110
|
+
/**
|
|
111
|
+
* Custom handler when unauthorized. Defaults to redirecting to
|
|
112
|
+
* `/${locale}/construction?redirect=${encodeURIComponent(path)}`.
|
|
113
|
+
*/
|
|
114
|
+
onUnauthorized?: (c: any, meta: {
|
|
115
|
+
locale: string;
|
|
116
|
+
path: string;
|
|
117
|
+
redirectUrl: string;
|
|
118
|
+
}) => Response | Promise<Response>;
|
|
119
|
+
}
|
|
62
120
|
//#endregion
|
|
63
|
-
export { SecurityOptions as t };
|
|
121
|
+
export { SecurityOptions as n, ConstructionOptions as t };
|
package/dist/types.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { SecurityOptions };
|
|
1
|
+
import { n as SecurityOptions, t as ConstructionOptions } from "./types-BdTXMZBc.mjs";
|
|
2
|
+
export { ConstructionOptions, SecurityOptions };
|
package/dist/vite.d.mts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/security",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment's Security Package",
|
|
6
6
|
"homepage": "https://rimelight.com/docs",
|
|
@@ -38,14 +38,24 @@
|
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@rimelight/ui": "0.0.51"
|
|
43
|
+
},
|
|
41
44
|
"devDependencies": {
|
|
42
45
|
"@astrojs/check": "0.9.10",
|
|
43
|
-
"@rimelight/config": "0.0.
|
|
46
|
+
"@rimelight/config": "0.0.9",
|
|
44
47
|
"astro": "7.3.2",
|
|
48
|
+
"hono": "4.12.3",
|
|
45
49
|
"typescript": "6.0.3"
|
|
46
50
|
},
|
|
47
51
|
"peerDependencies": {
|
|
48
|
-
"astro": ">=7.0.0"
|
|
52
|
+
"astro": ">=7.0.0",
|
|
53
|
+
"hono": ">=4.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"hono": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
49
59
|
},
|
|
50
60
|
"engines": {
|
|
51
61
|
"node": ">=26.8.2"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
import RLACard from "@rimelight/ui/components/card/RLACard.astro"
|
|
3
|
+
import RLAFormField from "@rimelight/ui/components/form-field/RLAFormField.astro"
|
|
4
|
+
import RLAInput from "@rimelight/ui/components/input/RLAInput.astro"
|
|
5
|
+
import RLAButton from "@rimelight/ui/components/button/RLAButton.astro"
|
|
6
|
+
import RLACheckbox from "@rimelight/ui/components/checkbox/RLACheckbox.astro"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
allowAuth?: boolean
|
|
10
|
+
endpoint?: string
|
|
11
|
+
passphraseLabel?: string
|
|
12
|
+
passphrasePlaceholder?: string
|
|
13
|
+
rememberMeLabel?: string
|
|
14
|
+
signInButtonLabel?: string
|
|
15
|
+
loadingButtonLabel?: string
|
|
16
|
+
returnLaterText?: string
|
|
17
|
+
class?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
allowAuth = true,
|
|
22
|
+
endpoint = "/api/construction-guest",
|
|
23
|
+
passphraseLabel = "Passphrase",
|
|
24
|
+
passphrasePlaceholder = "Enter passphrase...",
|
|
25
|
+
rememberMeLabel = "Remember me",
|
|
26
|
+
signInButtonLabel = "Sign In",
|
|
27
|
+
loadingButtonLabel = "Signing In...",
|
|
28
|
+
returnLaterText = "Please return at a later stage.",
|
|
29
|
+
class: className = ""
|
|
30
|
+
} = Astro.props
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
allowAuth ? (
|
|
35
|
+
<RLACard class={`bg-black text-left w-full max-w-md ${className}`}>
|
|
36
|
+
<slot name="header" />
|
|
37
|
+
<form id="construction-sign-in-form" data-endpoint={endpoint} class="flex flex-col gap-4">
|
|
38
|
+
<p id="construction-error-message" class="text-red-500 text-sm hidden" role="alert" />
|
|
39
|
+
|
|
40
|
+
<div class="flex flex-col gap-4">
|
|
41
|
+
<RLAFormField
|
|
42
|
+
label={passphraseLabel}
|
|
43
|
+
required
|
|
44
|
+
ui={{
|
|
45
|
+
label: "text-white",
|
|
46
|
+
description: "text-neutral-500"
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<RLAInput
|
|
50
|
+
id="construction-passphrase-input"
|
|
51
|
+
type="password"
|
|
52
|
+
name="passphrase"
|
|
53
|
+
placeholder={passphrasePlaceholder}
|
|
54
|
+
class="w-full text-white"
|
|
55
|
+
required
|
|
56
|
+
autocomplete="current-password"
|
|
57
|
+
/>
|
|
58
|
+
</RLAFormField>
|
|
59
|
+
|
|
60
|
+
<RLACheckbox
|
|
61
|
+
id="construction-remember-me-checkbox"
|
|
62
|
+
name="rememberMe"
|
|
63
|
+
label={rememberMeLabel}
|
|
64
|
+
checked
|
|
65
|
+
ui={{
|
|
66
|
+
label: "text-white",
|
|
67
|
+
indicator: "data-[state=checked]:bg-primary-500 data-[state=checked]:text-white"
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
<RLAButton
|
|
72
|
+
id="construction-submit-button"
|
|
73
|
+
type="submit"
|
|
74
|
+
color="primary"
|
|
75
|
+
variant="solid"
|
|
76
|
+
block
|
|
77
|
+
class="text-white bg-primary-500 hover:bg-primary-600 w-full"
|
|
78
|
+
data-label={signInButtonLabel}
|
|
79
|
+
data-loading-label={loadingButtonLabel}
|
|
80
|
+
>
|
|
81
|
+
{signInButtonLabel}
|
|
82
|
+
</RLAButton>
|
|
83
|
+
</div>
|
|
84
|
+
</form>
|
|
85
|
+
<slot name="footer" />
|
|
86
|
+
</RLACard>
|
|
87
|
+
) : (
|
|
88
|
+
<p class="text-neutral-400">{returnLaterText}</p>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
<script>
|
|
93
|
+
function getRedirectParam() {
|
|
94
|
+
const params = new URLSearchParams(window.location.search)
|
|
95
|
+
return params.get("redirect") || "/"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const form = document.getElementById("construction-sign-in-form") as HTMLFormElement | null
|
|
99
|
+
const passphraseInput = document.getElementById("construction-passphrase-input") as HTMLInputElement | null
|
|
100
|
+
const rememberCheckbox = document.getElementById("construction-remember-me-checkbox") as HTMLInputElement | null
|
|
101
|
+
const errorMessage = document.getElementById("construction-error-message")
|
|
102
|
+
const submitButton = document.getElementById("construction-submit-button")
|
|
103
|
+
|
|
104
|
+
if (form) {
|
|
105
|
+
form.addEventListener("submit", async (event) => {
|
|
106
|
+
event.preventDefault()
|
|
107
|
+
if (!passphraseInput || !submitButton || !errorMessage) return
|
|
108
|
+
|
|
109
|
+
const endpoint = form.getAttribute("data-endpoint") || "/api/construction-guest"
|
|
110
|
+
const defaultLabel = submitButton.getAttribute("data-label") || "Sign In"
|
|
111
|
+
const loadingLabel = submitButton.getAttribute("data-loading-label") || "Signing In..."
|
|
112
|
+
|
|
113
|
+
// Set loading state
|
|
114
|
+
submitButton.setAttribute("disabled", "true")
|
|
115
|
+
submitButton.textContent = loadingLabel
|
|
116
|
+
errorMessage.classList.add("hidden")
|
|
117
|
+
errorMessage.textContent = ""
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(endpoint, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "Content-Type": "application/json" },
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
passphrase: passphraseInput.value,
|
|
125
|
+
rememberMe: rememberCheckbox ? rememberCheckbox.checked : true
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const data = (await response.json().catch(() => ({}))) as { error?: string; success?: boolean }
|
|
130
|
+
|
|
131
|
+
if (!response.ok || !data.success) {
|
|
132
|
+
errorMessage.textContent = data?.error || "Invalid credentials"
|
|
133
|
+
errorMessage.classList.remove("hidden")
|
|
134
|
+
submitButton.removeAttribute("disabled")
|
|
135
|
+
submitButton.textContent = defaultLabel
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
window.location.href = getRedirectParam()
|
|
140
|
+
} catch (err: any) {
|
|
141
|
+
errorMessage.textContent = err?.message || "An unexpected error occurred."
|
|
142
|
+
errorMessage.classList.remove("hidden")
|
|
143
|
+
submitButton.removeAttribute("disabled")
|
|
144
|
+
submitButton.textContent = defaultLabel
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
</script>
|