mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-16 17:49:09 +00:00
miniflare init
This commit is contained in:
parent
8c07ed3123
commit
688954a2e0
52 changed files with 898 additions and 25 deletions
1
packages/api/.gitignore
vendored
1
packages/api/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
dist
|
||||
.mf
|
||||
|
|
1
packages/api/.nvmrc
Normal file
1
packages/api/.nvmrc
Normal file
|
@ -0,0 +1 @@
|
|||
17
|
14
packages/api/bindings.d.ts
vendored
14
packages/api/bindings.d.ts
vendored
|
@ -1,14 +0,0 @@
|
|||
export {};
|
||||
|
||||
declare global {
|
||||
const BOT_CLIENT_ID: string;
|
||||
const BOT_CLIENT_SECRET: string;
|
||||
const UI_PUBLIC_URI: string;
|
||||
const API_PUBLIC_URI: string;
|
||||
const ROOT_USERS: string;
|
||||
const ALLOWED_CALLBACK_HOSTS: string;
|
||||
|
||||
const KV_SESSIONS: KVNamespace;
|
||||
const KV_GUILDS: KVNamespace;
|
||||
const KV_GUILD_DATA: KVNamespace;
|
||||
}
|
|
@ -66,5 +66,5 @@ router.addFallback('root', () => {
|
|||
});
|
||||
|
||||
addEventListener('fetch', (event: FetchEvent) => {
|
||||
event.respondWith(router.handle(event));
|
||||
router.handle(event));
|
||||
});
|
|
@ -1,11 +1,13 @@
|
|||
{
|
||||
"name": "@roleypoly/api",
|
||||
"version": "0.1.0",
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "yarn workspace @roleypoly/worker-emulator build --basePath `pwd`",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"start": "cfw-emulator"
|
||||
"build": "esbuild --bundle --sourcemap --platform=node --format=esm --outdir=dist --out-extension:.js=.mjs ./src/index.ts",
|
||||
"dev": "miniflare --watch --debug",
|
||||
"lint:types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^2.2.2",
|
||||
"@roleypoly/misc-utils": "*",
|
||||
|
@ -14,8 +16,13 @@
|
|||
"@roleypoly/worker-utils": "*",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"deep-equal": "^2.0.5",
|
||||
"esbuild": "^0.14.13",
|
||||
"exponential-backoff": "^3.1.0",
|
||||
"itty-router": "^2.4.10",
|
||||
"ksuid": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"ts-loader": "^8.3.0"
|
||||
"miniflare": "^2.2.0",
|
||||
"ts-loader": "^8.3.0",
|
||||
"ulid-workers": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
|
64
packages/api/src/config.ts
Normal file
64
packages/api/src/config.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { WrappedKVNamespace } from '@roleypoly/api/src/kv';
|
||||
|
||||
export type Environment = {
|
||||
BOT_CLIENT_ID: string;
|
||||
BOT_CLIENT_SECRET: string;
|
||||
BOT_TOKEN: string;
|
||||
UI_PUBLIC_URI: string;
|
||||
API_PUBLIC_URI: string;
|
||||
ROOT_USERS: string;
|
||||
ALLOWED_CALLBACK_HOSTS: string;
|
||||
BOT_IMPORT_TOKEN: string;
|
||||
INTERACTIONS_SHARED_KEY: string;
|
||||
RP_SERVER_ID: string;
|
||||
RP_HELPER_ROLE_IDS: string;
|
||||
|
||||
KV_SESSIONS: KVNamespace;
|
||||
KV_GUILDS: KVNamespace;
|
||||
KV_GUILD_DATA: KVNamespace;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
botClientID: string;
|
||||
botClientSecret: string;
|
||||
botToken: string;
|
||||
uiPublicURI: string;
|
||||
apiPublicURI: string;
|
||||
rootUsers: string[];
|
||||
allowedCallbackHosts: string[];
|
||||
importSharedKey: string;
|
||||
interactionsSharedKey: string;
|
||||
roleypolyServerID: string;
|
||||
helperRoleIDs: string[];
|
||||
kv: {
|
||||
sessions: WrappedKVNamespace;
|
||||
guilds: WrappedKVNamespace;
|
||||
guildData: WrappedKVNamespace;
|
||||
};
|
||||
_raw: Environment;
|
||||
};
|
||||
|
||||
const toList = (x: string): string[] => x.split(',');
|
||||
const safeURI = (x: string) => x.replace(/\/$/, '');
|
||||
|
||||
export const parseEnvironment = (env: Environment): Config => {
|
||||
return {
|
||||
_raw: env,
|
||||
botClientID: env.BOT_CLIENT_ID,
|
||||
botClientSecret: env.BOT_CLIENT_SECRET,
|
||||
botToken: env.BOT_TOKEN,
|
||||
uiPublicURI: safeURI(env.UI_PUBLIC_URI),
|
||||
apiPublicURI: safeURI(env.API_PUBLIC_URI),
|
||||
rootUsers: toList(env.ROOT_USERS),
|
||||
allowedCallbackHosts: toList(env.ALLOWED_CALLBACK_HOSTS),
|
||||
importSharedKey: env.BOT_IMPORT_TOKEN,
|
||||
interactionsSharedKey: env.INTERACTIONS_SHARED_KEY,
|
||||
roleypolyServerID: env.RP_SERVER_ID,
|
||||
helperRoleIDs: toList(env.RP_HELPER_ROLE_IDS),
|
||||
kv: {
|
||||
sessions: new WrappedKVNamespace(env.KV_SESSIONS),
|
||||
guilds: new WrappedKVNamespace(env.KV_GUILDS),
|
||||
guildData: new WrappedKVNamespace(env.KV_GUILD_DATA),
|
||||
},
|
||||
};
|
||||
};
|
32
packages/api/src/index.ts
Normal file
32
packages/api/src/index.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
// @ts-ignore
|
||||
import { authBounce } from '@roleypoly/api/src/routes/auth/bounce';
|
||||
import { json, notFound } from '@roleypoly/api/src/utils/response';
|
||||
import { Router } from 'itty-router';
|
||||
import { Config, Environment, parseEnvironment } from './config';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/auth/bounce', authBounce);
|
||||
|
||||
router.get('/', (request: Request, config: Config) =>
|
||||
json({
|
||||
__warning: '🦊',
|
||||
this: 'is',
|
||||
a: 'fox-based',
|
||||
web: 'application',
|
||||
please: 'be',
|
||||
mindful: 'of',
|
||||
your: 'surroundings',
|
||||
warning__: '🦊',
|
||||
meta: config.uiPublicURI,
|
||||
})
|
||||
);
|
||||
|
||||
router.get('*', () => notFound());
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Environment, ctx: FetchEvent) {
|
||||
const config = parseEnvironment(env);
|
||||
return router.handle(request, config, ctx);
|
||||
},
|
||||
};
|
23
packages/api/src/kv.ts
Normal file
23
packages/api/src/kv.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
export class WrappedKVNamespace {
|
||||
constructor(private kvNamespace: KVNamespace) {}
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const data = await this.kvNamespace.get(key, 'text');
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(data) as T;
|
||||
}
|
||||
|
||||
async put<T>(key: string, value: T, ttlSeconds?: number) {
|
||||
await this.kvNamespace.put(key, JSON.stringify(value), {
|
||||
expirationTtl: ttlSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
getRaw = this.kvNamespace.get;
|
||||
list = this.kvNamespace.list;
|
||||
getWithMetadata = this.kvNamespace.getWithMetadata;
|
||||
delete = this.kvNamespace.delete;
|
||||
}
|
45
packages/api/src/routes/auth/bounce.ts
Normal file
45
packages/api/src/routes/auth/bounce.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { Config } from '@roleypoly/api/src/config';
|
||||
import { setupStateSession } from '@roleypoly/api/src/sessions/state';
|
||||
import { getQuery } from '@roleypoly/api/src/utils/request';
|
||||
import { seeOther } from '@roleypoly/api/src/utils/response';
|
||||
import { StateSession } from '@roleypoly/types';
|
||||
|
||||
type URLParams = {
|
||||
clientID: string;
|
||||
redirectURI: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
export const buildURL = (params: URLParams) =>
|
||||
`https://discord.com/api/oauth2/authorize?client_id=${
|
||||
params.clientID
|
||||
}&response_type=code&scope=identify%20guilds&prompt=none&redirect_uri=${encodeURIComponent(
|
||||
params.redirectURI
|
||||
)}&state=${params.state}`;
|
||||
|
||||
export const isAllowedCallbackHost = (config: Config, host: string): boolean => {
|
||||
return (
|
||||
host === config.apiPublicURI ||
|
||||
config.allowedCallbackHosts.includes(host) ||
|
||||
config.allowedCallbackHosts
|
||||
.filter((callbackHost) => callbackHost.includes('*'))
|
||||
.find((wildcard) => new RegExp(wildcard.replace('*', '[a-z0-9-]+')).test(host)) !==
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
export const authBounce = async (request: Request, config: Config) => {
|
||||
const stateSessionData: StateSession = {};
|
||||
|
||||
const { cbh: callbackHost } = getQuery(request);
|
||||
if (callbackHost && isAllowedCallbackHost(config, callbackHost)) {
|
||||
stateSessionData.callbackHost = callbackHost;
|
||||
}
|
||||
|
||||
const state = await setupStateSession(config.kv.sessions, stateSessionData);
|
||||
|
||||
const redirectURI = `${config.apiPublicURI}/login-callback`;
|
||||
const clientID = config.botClientID;
|
||||
|
||||
return seeOther(buildURL({ state, redirectURI, clientID }));
|
||||
};
|
75
packages/api/src/routes/auth/callback.ts
Normal file
75
packages/api/src/routes/auth/callback.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { Config } from '@roleypoly/api/src/config';
|
||||
import { isAllowedCallbackHost } from '@roleypoly/api/src/routes/auth/bounce';
|
||||
import { getStateSession } from '@roleypoly/api/src/sessions/state';
|
||||
import { getQuery, seeOther } from '@roleypoly/api/src/utils';
|
||||
import { AuthType, discordAPIBase, discordFetch } from '@roleypoly/api/src/utils/discord';
|
||||
import { formData } from '@roleypoly/api/src/utils/request';
|
||||
import { AuthTokenResponse, StateSession } from '@roleypoly/types';
|
||||
import { decodeTime, monotonicFactory } from 'ulid-workers';
|
||||
const ulid = monotonicFactory();
|
||||
|
||||
const authFailure = (uiPublicURI: string, extra?: string) =>
|
||||
seeOther(
|
||||
uiPublicURI +
|
||||
`/machinery/error?error_code=authFailure${extra ? `&extra=${extra}` : ''}`
|
||||
);
|
||||
|
||||
export const authCallback = async (request: Request, config: Config) => {
|
||||
let bounceBaseUrl = config.uiPublicURI;
|
||||
|
||||
const { state: stateValue, code } = getQuery(request);
|
||||
|
||||
if (stateValue === null) {
|
||||
return authFailure('state missing');
|
||||
}
|
||||
|
||||
try {
|
||||
const stateTime = decodeTime(stateValue);
|
||||
const stateExpiry = stateTime + 1000 * 60 * 5;
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (currentTime > stateExpiry) {
|
||||
return authFailure('state expired');
|
||||
}
|
||||
|
||||
const stateSession = await getStateSession<StateSession>(
|
||||
config.kv.sessions,
|
||||
stateValue
|
||||
);
|
||||
if (
|
||||
stateSession?.callbackHost &&
|
||||
isAllowedCallbackHost(config, stateSession.callbackHost)
|
||||
) {
|
||||
bounceBaseUrl = stateSession.callbackHost;
|
||||
}
|
||||
} catch (e) {
|
||||
return authFailure('state invalid');
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return authFailure('code missing');
|
||||
}
|
||||
|
||||
const response = await discordFetch<AuthTokenResponse>(
|
||||
`${discordAPIBase}/oauth2/token`,
|
||||
'',
|
||||
AuthType.None,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData({
|
||||
client_id: config.botClientID,
|
||||
client_secret: config.botClientSecret,
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: config.apiPublicURI + '/auth/callback',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
return authFailure('code auth failure');
|
||||
}
|
||||
};
|
8
packages/api/src/sessions/create.ts
Normal file
8
packages/api/src/sessions/create.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Config } from "@roleypoly/api/src/config";
|
||||
import { AuthTokenResponse } from "@roleypoly/types";
|
||||
|
||||
export const createSession = async (
|
||||
config: Config,
|
||||
sessionId: string,
|
||||
tokens: AuthTokenResponse
|
||||
)
|
5
packages/api/src/sessions/flags.ts
Normal file
5
packages/api/src/sessions/flags.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { SessionData, SessionFlags } from '@roleypoly/types';
|
||||
|
||||
export const getSessionFlags = async (session: SessionData): Promise<SessionFlags> => {
|
||||
return SessionFlags.None;
|
||||
};
|
23
packages/api/src/sessions/state.ts
Normal file
23
packages/api/src/sessions/state.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { WrappedKVNamespace } from '@roleypoly/api/src/kv';
|
||||
import { monotonicFactory } from 'ulid-workers';
|
||||
const ulid = monotonicFactory();
|
||||
|
||||
export const setupStateSession = async <T>(
|
||||
Sessions: WrappedKVNamespace,
|
||||
data: T
|
||||
): Promise<string> => {
|
||||
const stateID = ulid();
|
||||
|
||||
await Sessions.put(`state_${stateID}`, { data }, 60 * 5);
|
||||
|
||||
return stateID;
|
||||
};
|
||||
|
||||
export const getStateSession = async <T>(
|
||||
Sessions: WrappedKVNamespace,
|
||||
stateID: string
|
||||
): Promise<T | undefined> => {
|
||||
const stateSession = await Sessions.get<{ data: T }>(`state_${stateID}`);
|
||||
|
||||
return stateSession?.data;
|
||||
};
|
123
packages/api/src/utils/discord.ts
Normal file
123
packages/api/src/utils/discord.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { Config } from '@roleypoly/api/src/config';
|
||||
import {
|
||||
evaluatePermission,
|
||||
permissions as Permissions,
|
||||
} from '@roleypoly/misc-utils/hasPermission';
|
||||
import {
|
||||
AuthTokenResponse,
|
||||
DiscordUser,
|
||||
GuildSlug,
|
||||
UserGuildPermissions,
|
||||
} from '@roleypoly/types';
|
||||
|
||||
export const userAgent =
|
||||
'DiscordBot (https://github.com/roleypoly/roleypoly, git-main) (+https://roleypoly.com)';
|
||||
|
||||
export const discordAPIBase = 'https://discordapp.com/api/v9';
|
||||
|
||||
export enum AuthType {
|
||||
Bearer = 'Bearer',
|
||||
Bot = 'Bot',
|
||||
None = 'None',
|
||||
}
|
||||
|
||||
export const discordFetch = async <T>(
|
||||
url: string,
|
||||
auth: string,
|
||||
authType: AuthType = AuthType.Bearer,
|
||||
init?: RequestInit
|
||||
): Promise<T | null> => {
|
||||
const response = await fetch(discordAPIBase + url, {
|
||||
...(init || {}),
|
||||
headers: {
|
||||
...(init?.headers || {}),
|
||||
...(authType !== AuthType.None
|
||||
? {
|
||||
authorization: `${AuthType[authType]} ${auth}`,
|
||||
}
|
||||
: {}),
|
||||
'user-agent': userAgent,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
console.error('discordFetch failed', {
|
||||
url,
|
||||
authType,
|
||||
payload: await response.text(),
|
||||
});
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return (await response.json()) as T;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getTokenUser = async (
|
||||
accessToken: AuthTokenResponse['access_token']
|
||||
): Promise<DiscordUser | null> => {
|
||||
const user = await discordFetch<DiscordUser>(
|
||||
'/users/@me',
|
||||
accessToken,
|
||||
AuthType.Bearer
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, username, discriminator, bot, avatar } = user;
|
||||
|
||||
return { id, username, discriminator, bot, avatar };
|
||||
};
|
||||
|
||||
type UserGuildsPayload = {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
owner: boolean;
|
||||
permissions: number;
|
||||
features: string[];
|
||||
}[];
|
||||
|
||||
export const getTokenGuilds = async (accessToken: string, config: Config) => {
|
||||
const guilds = await discordFetch<UserGuildsPayload>(
|
||||
'/users/@me/guilds',
|
||||
accessToken,
|
||||
AuthType.Bearer
|
||||
);
|
||||
|
||||
if (!guilds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const guildSlugs = guilds.map<GuildSlug>((guild) => ({
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
icon: guild.icon,
|
||||
permissionLevel: parsePermissions(
|
||||
BigInt(guild.permissions),
|
||||
guild.owner,
|
||||
config.roleypolyServerID
|
||||
),
|
||||
}));
|
||||
|
||||
return guildSlugs;
|
||||
};
|
||||
|
||||
export const parsePermissions = (
|
||||
permissions: bigint,
|
||||
owner: boolean = false
|
||||
): UserGuildPermissions => {
|
||||
if (owner || evaluatePermission(permissions, Permissions.ADMINISTRATOR)) {
|
||||
return UserGuildPermissions.Admin;
|
||||
}
|
||||
|
||||
if (evaluatePermission(permissions, Permissions.MANAGE_ROLES)) {
|
||||
return UserGuildPermissions.Manager;
|
||||
}
|
||||
|
||||
return UserGuildPermissions.User;
|
||||
};
|
15
packages/api/src/utils/request.ts
Normal file
15
packages/api/src/utils/request.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export const getQuery = (request: Request): { [x: string]: string } => {
|
||||
const output: { [x: string]: string } = {};
|
||||
|
||||
for (let [key, value] of new URL(request.url).searchParams.entries()) {
|
||||
output[key] = value;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
export const formData = (obj: Record<string, any>): string => {
|
||||
return Object.keys(obj)
|
||||
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
|
||||
.join('&');
|
||||
};
|
24
packages/api/src/utils/response.ts
Normal file
24
packages/api/src/utils/response.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
export const json = (obj: any, init?: ResponseInit): Response => {
|
||||
const body = JSON.stringify(obj);
|
||||
return new Response(body, {
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const notFound = () => json({ error: 'not found' }, { status: 404 });
|
||||
|
||||
export const seeOther = (url: string) =>
|
||||
new Response(
|
||||
`<!doctype html>If you are not redirected soon, <a href="${url}">click here.</a>`,
|
||||
{
|
||||
status: 303,
|
||||
headers: {
|
||||
location: url,
|
||||
'content-type': 'text/html; charset=utf-8',
|
||||
},
|
||||
}
|
||||
);
|
|
@ -3,7 +3,9 @@
|
|||
"outDir": "./dist",
|
||||
"lib": ["esnext", "webworker", "ES2020.BigInt", "ES2020.Promise"],
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"target": "ES2019"
|
||||
"target": "ES2019",
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs"
|
||||
},
|
||||
"include": [
|
||||
"./*.ts",
|
||||
|
|
29
packages/api/wrangler.toml
Normal file
29
packages/api/wrangler.toml
Normal file
|
@ -0,0 +1,29 @@
|
|||
# THIS DOES NOT WORK WITH WRANGLER BY DEFAULT.
|
||||
# BE EXTREMELY AWARE OF THIS CAVEAT.
|
||||
|
||||
name = "api"
|
||||
type = "javascript"
|
||||
account_id = ""
|
||||
workers_dev = true
|
||||
route = ""
|
||||
zone_id = ""
|
||||
|
||||
kv_namespaces = [
|
||||
{ binding = "KV_SESSIONS", id = "", preview_id = "" },
|
||||
{ binding = "KV_GUILDS", id = "", preview_id = "" },
|
||||
{ binding = "KV_GUILD_DATA", id = "", preview_id = "" },
|
||||
]
|
||||
|
||||
[build]
|
||||
command = "yarn build"
|
||||
[build.upload]
|
||||
format = "modules"
|
||||
dir = "dist"
|
||||
main = "index.mjs"
|
||||
|
||||
[miniflare]
|
||||
host = "0.0.0.0"
|
||||
port = 6609
|
||||
watch = true
|
||||
env_path = "../../.env"
|
||||
kv_persist = true
|
|
@ -354,8 +354,8 @@ export const LunarNewYear: Variant = {
|
|||
// Feb 1, 2022
|
||||
// Jan 22, 2023
|
||||
activeIf: (currentDate?: Date) =>
|
||||
matchDay(new Date('2022-Jan-30'), new Date('2022-Feb-3'), currentDate, true) ||
|
||||
matchDay(new Date('2023-Jan-20'), new Date('2023-Jan-24'), currentDate, true),
|
||||
matchDay(new Date('2022-Jan-27'), new Date('2022-Feb-10'), currentDate, true) ||
|
||||
matchDay(new Date('2023-Jan-17'), new Date('2023-Jan-29'), currentDate, true),
|
||||
sharedProps: {
|
||||
circleFill: palette.red200,
|
||||
circleOuterFill: palette.gold400,
|
||||
|
|
|
@ -50,6 +50,7 @@ export enum UserGuildPermissions {
|
|||
User,
|
||||
Manager = 1 << 1,
|
||||
Admin = 1 << 2,
|
||||
RoleypolySupport = 1 << 3,
|
||||
}
|
||||
|
||||
export type GuildSlug = {
|
||||
|
|
|
@ -9,12 +9,19 @@ export type AuthTokenResponse = {
|
|||
scope: string;
|
||||
};
|
||||
|
||||
export enum SessionFlags {
|
||||
None = 0,
|
||||
IsRoot = 1 << 0,
|
||||
IsSupport = 1 << 1,
|
||||
}
|
||||
|
||||
export type SessionData = {
|
||||
/** sessionID is a KSUID */
|
||||
sessionID: string;
|
||||
tokens: AuthTokenResponse;
|
||||
user: DiscordUser;
|
||||
guilds: GuildSlug[];
|
||||
flags: SessionFlags;
|
||||
};
|
||||
|
||||
export type StateSession = {
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"@types/react-helmet": "^6.1.2",
|
||||
"babel-loader": "8.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"exponential-backoff": "^3.1.0",
|
||||
"ts-loader": "^8.3.0",
|
||||
"webpack": "4.44.2"
|
||||
},
|
||||
|
|
|
@ -160,6 +160,7 @@ export const SessionContextProvider = (props: { children: React.ReactNode }) =>
|
|||
setLock(false);
|
||||
} catch (e) {
|
||||
console.error('syncSession failed', e);
|
||||
deleteSessionKey();
|
||||
setLock(false);
|
||||
}
|
||||
};
|
||||
|
@ -179,7 +180,7 @@ const saveSessionKey = (key: string) => localStorage.setItem('rp_session_key', k
|
|||
const deleteSessionKey = () => localStorage.removeItem('rp_session_key');
|
||||
const getSessionKey = () => localStorage.getItem('rp_session_key');
|
||||
|
||||
type ServerSession = Omit<SessionData, 'tokens'>;
|
||||
type ServerSession = Omit<Omit<SessionData, 'tokens'>, 'flags'>;
|
||||
const fetchSession = async (
|
||||
authedFetch: SessionContextT['authedFetch']
|
||||
): Promise<ServerSession | null> => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue