Style wiki auth screens with guild design system
Add guild-styled HTML templates for OIDC logout confirmation, post-logout success, and error pages. Update wiki login heading to brand convention (candlelight + warm-text). Restyle magic link email from blue to guild colour tokens.
This commit is contained in:
parent
79d3ba0f78
commit
bf57f4b33d
3 changed files with 175 additions and 18 deletions
|
|
@ -31,12 +31,12 @@ async function sendMagicLink() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 px-4">
|
<div class="min-h-screen flex items-center justify-center bg-guild-900 px-4">
|
||||||
<div class="w-full max-w-sm">
|
<div class="w-full max-w-sm">
|
||||||
<div class="bg-white rounded-lg shadow-md p-8">
|
<div class="bg-guild-800 rounded-lg shadow-md p-8">
|
||||||
<div class="text-center mb-6">
|
<div class="text-center mb-6">
|
||||||
<h1 class="text-2xl font-bold text-gray-900">Ghost Guild Wiki</h1>
|
<h1 class="text-display-sm font-bold text-candlelight-400 warm-text">Ghost Guild Wiki</h1>
|
||||||
<p class="mt-2 text-sm text-gray-600">
|
<p class="mt-2 text-sm text-guild-400">
|
||||||
Sign in with your Ghost Guild account
|
Sign in with your Ghost Guild account
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -44,7 +44,7 @@ async function sendMagicLink() {
|
||||||
<template v-if="!sent">
|
<template v-if="!sent">
|
||||||
<form @submit.prevent="sendMagicLink" class="space-y-4">
|
<form @submit.prevent="sendMagicLink" class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">
|
<label for="email" class="block text-sm font-medium text-guild-300 mb-1">
|
||||||
Email address
|
Email address
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -53,23 +53,23 @@ async function sendMagicLink() {
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
class="w-full px-3 py-2 border border-guild-600 rounded-md shadow-sm bg-guild-900 text-guild-100 focus:ring-2 focus:ring-candlelight-500 focus:border-candlelight-500 text-sm"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
|
<p v-if="error" class="text-sm text-ember-400">{{ error }}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="loading || !email"
|
:disabled="loading || !email"
|
||||||
class="w-full py-2 px-4 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
class="w-full py-2 px-4 bg-candlelight-600 text-guild-100 text-sm font-medium rounded-md hover:bg-candlelight-500 focus:outline-none focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{{ loading ? "Sending..." : "Send magic link" }}
|
{{ loading ? "Sending..." : "Send magic link" }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="mt-4 text-xs text-center text-gray-500">
|
<p class="mt-4 text-xs text-center text-guild-500">
|
||||||
We'll send a sign-in link to your email.
|
We'll send a sign-in link to your email.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -77,14 +77,14 @@ async function sendMagicLink() {
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="text-center space-y-3">
|
<div class="text-center space-y-3">
|
||||||
<div class="text-4xl">✉️</div>
|
<div class="text-4xl">✉️</div>
|
||||||
<h2 class="text-lg font-semibold text-gray-900">Check your email</h2>
|
<h2 class="text-lg font-semibold text-guild-100">Check your email</h2>
|
||||||
<p class="text-sm text-gray-600">
|
<p class="text-sm text-guild-400">
|
||||||
We sent a sign-in link to <strong>{{ email }}</strong>.
|
We sent a sign-in link to <strong>{{ email }}</strong>.
|
||||||
Click the link in the email to continue.
|
Click the link in the email to continue.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
@click="sent = false; email = '';"
|
@click="sent = false; email = '';"
|
||||||
class="mt-4 text-sm text-blue-600 hover:text-blue-800"
|
class="mt-4 text-sm text-candlelight-600 hover:text-candlelight-500"
|
||||||
>
|
>
|
||||||
Use a different email
|
Use a different email
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -54,16 +54,16 @@ export default defineEventHandler(async (event) => {
|
||||||
to: email,
|
to: email,
|
||||||
subject: "Sign in to Ghost Guild Wiki",
|
subject: "Sign in to Ghost Guild Wiki",
|
||||||
html: `
|
html: `
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
<div style="font-family: Georgia, 'Times New Roman', serif; max-width: 600px; margin: 0 auto; background-color: #2a241c; border-radius: 8px; padding: 32px;">
|
||||||
<h2 style="color: #2563eb;">Sign in to the Ghost Guild Wiki</h2>
|
<h2 style="color: #d09e4e; margin-top: 0;">Sign in to the Ghost Guild Wiki</h2>
|
||||||
<p>Click the button below to sign in:</p>
|
<p style="color: #bfb3a2;">Click the button below to sign in:</p>
|
||||||
<div style="text-align: center; margin: 30px 0;">
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
<a href="${baseUrl}/oidc/interaction/verify?token=${token}"
|
<a href="${baseUrl}/oidc/interaction/verify?token=${token}"
|
||||||
style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
|
style="background-color: #9a6f2c; color: #f0ebe4; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: 600;">
|
||||||
Sign In
|
Sign In
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p style="color: #666; font-size: 14px;">
|
<p style="color: #6b5f4d; font-size: 14px;">
|
||||||
This link expires in 15 minutes. If you didn't request this, you can safely ignore this email.
|
This link expires in 15 minutes. If you didn't request this, you can safely ignore this email.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,126 @@ import { MongoAdapter } from "./oidc-mongodb-adapter.js";
|
||||||
import Member from "../models/member.js";
|
import Member from "../models/member.js";
|
||||||
import { connectDB } from "./mongoose.js";
|
import { connectDB } from "./mongoose.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a standalone HTML page in the guild dark style.
|
||||||
|
* Used for OIDC logout/error screens that are served outside Nuxt.
|
||||||
|
*/
|
||||||
|
function guildPageShell(title: string, bodyContent: string, extraStyles = "") {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>${title} — Ghost Guild</title>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Quietism';
|
||||||
|
src: url('/fonts/Quietism-Regular.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Quietism';
|
||||||
|
src: url('/fonts/Quietism-Bold.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
background-color: #1a1510;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse at 20% 50%, rgba(154, 111, 44, 0.06) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse at 80% 50%, rgba(154, 111, 44, 0.04) 0%, transparent 60%);
|
||||||
|
color: #bfb3a2;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background-color: #2a241c;
|
||||||
|
border: 1px solid rgba(154, 111, 44, 0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 0 30px rgba(208, 158, 78, 0.06);
|
||||||
|
padding: 2.5rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-family: 'Quietism', Georgia, 'Times New Roman', serif;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #d09e4e;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
p { line-height: 1.6; margin-bottom: 1rem; }
|
||||||
|
.subtext { font-size: 0.875rem; color: #6b5f4d; }
|
||||||
|
.btn-primary {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #9a6f2c;
|
||||||
|
color: #f0ebe4;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { background-color: #b8862f; }
|
||||||
|
.btn-secondary {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #bfb3a2;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(154, 111, 44, 0.3);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover { border-color: rgba(154, 111, 44, 0.5); }
|
||||||
|
.actions { display: flex; gap: 0.75rem; justify-content: center; margin-top: 1.5rem; }
|
||||||
|
.brand {
|
||||||
|
margin-top: 2rem;
|
||||||
|
font-family: 'Quietism', Georgia, 'Times New Roman', serif;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-variant: small-caps;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: #6b5f4d;
|
||||||
|
}
|
||||||
|
.error-detail {
|
||||||
|
margin-top: 1rem;
|
||||||
|
background-color: #1a1510;
|
||||||
|
border: 1px solid rgba(154, 111, 44, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
font-family: 'Ubuntu Mono', 'Courier New', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b5f4d;
|
||||||
|
text-align: left;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
${extraStyles}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
${bodyContent}
|
||||||
|
<div class="brand">Ghost Guild</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
let _provider: InstanceType<typeof Provider> | null = null;
|
let _provider: InstanceType<typeof Provider> | null = null;
|
||||||
|
|
||||||
export async function getOidcProvider() {
|
export async function getOidcProvider() {
|
||||||
|
|
@ -90,7 +210,30 @@ export async function getOidcProvider() {
|
||||||
enabled: process.env.NODE_ENV !== "production",
|
enabled: process.env.NODE_ENV !== "production",
|
||||||
},
|
},
|
||||||
revocation: { enabled: true },
|
revocation: { enabled: true },
|
||||||
rpInitiatedLogout: { enabled: true },
|
rpInitiatedLogout: {
|
||||||
|
enabled: true,
|
||||||
|
logoutSource: async (ctx: any, form: string) => {
|
||||||
|
ctx.body = guildPageShell("Sign Out", `
|
||||||
|
<h1>Sign Out</h1>
|
||||||
|
<p>Do you want to sign out of your Ghost Guild session?</p>
|
||||||
|
<p class="subtext">This will sign you out of the wiki and any other connected services.</p>
|
||||||
|
${form}
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn-primary" form="op.logoutForm" type="submit" value="yes" name="logout">Yes, sign me out</button>
|
||||||
|
<a class="btn-secondary" href="https://wiki.ghostguild.org">Stay signed in</a>
|
||||||
|
</div>
|
||||||
|
`, "form#op\\.logoutForm { display: none; }");
|
||||||
|
},
|
||||||
|
postLogoutSuccessSource: async (ctx: any) => {
|
||||||
|
ctx.body = guildPageShell("Signed Out", `
|
||||||
|
<h1>Signed Out</h1>
|
||||||
|
<p>You have been successfully signed out.</p>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="btn-primary" href="https://wiki.ghostguild.org">Return to Wiki</a>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mount all OIDC endpoints under /oidc prefix
|
// Mount all OIDC endpoints under /oidc prefix
|
||||||
|
|
@ -115,6 +258,20 @@ export async function getOidcProvider() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderError: async (ctx: any, out: Record<string, string>, _error: Error) => {
|
||||||
|
const details = Object.entries(out)
|
||||||
|
.map(([key, value]) => `<strong>${key}:</strong> ${value}`)
|
||||||
|
.join("<br>");
|
||||||
|
ctx.body = guildPageShell("Something Went Wrong", `
|
||||||
|
<h1>Something Went Wrong</h1>
|
||||||
|
<p>An error occurred during authentication. Please try again.</p>
|
||||||
|
<div class="error-detail">${details}</div>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="btn-primary" href="https://wiki.ghostguild.org">Return to Wiki</a>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
// Allow Outline to use PKCE but don't require it
|
// Allow Outline to use PKCE but don't require it
|
||||||
pkce: {
|
pkce: {
|
||||||
required: () => false,
|
required: () => false,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue