mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
c7a087eea1
- localPath resolves the path against this origin, so control characters or backslashes can't turn it into another site's address. - link, sign-out and unlink drop the cached account answer.
74 lines
3.3 KiB
TypeScript
74 lines
3.3 KiB
TypeScript
// Website sign-in through the server's OpenID Connect provider
|
|
// (ExternalSignInController). The session is an HttpOnly cookie on this host,
|
|
// so there is no token to keep here: same-origin fetches carry it.
|
|
import { get, post } from './api';
|
|
|
|
export interface AccountMe {
|
|
signInAvailable: boolean;
|
|
provider: string;
|
|
signedIn: boolean;
|
|
player: { id: string; username: string; email: string } | null;
|
|
pendingLink: { email: string } | null;
|
|
}
|
|
|
|
let cached: Promise<AccountMe> | null = null;
|
|
|
|
/** Who is signed in. Fetched once per page load; `fresh` re-reads it. */
|
|
export function getMe(fresh = false): Promise<AccountMe> {
|
|
if (!cached || fresh) cached = get<AccountMe>('/account/me').catch((e) => { cached = null; throw e; });
|
|
return cached;
|
|
}
|
|
|
|
/** `url` if it is a path on this site, else `/` (the server applies the same
|
|
* rule): never navigate to an address taken from the query string otherwise. */
|
|
export function localPath(url: string | null | undefined): string {
|
|
if (!url || !url.startsWith('/')) return '/';
|
|
// Parse it the way the browser will (it drops tabs and newlines and reads
|
|
// \ as /) and keep it only if it stays on this origin.
|
|
try {
|
|
const u = new URL(url, window.location.origin);
|
|
return u.origin === window.location.origin ? u.pathname + u.search + u.hash : '/';
|
|
} catch {
|
|
return '/';
|
|
}
|
|
}
|
|
|
|
/** Starts sign-in at the provider; comes back to `returnUrl` on this site. */
|
|
export function signInUrl(returnUrl = window.location.pathname + window.location.search): string {
|
|
return `/api/auth/external/login?returnUrl=${encodeURIComponent(returnUrl)}`;
|
|
}
|
|
|
|
// Each of these changes who is signed in, so the cached answer is dropped.
|
|
async function changesAccount<T>(request: Promise<T>): Promise<T> {
|
|
const result = await request;
|
|
cached = null;
|
|
return result;
|
|
}
|
|
|
|
export const link = (username: string, password: string) =>
|
|
changesAccount(post<{ linked: boolean; username: string }>('/account/link', { username, password }));
|
|
export const signOut = () => changesAccount(post('/account/logout', {}));
|
|
export const unlink = () => changesAccount(post('/account/unlink', {}));
|
|
|
|
// Server error code -> friendly copy.
|
|
export const LINK_ERRORS: Record<string, string> = {
|
|
invalid_credentials: "That username and password don't match a DorkNet account.",
|
|
missing_credentials: 'Enter your DorkNet username and password.',
|
|
too_many_attempts: 'Too many wrong attempts. Wait 15 minutes and try again.',
|
|
banned: 'That account is banned.',
|
|
identity_linked_to_another_account: 'This sign-in is already linked to a different DorkNet account.',
|
|
no_pending_identity: 'Your sign-in expired. Start again with "Sign in".',
|
|
};
|
|
|
|
/** Friendly copy for a known error code, else `fallback`. For codes read from
|
|
* the query string, pass a fixed fallback: anyone can put text in a link. */
|
|
export function errorCopy(map: Record<string, string>, code: string | null | undefined, fallback: string): string {
|
|
return code && Object.hasOwn(map, code) ? map[code] : fallback;
|
|
}
|
|
|
|
export const SIGNIN_ERRORS: Record<string, string> = {
|
|
account_unavailable: 'The account linked to this sign-in is unavailable (deleted or banned).',
|
|
no_identity: "The sign-in provider didn't return an identity. Try again.",
|
|
provider_failed: 'The sign-in was cancelled or failed at the provider. Try again.',
|
|
};
|