353 lines
12 KiB
HTML
353 lines
12 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Mock - OAuth Provider</title>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script>
|
|
/*
|
|
* This file's purpose is to provide a way to develop the UI without
|
|
* running a full featured OAuth server. It mocks the server responses and
|
|
* provides configuration data to the UI.
|
|
*
|
|
* This file is not part of the production build.
|
|
*
|
|
* Start the development server with the following command from the
|
|
* oauth-provider root:
|
|
*
|
|
* ```sh
|
|
* pnpm run start:ui
|
|
* ```
|
|
*
|
|
* Then open the browser at http://localhost:5173/
|
|
*/
|
|
</script>
|
|
<style>
|
|
:root {
|
|
--branding-color-primary: 10 122 255;
|
|
--branding-color-primary-contrast: 255 255 255;
|
|
--branding-color-primary-hue: 212.57142857142856;
|
|
|
|
--branding-color-error: 244 11 66;
|
|
--branding-color-error-contrast: 255 255 255;
|
|
--branding-color-error-hue: 345.83690987124464;
|
|
|
|
--branding-color-warning: 251 86 7;
|
|
--branding-color-warning-contrast: 255 255 255;
|
|
--branding-color-warning-hue: 19.426229508196723;
|
|
|
|
--branding-color-success: 2 195 154;
|
|
--branding-color-success-contrast: 0 0 0;
|
|
--branding-color-success-hue: 167.2538860103627;
|
|
}
|
|
</style>
|
|
<script type="module">
|
|
import { API_ENDPOINT_PREFIX } from '@atproto/oauth-provider-api'
|
|
|
|
/*
|
|
* PDS branding configuration
|
|
*/
|
|
|
|
history.replaceState(history.state, '', '/account')
|
|
|
|
const devices = new Map([
|
|
[
|
|
'device1',
|
|
{
|
|
userAgent:
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
ipAddress: '192.0.0.1',
|
|
},
|
|
],
|
|
[
|
|
'device2',
|
|
{
|
|
userAgent:
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
ipAddress: '192.0.0.1',
|
|
},
|
|
],
|
|
])
|
|
|
|
const accounts = new Map(
|
|
[
|
|
{
|
|
sub: 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',
|
|
email: 'eric@foobar.com',
|
|
email_verified: true,
|
|
name: 'Eric',
|
|
preferred_username: 'esb.lol',
|
|
picture:
|
|
'https://cdn.bsky.app/img/avatar/plain/did:plc:3jpt2mvvsumj2r7eqk4gzzjz/bafkreiaexnb3bkzbaxktm5q3l3txyweflh3smcruigesvroqjrqxec4zv4@jpeg',
|
|
},
|
|
{
|
|
sub: 'did:plc:dpajgwmnecpdyjyqzjzm6bnb',
|
|
email: 'eric@foobar.com',
|
|
email_verified: true,
|
|
name: 'Tom Sawyeeeeeeeeeee',
|
|
preferred_username: 'test.esb.lol',
|
|
picture:
|
|
'https://cdn.bsky.app/img/avatar/plain/did:plc:dpajgwmnecpdyjyqzjzm6bnb/bafkreia6dx7fhoi6fxwfpgm7jrxijpqci7ap53wpilkpazojwvqlmgud2m@jpeg',
|
|
},
|
|
{
|
|
sub: 'did:plc:matttmattmattmattmattmat',
|
|
email: 'matthieu@foobar.com',
|
|
email_verified: true,
|
|
name: 'Matthieu',
|
|
preferred_username: 'matthieu.bsky.test',
|
|
picture: /** @type {sting|undefined} */ (undefined),
|
|
},
|
|
].map((a) => [a.sub, a]),
|
|
)
|
|
|
|
const clients = new Map(
|
|
[
|
|
{
|
|
client_id: 'https://bsky.app/oauth-client.json',
|
|
client_name: 'Bluesky',
|
|
client_uri: 'https://bsky.app',
|
|
logo_uri: 'https://web-cdn.bsky.app/static/apple-touch-icon.png',
|
|
},
|
|
].map((c) => [c.client_id, c]),
|
|
)
|
|
|
|
// Unable to load metadata for this client:
|
|
clients.set('https://example.com/oauth-client.json', undefined)
|
|
|
|
const accountDeviceSessions = new Map([
|
|
[
|
|
'device1',
|
|
[
|
|
{
|
|
sub: 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',
|
|
remember: true,
|
|
loginRequired: true,
|
|
},
|
|
{
|
|
sub: 'did:plc:dpajgwmnecpdyjyqzjzm6bnb',
|
|
remember: false,
|
|
loginRequired: false,
|
|
},
|
|
],
|
|
],
|
|
[
|
|
'device2',
|
|
[
|
|
{
|
|
sub: 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',
|
|
remember: true,
|
|
loginRequired: false,
|
|
},
|
|
],
|
|
],
|
|
])
|
|
|
|
const accountOAuthSessions = new Map([
|
|
[
|
|
'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',
|
|
[
|
|
{
|
|
tokenId: 'token1',
|
|
createdAt: '2023-10-01T00:00:00.000Z',
|
|
updatedAt: '2025-10-01T00:00:00.000Z',
|
|
clientId: 'https://bsky.app/oauth-client.json',
|
|
scope: 'atproto transition:generic transition:chat.bsky',
|
|
},
|
|
],
|
|
],
|
|
[
|
|
'did:plc:dpajgwmnecpdyjyqzjzm6bnb',
|
|
[
|
|
{
|
|
tokenId: 'token2',
|
|
createdAt: '2023-10-01T00:00:00.000Z',
|
|
updatedAt: '2023-10-01T00:00:00.000Z',
|
|
clientId: 'https://bsky.app/oauth-client.json',
|
|
scope: 'atproto transition:generic transition:chat.bsky',
|
|
},
|
|
{
|
|
tokenId: 'token3',
|
|
createdAt: '2024-08-01T00:00:00.000Z',
|
|
updatedAt: '2025-10-01T00:00:00.000Z',
|
|
clientId: 'https://example.com/oauth-client.json',
|
|
scope: /** @type {string|undefined} */ (undefined),
|
|
},
|
|
],
|
|
],
|
|
])
|
|
|
|
const currentDeviceId = 'device1' // Simulate that this device is "device1"
|
|
|
|
async function mockFetch(...args) {
|
|
const [input, init] = args
|
|
|
|
const method = init?.method ?? 'GET'
|
|
const url =
|
|
typeof input === 'string'
|
|
? new URL(input, window.location)
|
|
: input instanceof URL
|
|
? input
|
|
: undefined
|
|
|
|
if (url) {
|
|
console.log(`Fetching: ${method} ${url.pathname}${url.search}`)
|
|
switch (`${method} ${url.pathname}`) {
|
|
case `POST ${API_ENDPOINT_PREFIX}/sign-up`: {
|
|
const {
|
|
locale,
|
|
handle,
|
|
email,
|
|
password,
|
|
inviteCode,
|
|
hcaptchaToken,
|
|
} = JSON.parse(init.body)
|
|
|
|
return jsonResponse({ error: 'Not implemented' }, 400)
|
|
}
|
|
|
|
case `POST ${API_ENDPOINT_PREFIX}/sign-in`: {
|
|
const { username, remember } = JSON.parse(init.body)
|
|
for (const [sub, account] of accounts) {
|
|
if (
|
|
account.email === username ||
|
|
account.preferred_username === username ||
|
|
username === 'a'
|
|
) {
|
|
accountDeviceSessions.set(
|
|
currentDeviceId,
|
|
(
|
|
accountDeviceSessions
|
|
.get(currentDeviceId)
|
|
?.filter((s) => s.sub !== sub) ?? []
|
|
).concat({ sub, remember, loginRequired: false }),
|
|
)
|
|
return jsonResponse({ account })
|
|
}
|
|
}
|
|
return jsonResponse({ error: 'Invalid credentials' }, 400)
|
|
}
|
|
case `GET ${API_ENDPOINT_PREFIX}/device-sessions`:
|
|
return jsonResponse(
|
|
accountDeviceSessions.get(currentDeviceId)?.map((s) => ({
|
|
remembered: s.remember,
|
|
loginRequired: s.loginRequired,
|
|
account: accounts.get(s.sub),
|
|
})) ?? [],
|
|
)
|
|
case `GET ${API_ENDPOINT_PREFIX}/oauth-sessions`: {
|
|
const sub = url.searchParams.get('sub')
|
|
return jsonResponse(
|
|
accountOAuthSessions.get(sub)?.map((oauthSession) => ({
|
|
...oauthSession,
|
|
clientMetadata: clients.get(oauthSession.clientId),
|
|
})) ?? [],
|
|
)
|
|
}
|
|
case `GET ${API_ENDPOINT_PREFIX}/account-sessions`: {
|
|
const sub = url.searchParams.get('sub')
|
|
return jsonResponse(
|
|
Array.from(
|
|
accountDeviceSessions.entries(),
|
|
([deviceId, deviceSession]) =>
|
|
deviceSession
|
|
.filter((s) => s.sub === sub)
|
|
.map((s) => ({
|
|
deviceId,
|
|
deviceMetadata: devices.get(deviceId),
|
|
remember: s.remember,
|
|
isCurrentDevice: true,
|
|
})),
|
|
).flat(),
|
|
)
|
|
}
|
|
case `POST ${API_ENDPOINT_PREFIX}/sign-out`: {
|
|
const { sub } = JSON.parse(init.body)
|
|
accountDeviceSessions.set(
|
|
currentDeviceId,
|
|
accountDeviceSessions
|
|
.get(currentDeviceId)
|
|
?.filter((s) => s.sub !== sub) ?? [],
|
|
)
|
|
return jsonResponse({ success: true })
|
|
}
|
|
case `POST ${API_ENDPOINT_PREFIX}/revoke-account-session`: {
|
|
const { sub, deviceId } = JSON.parse(init.body)
|
|
accountDeviceSessions.set(
|
|
deviceId,
|
|
accountDeviceSessions
|
|
.get(deviceId)
|
|
?.filter((s) => s.sub !== sub) ?? [],
|
|
)
|
|
return jsonResponse({ success: true })
|
|
}
|
|
case `POST ${API_ENDPOINT_PREFIX}/verify-handle-availability`:
|
|
return jsonResponse({ available: true })
|
|
case `POST ${API_ENDPOINT_PREFIX}/reset-password-request`:
|
|
return jsonResponse({ available: true })
|
|
case `POST ${API_ENDPOINT_PREFIX}/reset-password-confirm`:
|
|
return jsonResponse({ available: true })
|
|
}
|
|
}
|
|
|
|
return origFetch.call(this, ...args)
|
|
}
|
|
|
|
function jsonResponse(payload, status = 200) {
|
|
console.log('Mock response:', payload)
|
|
return new Response(JSON.stringify(payload), {
|
|
status,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
}
|
|
|
|
const origFetch = window.fetch
|
|
Object.defineProperty(window, 'fetch', {
|
|
writable: true,
|
|
configurable: true,
|
|
value: mockFetch,
|
|
})
|
|
|
|
window.__customizationData = {
|
|
availableUserDomains: ['.bsky.social', '.bsky.team'],
|
|
inviteCodeRequired: false,
|
|
hcaptchaSiteKey: undefined,
|
|
name: 'Bluesky',
|
|
links: [
|
|
{
|
|
title: { en: 'Home', fr: 'Accueil' },
|
|
href: 'https://bsky.social/',
|
|
rel: 'canonical', // prevents the login page from being indexed by search engines
|
|
},
|
|
{
|
|
title: { en: 'Terms of Service' },
|
|
href: 'https://bsky.social/about/support/tos',
|
|
rel: 'terms-of-service',
|
|
},
|
|
{
|
|
title: { en: 'Privacy Policy' },
|
|
href: 'https://bsky.social/about/support/privacy-policy',
|
|
rel: 'privacy-policy',
|
|
},
|
|
{
|
|
title: { en: 'Support' },
|
|
href: 'https://blueskyweb.zendesk.com/hc/en-us',
|
|
rel: 'help',
|
|
},
|
|
],
|
|
logo: `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 320 286"><path fill="rgb(10,122,255)" d="M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z" /></svg>')}`,
|
|
}
|
|
|
|
window.__deviceSessions =
|
|
accountDeviceSessions.get(currentDeviceId)?.map((s) => ({
|
|
remembered: s.remember,
|
|
loginRequired: s.loginRequired,
|
|
account: accounts.get(s.sub),
|
|
})) ?? []
|
|
</script>
|
|
<script src="./src/account-page.tsx" type="module"></script>
|
|
</body>
|
|
</html>
|