mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-24 19:42:24 +03:00
refactor(auth): describe sign-in screens as data, draw them in one place
Providers no longer return a `Response`. Each one says what it needs from the person — an address, a pin, a yes-or-no — as a `Screen`, and a single `Renderer` decides how that is drawn. The old arrangement made every provider a small web framework. It had to know about markup, about the stylesheet's attribute names, about how a page is assembled, so each grew its own callback signature and its own copy of `new Response(jsx.toString())`. Three consequences, all of them visible in the tree before this change: - The device flow never got a design at all. Its two pages were built by concatenating HTML strings, with an inline `style` on the user code, and six of its replies were `text/plain` — unstyled black-on-white in the middle of signing in, which is also what a person got when their sign-in cookie expired. - The password screens were drifting. They were written against attribute names the stylesheet no longer had, and nobody noticed because password sign-in is not switched on. They are deleted here rather than repaired; the flow is now six screen descriptions and no markup. - A provider could not be named or marked without editing the library. The brand marks and display names were two hardcoded records inside the code that drew the chooser, so anything missing from them rendered as its own lowercase identifier with no icon. Providers now declare `display` themselves, and the chooser is built from what they say. Also removes the theme global. It was `globalThis`, with a comment conceding as much, which made every component depend on something invisible at the call site — untestable in isolation, and shared mutable state on a runtime that keeps one module instance across requests. The theme is now a closure argument, and the same change shrinks `Theme` to the handful of values a deployment sets that the stylesheet cannot. Adding a screen now touches no CSS, and swapping the presentation layer means implementing one method. The code flow's tests demonstrate the second: they render screens as JSON. Behaviour is unchanged. Status codes, cookies and the confirmation step are the same, which the device tests cover unmodified.
This commit is contained in:
@@ -51,6 +51,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_APPLE } from '../ui/mark.js';
|
||||
import { Oauth2Provider, Oauth2WrappedConfig } from './oauth2.js';
|
||||
import { OidcProvider, OidcWrappedConfig } from './oidc.js';
|
||||
|
||||
@@ -96,6 +97,7 @@ export function AppleProvider(config: AppleConfig) {
|
||||
return Oauth2Provider({
|
||||
...restConfig,
|
||||
type: 'apple' as const,
|
||||
display: { name: 'Apple', icon: MARK_APPLE },
|
||||
endpoint: {
|
||||
authorization: 'https://appleid.apple.com/auth/authorize',
|
||||
token: 'https://appleid.apple.com/auth/token',
|
||||
@@ -122,6 +124,7 @@ export function AppleOidcProvider(config: AppleOidcConfig) {
|
||||
return OidcProvider({
|
||||
...config,
|
||||
type: 'apple' as const,
|
||||
display: { name: 'Apple', icon: MARK_APPLE },
|
||||
issuer: 'https://appleid.apple.com'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ import { Context } from 'hono';
|
||||
|
||||
import { generateUnbiasedDigits, generateUnbiasedString, timingSafeCompare } from '../random.js';
|
||||
import { Storage } from '../storage/storage.js';
|
||||
import { MARK_CODE } from '../ui/mark.js';
|
||||
import type { Screen } from '../ui/screen.js';
|
||||
import { Provider } from './provider.js';
|
||||
|
||||
export interface CodeProviderConfig<
|
||||
@@ -120,23 +122,18 @@ export interface CodeProviderConfig<
|
||||
*/
|
||||
resendInterval?: number;
|
||||
/**
|
||||
* The request handler to generate the UI for the code flow.
|
||||
* What to ask for at each step of the flow.
|
||||
*
|
||||
* Takes the standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* and optionally [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
|
||||
* ojects.
|
||||
*
|
||||
* Also passes in the current `state` of the flow and any `error` that occurred.
|
||||
*
|
||||
* Expects the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object
|
||||
* in return.
|
||||
* Returns a {@link Screen} describing the question, not a rendered page.
|
||||
* Which one is drawn follows from `state`, and `error` says what to say
|
||||
* above it.
|
||||
*/
|
||||
request: (
|
||||
req: Request,
|
||||
state: CodeProviderState,
|
||||
form?: FormData,
|
||||
error?: CodeProviderError
|
||||
) => Promise<Response>;
|
||||
) => Promise<Screen>;
|
||||
/**
|
||||
* Callback to send the pin code to the user.
|
||||
*
|
||||
@@ -242,6 +239,7 @@ export function CodeProvider<Claims extends Record<string, string> = Record<stri
|
||||
|
||||
return {
|
||||
type: 'code',
|
||||
display: { name: 'Email', icon: MARK_CODE },
|
||||
init(routes, ctx) {
|
||||
async function transition(
|
||||
c: Context,
|
||||
@@ -253,8 +251,7 @@ export function CodeProvider<Claims extends Record<string, string> = Record<stri
|
||||
// Twenty-four hours, which is what this was, made a six-digit
|
||||
// pin usable for a day.
|
||||
await ctx.set<CodeProviderState>(c, 'provider', ttl, next);
|
||||
const resp = ctx.forward(c, await config.request(c.req.raw, next, fd, err));
|
||||
return resp;
|
||||
return ctx.screen(c, await config.request(c.req.raw, next, fd, err));
|
||||
}
|
||||
|
||||
routes.get('/authorize', async (c) => {
|
||||
@@ -383,7 +380,7 @@ export function CodeProvider<Claims extends Record<string, string> = Record<stri
|
||||
return transition(c, { type: 'start' }, fd);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,7 @@ export function CognitoProvider(config: CognitoConfig) {
|
||||
|
||||
return Oauth2Provider({
|
||||
type: 'cognito',
|
||||
display: { name: 'Cognito' },
|
||||
...config,
|
||||
endpoint: {
|
||||
authorization: `https://${domain}/oauth2/authorize`,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_DISCORD } from '../ui/mark.js';
|
||||
import { Oauth2Provider, Oauth2WrappedConfig } from './oauth2.js';
|
||||
|
||||
export interface DiscordConfig extends Oauth2WrappedConfig {}
|
||||
@@ -36,6 +37,7 @@ export interface DiscordConfig extends Oauth2WrappedConfig {}
|
||||
export function DiscordProvider(config: DiscordConfig) {
|
||||
return Oauth2Provider({
|
||||
type: 'discord',
|
||||
display: { name: 'Discord', icon: MARK_DISCORD },
|
||||
...config,
|
||||
endpoint: {
|
||||
authorization: 'https://discord.com/oauth2/authorize',
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_FACEBOOK } from '../ui/mark.js';
|
||||
import { Oauth2Provider, Oauth2WrappedConfig } from './oauth2.js';
|
||||
import { OidcProvider, OidcWrappedConfig } from './oidc.js';
|
||||
|
||||
@@ -55,6 +56,7 @@ export function FacebookProvider(config: FacebookConfig) {
|
||||
return Oauth2Provider({
|
||||
...config,
|
||||
type: 'facebook',
|
||||
display: { name: 'Facebook', icon: MARK_FACEBOOK },
|
||||
endpoint: {
|
||||
authorization: 'https://www.facebook.com/v12.0/dialog/oauth',
|
||||
token: 'https://graph.facebook.com/v12.0/oauth/access_token'
|
||||
@@ -79,6 +81,7 @@ export function FacebookOidcProvider(config: FacebookOidcConfig) {
|
||||
return OidcProvider({
|
||||
...config,
|
||||
type: 'facebook',
|
||||
display: { name: 'Facebook', icon: MARK_FACEBOOK },
|
||||
issuer: 'https://graph.facebook.com'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_GITHUB } from '../ui/mark.js';
|
||||
import { Oauth2Provider, Oauth2WrappedConfig } from './oauth2.js';
|
||||
|
||||
export interface GithubConfig extends Oauth2WrappedConfig {}
|
||||
@@ -37,6 +38,7 @@ export function GithubProvider(config: GithubConfig) {
|
||||
return Oauth2Provider({
|
||||
...config,
|
||||
type: 'github',
|
||||
display: { name: 'GitHub', icon: MARK_GITHUB },
|
||||
endpoint: {
|
||||
authorization: 'https://github.com/login/oauth/authorize',
|
||||
token: 'https://github.com/login/oauth/access_token'
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_GOOGLE } from '../ui/mark.js';
|
||||
import { Oauth2Provider, Oauth2WrappedConfig } from './oauth2.js';
|
||||
import { OidcProvider, OidcWrappedConfig } from './oidc.js';
|
||||
|
||||
@@ -55,6 +56,7 @@ export function GoogleProvider(config: GoogleConfig) {
|
||||
return Oauth2Provider({
|
||||
...config,
|
||||
type: 'google',
|
||||
display: { name: 'Google', icon: MARK_GOOGLE },
|
||||
endpoint: {
|
||||
authorization: 'https://accounts.google.com/o/oauth2/v2/auth',
|
||||
token: 'https://oauth2.googleapis.com/token',
|
||||
@@ -80,6 +82,7 @@ export function GoogleOidcProvider(config: GoogleOidcConfig) {
|
||||
return OidcProvider({
|
||||
...config,
|
||||
type: 'google',
|
||||
display: { name: 'Google', icon: MARK_GOOGLE },
|
||||
issuer: 'https://accounts.google.com'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface JumpCloudConfig extends Oauth2WrappedConfig {}
|
||||
export function JumpCloudProvider(config: JumpCloudConfig) {
|
||||
return Oauth2Provider({
|
||||
type: 'jumpcloud',
|
||||
display: { name: 'JumpCloud' },
|
||||
...config,
|
||||
endpoint: {
|
||||
authorization: 'https://oauth.id.jumpcloud.com/oauth2/auth',
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { MARK_LINKEDIN } from '../ui/mark.js';
|
||||
import { Oauth2Provider, type Oauth2WrappedConfig } from './oauth2.js';
|
||||
|
||||
export function LinkedInAdapter(config: Oauth2WrappedConfig) {
|
||||
return Oauth2Provider({
|
||||
...config,
|
||||
type: 'linkedin',
|
||||
display: { name: 'LinkedIn', icon: MARK_LINKEDIN },
|
||||
endpoint: {
|
||||
authorization: 'https://www.linkedin.com/oauth/v2/authorization',
|
||||
token: 'https://www.linkedin.com/oauth/v2/accessToken'
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_MICROSOFT } from '../ui/mark.js';
|
||||
import { Oauth2Provider, Oauth2WrappedConfig } from './oauth2.js';
|
||||
import { OidcProvider, OidcWrappedConfig } from './oidc.js';
|
||||
|
||||
@@ -71,6 +72,7 @@ export function MicrosoftProvider(config: MicrosoftConfig) {
|
||||
return Oauth2Provider({
|
||||
...config,
|
||||
type: 'microsoft',
|
||||
display: { name: 'Microsoft', icon: MARK_MICROSOFT },
|
||||
endpoint: {
|
||||
authorization: `https://login.microsoftonline.com/${config?.tenant}/oauth2/v2.0/authorize`,
|
||||
token: `https://login.microsoftonline.com/${config?.tenant}/oauth2/v2.0/token`
|
||||
@@ -95,6 +97,7 @@ export function MicrosoftOidcProvider(config: MicrosoftOidcConfig) {
|
||||
return OidcProvider({
|
||||
...config,
|
||||
type: 'microsoft',
|
||||
display: { name: 'Microsoft', icon: MARK_MICROSOFT },
|
||||
issuer: 'https://graph.microsoft.com/oidc/userinfo'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,9 +27,16 @@ import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
import { OauthError } from '../error.js';
|
||||
import { generatePKCE } from '../pkce.js';
|
||||
import { getRelativeUrl } from '../util.js';
|
||||
import { Provider } from './provider.js';
|
||||
import { Provider, type ProviderDisplay } from './provider.js';
|
||||
|
||||
export interface Oauth2Config {
|
||||
/**
|
||||
* How this provider is named and marked on the chooser.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
display?: ProviderDisplay;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -216,6 +223,7 @@ export function Oauth2Provider(
|
||||
|
||||
return {
|
||||
type: config.type || 'oauth2',
|
||||
display: config.display,
|
||||
init(routes, ctx) {
|
||||
routes.get('/authorize', async (c) => {
|
||||
const state = crypto.randomUUID();
|
||||
|
||||
@@ -24,9 +24,16 @@ import { createLocalJWKSet, JSONWebKeySet, jwtVerify } from 'jose';
|
||||
import { WellKnown } from '../client.js';
|
||||
import { OauthError } from '../error.js';
|
||||
import { getRelativeUrl, lazy } from '../util.js';
|
||||
import { Provider } from './provider.js';
|
||||
import { Provider, type ProviderDisplay } from './provider.js';
|
||||
|
||||
export interface OidcConfig {
|
||||
/**
|
||||
* How this provider is named and marked on the chooser.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
display?: ProviderDisplay;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -123,6 +130,7 @@ export function OidcProvider(config: OidcConfig): Provider<{ id: JWTPayload; cli
|
||||
|
||||
return {
|
||||
type: config.type || 'oidc',
|
||||
display: config.display,
|
||||
init(routes, ctx) {
|
||||
routes.get('/authorize', async (c) => {
|
||||
const provider: ProviderState = {
|
||||
|
||||
@@ -5,8 +5,8 @@ import { v1 } from '@standard-schema/spec';
|
||||
* paired with the `PasswordUI`.
|
||||
*
|
||||
* ```ts
|
||||
* import { PasswordUI } from "@openauthjs/openauth/ui/password"
|
||||
* import { PasswordProvider } from "@openauthjs/openauth/provider/password"
|
||||
* import { PasswordUI } from "@nestri/auth/ui/password"
|
||||
* import { PasswordProvider } from "@nestri/auth/provider/password"
|
||||
*
|
||||
* export default issuer({
|
||||
* providers: {
|
||||
@@ -23,25 +23,26 @@ import { v1 } from '@standard-schema/spec';
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Behind the scenes, the `PasswordProvider` expects callbacks that implements request handlers
|
||||
* that generate the UI for the following.
|
||||
* Behind the scenes, the `PasswordProvider` asks its config what to put on
|
||||
* each screen. Each callback returns a `Screen` — a description of what is
|
||||
* being asked for — and the issuer's renderer decides how it is drawn.
|
||||
*
|
||||
* ```ts
|
||||
* PasswordProvider({
|
||||
* // ...
|
||||
* login: (req, form, error) => Promise<Response>
|
||||
* register: (req, state, form, error) => Promise<Response>
|
||||
* change: (req, state, form, error) => Promise<Response>
|
||||
* login: (req, form, error) => Promise<Screen>
|
||||
* register: (req, state, form, error) => Promise<Screen>
|
||||
* change: (req, state, form, error) => Promise<Screen>
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* This allows you to create your own UI for each of these screens.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
import { UnknownStateError } from '../error.js';
|
||||
import { generateUnbiasedDigits, timingSafeCompare } from '../random.js';
|
||||
import { Storage } from '../storage/storage.js';
|
||||
import { MARK_PASSWORD } from '../ui/mark.js';
|
||||
import type { Screen } from '../ui/screen.js';
|
||||
import { Provider } from './provider.js';
|
||||
|
||||
/**
|
||||
@@ -64,52 +65,37 @@ export interface PasswordConfig {
|
||||
/**
|
||||
* The request handler to generate the UI for the login screen.
|
||||
*
|
||||
* Takes the standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* and optionally [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
|
||||
* ojects.
|
||||
*
|
||||
* In case of an error, this is called again with the `error`.
|
||||
*
|
||||
* Expects the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object
|
||||
* in return.
|
||||
* Returns a `Screen` describing what to ask for, not a rendered page.
|
||||
*/
|
||||
login: (req: Request, form?: FormData, error?: PasswordLoginError) => Promise<Response>;
|
||||
login: (req: Request, form?: FormData, error?: PasswordLoginError) => Promise<Screen>;
|
||||
/**
|
||||
* The request handler to generate the UI for the register screen.
|
||||
*
|
||||
* Takes the standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* and optionally [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
|
||||
* ojects.
|
||||
*
|
||||
* Also passes in the current `state` of the flow and any `error` that occurred.
|
||||
*
|
||||
* Expects the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object
|
||||
* in return.
|
||||
* Returns a `Screen` describing what to ask for, not a rendered page.
|
||||
*/
|
||||
register: (
|
||||
req: Request,
|
||||
state: PasswordRegisterState,
|
||||
form?: FormData,
|
||||
error?: PasswordRegisterError
|
||||
) => Promise<Response>;
|
||||
) => Promise<Screen>;
|
||||
/**
|
||||
* The request handler to generate the UI for the change password screen.
|
||||
*
|
||||
* Takes the standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* and optionally [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
|
||||
* ojects.
|
||||
*
|
||||
* Also passes in the current `state` of the flow and any `error` that occurred.
|
||||
*
|
||||
* Expects the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object
|
||||
* in return.
|
||||
* Returns a `Screen` describing what to ask for, not a rendered page.
|
||||
*/
|
||||
change: (
|
||||
req: Request,
|
||||
state: PasswordChangeState,
|
||||
form?: FormData,
|
||||
error?: PasswordChangeError
|
||||
) => Promise<Response>;
|
||||
) => Promise<Screen>;
|
||||
/**
|
||||
* Callback to send the confirmation pin code to the user.
|
||||
*
|
||||
@@ -268,13 +254,14 @@ export function PasswordProvider(config: PasswordConfig): Provider<{ email: stri
|
||||
}
|
||||
return {
|
||||
type: 'password',
|
||||
display: { name: 'Password', icon: MARK_PASSWORD },
|
||||
init(routes, ctx) {
|
||||
routes.get('/authorize', async (c) => ctx.forward(c, await config.login(c.req.raw)));
|
||||
routes.get('/authorize', async (c) => ctx.screen(c, await config.login(c.req.raw)));
|
||||
|
||||
routes.post('/authorize', async (c) => {
|
||||
const fd = await c.req.formData();
|
||||
async function error(err: PasswordLoginError) {
|
||||
return ctx.forward(c, await config.login(c.req.raw, fd, err));
|
||||
return ctx.screen(c, await config.login(c.req.raw, fd, err));
|
||||
}
|
||||
const email = fd.get('email')?.toString()?.toLowerCase();
|
||||
if (!email) return error({ type: 'invalid_email' });
|
||||
@@ -300,7 +287,7 @@ export function PasswordProvider(config: PasswordConfig): Provider<{ email: stri
|
||||
type: 'start'
|
||||
};
|
||||
await ctx.set(c, 'provider', 60 * 60 * 24, state);
|
||||
return ctx.forward(c, await config.register(c.req.raw, state));
|
||||
return ctx.screen(c, await config.register(c.req.raw, state));
|
||||
});
|
||||
|
||||
routes.post('/register', async (c) => {
|
||||
@@ -311,7 +298,7 @@ export function PasswordProvider(config: PasswordConfig): Provider<{ email: stri
|
||||
|
||||
async function transition(next: PasswordRegisterState, err?: PasswordRegisterError) {
|
||||
await ctx.set<PasswordRegisterState>(c, 'provider', 60 * 60 * 24, next);
|
||||
return ctx.forward(c, await config.register(c.req.raw, next, fd, err));
|
||||
return ctx.screen(c, await config.register(c.req.raw, next, fd, err));
|
||||
}
|
||||
|
||||
if (action === 'register' && provider.type === 'start') {
|
||||
@@ -386,7 +373,7 @@ export function PasswordProvider(config: PasswordConfig): Provider<{ email: stri
|
||||
redirect
|
||||
};
|
||||
await ctx.set(c, 'provider', 60 * 60 * 24, state);
|
||||
return ctx.forward(c, await config.change(c.req.raw, state));
|
||||
return ctx.screen(c, await config.change(c.req.raw, state));
|
||||
});
|
||||
|
||||
routes.post('/change', async (c) => {
|
||||
@@ -397,7 +384,7 @@ export function PasswordProvider(config: PasswordConfig): Provider<{ email: stri
|
||||
|
||||
async function transition(next: PasswordChangeState, err?: PasswordChangeError) {
|
||||
await ctx.set<PasswordChangeState>(c, 'provider', 60 * 60 * 24, next);
|
||||
return ctx.forward(c, await config.change(c.req.raw, next, fd, err));
|
||||
return ctx.screen(c, await config.change(c.req.raw, next, fd, err));
|
||||
}
|
||||
|
||||
if (action === 'code') {
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
import type { Context, Hono } from 'hono';
|
||||
|
||||
import { StorageAdapter } from '../storage/storage.js';
|
||||
import type { Mark, Screen } from '../ui/screen.js';
|
||||
|
||||
export type ProviderRoute = Hono;
|
||||
|
||||
/**
|
||||
* How a provider is offered to a person choosing one.
|
||||
*
|
||||
* Declared by the provider rather than looked up by whatever draws the
|
||||
* chooser. That used to be two hardcoded records inside the rendering code, so
|
||||
* a provider the library had not been told about rendered as its own lowercase
|
||||
* identifier with no mark beside it — and there was no way to fix it from
|
||||
* outside the library.
|
||||
*/
|
||||
export interface ProviderDisplay {
|
||||
/** The name as a person reads it: `GitHub`, not `github`. */
|
||||
name: string;
|
||||
/** Raw SVG for the brand mark, from `ui/mark.ts`. */
|
||||
icon?: Mark;
|
||||
}
|
||||
|
||||
export interface Provider<Properties = any> {
|
||||
type: string;
|
||||
/**
|
||||
* What to call this provider, and what to draw beside it.
|
||||
*
|
||||
* Optional because a provider nobody picks from a list — one reached
|
||||
* directly, or one with no browser in the flow at all — has nothing to
|
||||
* display. Falling back to `type` is correct there and only there.
|
||||
*/
|
||||
display?: ProviderDisplay;
|
||||
init: (route: ProviderRoute, options: ProviderOptions<Properties>) => void;
|
||||
client?: (input: {
|
||||
clientID: string;
|
||||
@@ -24,6 +49,15 @@ export interface ProviderOptions<Properties> {
|
||||
}
|
||||
) => Promise<Response>;
|
||||
forward: (ctx: Context, response: Response) => Response;
|
||||
/**
|
||||
* Draw a screen and return it as this request's response.
|
||||
*
|
||||
* The only way a provider produces a page. It cannot reach the renderer
|
||||
* itself, which is the point: a provider says what it needs to ask and
|
||||
* never how it looks, so there is exactly one place that has to agree with
|
||||
* the stylesheet.
|
||||
*/
|
||||
screen: (ctx: Context, screen: Screen) => Response;
|
||||
set: <T>(ctx: Context, key: string, maxAge: number, value: T) => Promise<void>;
|
||||
get: <T>(ctx: Context, key: string) => Promise<T>;
|
||||
unset: (ctx: Context, key: string) => Promise<void>;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_SLACK } from '../ui/mark.js';
|
||||
import { Oauth2Provider, Oauth2WrappedConfig } from './oauth2.js';
|
||||
|
||||
export interface SlackConfig extends Oauth2WrappedConfig {
|
||||
@@ -59,6 +60,7 @@ export function SlackProvider(config: SlackConfig) {
|
||||
return Oauth2Provider({
|
||||
...config,
|
||||
type: 'slack',
|
||||
display: { name: 'Slack', icon: MARK_SLACK },
|
||||
endpoint: {
|
||||
authorization: 'https://slack.com/openid/connect/authorize',
|
||||
token: 'https://slack.com/api/openid.connect.token'
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_SPOTIFY } from '../ui/mark.js';
|
||||
import { Oauth2Provider, type Oauth2WrappedConfig } from './oauth2.js';
|
||||
|
||||
export interface SpotifyConfig extends Oauth2WrappedConfig {}
|
||||
@@ -37,6 +38,7 @@ export function SpotifyProvider(config: SpotifyConfig) {
|
||||
return Oauth2Provider({
|
||||
...config,
|
||||
type: 'spotify',
|
||||
display: { name: 'Spotify', icon: MARK_SPOTIFY },
|
||||
endpoint: {
|
||||
authorization: 'https://accounts.spotify.com/authorize',
|
||||
token: 'https://accounts.spotify.com/api/token'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MARK_STEAM } from '../ui/mark.js';
|
||||
import { getRelativeUrl } from '../util.js';
|
||||
import { Provider } from './provider.js';
|
||||
|
||||
@@ -6,6 +7,7 @@ const STEAM_OPENID_URL = 'https://steamcommunity.com/openid/login';
|
||||
export function SteamProvider(): Provider<{ steamid: string }> {
|
||||
return {
|
||||
type: 'steam',
|
||||
display: { name: 'Steam', icon: MARK_STEAM },
|
||||
init(routes, ctx) {
|
||||
routes.get('/authorize', async (c) => {
|
||||
const returnUrl = getRelativeUrl(c, './callback');
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_TWITCH } from '../ui/mark.js';
|
||||
import { Oauth2Provider, Oauth2WrappedConfig } from './oauth2.js';
|
||||
|
||||
export interface TwitchConfig extends Oauth2WrappedConfig {}
|
||||
@@ -36,6 +37,7 @@ export interface TwitchConfig extends Oauth2WrappedConfig {}
|
||||
export function TwitchProvider(config: TwitchConfig) {
|
||||
return Oauth2Provider({
|
||||
type: 'twitch',
|
||||
display: { name: 'Twitch', icon: MARK_TWITCH },
|
||||
...config,
|
||||
endpoint: {
|
||||
authorization: 'https://id.twitch.tv/oauth2/authorize',
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_X } from '../ui/mark.js';
|
||||
import { Oauth2Provider, Oauth2WrappedConfig } from './oauth2.js';
|
||||
|
||||
export interface XProviderConfig extends Oauth2WrappedConfig {}
|
||||
@@ -37,6 +38,7 @@ export function XProvider(config: XProviderConfig) {
|
||||
return Oauth2Provider({
|
||||
...config,
|
||||
type: 'x',
|
||||
display: { name: 'X', icon: MARK_X },
|
||||
endpoint: {
|
||||
authorization: 'https://twitter.com/i/oauth2/authorize',
|
||||
token: 'https://api.x.com/2/oauth2/token'
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { MARK_YAHOO } from '../ui/mark.js';
|
||||
import { Oauth2Provider, Oauth2WrappedConfig } from './oauth2.js';
|
||||
|
||||
export interface YahooConfig extends Oauth2WrappedConfig {}
|
||||
@@ -37,6 +38,7 @@ export function YahooProvider(config: YahooConfig) {
|
||||
return Oauth2Provider({
|
||||
...config,
|
||||
type: 'yahoo',
|
||||
display: { name: 'Yahoo', icon: MARK_YAHOO },
|
||||
endpoint: {
|
||||
authorization: 'https://api.login.yahoo.com/oauth2/request_auth',
|
||||
token: 'https://api.login.yahoo.com/oauth2/get_token'
|
||||
|
||||
Reference in New Issue
Block a user