update web, fix integration issues

This commit is contained in:
41666 2022-01-30 16:14:52 -05:00
parent 2fb721078e
commit e162096c03
30 changed files with 476 additions and 2574 deletions

View file

@ -41,12 +41,14 @@ This is the fastest way to start. You must be using MacOS or Linux (WSL2 is ok!)
- Setup `.env` using [`.env.example`][envexample] as a template and guide.
- When setting up your Discord Application, be sure to set `http://localhost:6609/login-callback` as the OAuth2 callback URL.
- Run: `yarn install`
- Run both: `yarn start`
- Run: `yarn start`
- This starts the Web UI, Storybook, and API servers in hot-reload dev/emulation mode. All changes to TS/TSX files should be properly captured and reloaded for you!
- Develop you a Roleypoly!
#### Option 3 🐄🤠: Wrangler (No emulation)
**Outdated. This won't work, but could give you an idea of what to do.**
This is probably extremely painful and requires you to have a Cloudflare account.
- With pre-requisites:
@ -93,26 +95,26 @@ Run:
- `yarn` to install deps
- `yarn start:design-system` to open storybook
- `yarn test` to test
- `yarn test:design-system` to test
### Developing Web UI
For working with the Next.js frontend components, use the below steps as reference. Code lives in `src/pages` among elsewhere.
For working with the Next.js frontend components, use the below steps as reference. Code lives in `src/web` among elsewhere.
Run:
- `yarn` to install deps
- `yarn start:web` to run Next.js dev server
- `yarn test` to test
- `yarn test:web` to test
### Developing API Components
For working with the API, use the below steps as reference. Code lives in `src/backend-worker`.
For working with the API, use the below steps as reference. Code lives in `src/api`.
Run:
- `yarn` to install deps
- `yarn start:api` to start an emulated worker
- `yarn test` to test
- `yarn test:api` to test
[envexample]: .env.example

View file

@ -31,10 +31,10 @@
"lint:types-api": "yarn workspace @roleypoly/api run lint:types",
"postinstall": "is-ci || husky install",
"start": "run-p -c start:*",
"start:api": "yarn workspace @roleypoly/api start",
"start:bot": "yarn workspace @roleypoly/bot start",
"start:design-system": "yarn workspace @roleypoly/design-system start",
"start:web": "yarn workspace @roleypoly/web start",
"start:worker": "yarn workspace @roleypoly/api start",
"test": "run-p -c test:* --",
"test:api": "yarn workspace @roleypoly/api run test",
"test:design-system": "yarn workspace @roleypoly/design-system run test",
@ -48,6 +48,7 @@
"jest-react-hooks-shallow": "^1.5.1",
"lint-staged": "^12.3.2",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.5",
"postcss-syntax": "^0.36.2",
"prettier": "^2.5.1",
"prettier-plugin-organize-imports": "^2.3.4",

View file

@ -46,7 +46,7 @@ describe('getGuild', () => {
roles: [],
};
await config.kv.guilds.put('guilds/123', guild, config.retention.guild);
await config.kv.guilds.put('123', guild, config.retention.guild);
mockDiscordFetch.mockReturnValue({ ...guild, name: 'test2' });
const result = await getGuild(config, '123');
@ -220,7 +220,7 @@ describe('getGuildMember', () => {
nick: 'test2',
};
await config.kv.guilds.put('guilds/123/members/123', member, config.retention.guild);
await config.kv.guilds.put('123:members:123', member, config.retention.guild);
mockDiscordFetch.mockReturnValue({ ...member, nick: 'test' });
const result = await getGuildMember(config, '123', '123');

View file

@ -25,7 +25,7 @@ export const getGuild = async (
forceMiss?: boolean
): Promise<(Guild & OwnRoleInfo) | null> =>
config.kv.guilds.cacheThrough(
`guilds/${id}`,
`guild/${id}`,
async () => {
const guildRaw = await discordFetch<APIGuild>(
`/guilds/${id}`,
@ -54,7 +54,7 @@ export const getGuild = async (
managed: role.managed,
position: role.position,
permissions: role.permissions,
safety: RoleSafety.Safe, // TODO: calculate this
safety: calculateRoleSafety(role, highestRolePosition),
}));
const guild: Guild & OwnRoleInfo = {
@ -133,7 +133,7 @@ export const getGuildMember = async (
overrideRetention?: number // allows for own-member to be cached as long as it's used.
): Promise<Member | null> =>
config.kv.guilds.cacheThrough(
`guilds/${serverID}/members/${userID}`,
`members/${serverID}/${userID}`,
async () => {
const discordMember = await discordFetch<APIMember>(
`/guilds/${serverID}/members/${userID}`,
@ -156,6 +156,23 @@ export const getGuildMember = async (
forceMiss
);
export const updateGuildMember = async (
config: Config,
serverID: string,
member: APIMember
): Promise<void> => {
config.kv.guilds.put(
`members/${serverID}/${member.user.id}`,
{
guildid: serverID,
roles: member.roles,
pending: member.pending,
nick: member.nick,
},
config.retention.member
);
};
const calculateRoleSafety = (role: Role | APIRole, highestBotRolePosition: number) => {
let safety = RoleSafety.Safe;

View file

@ -19,7 +19,7 @@ import { Router } from 'itty-router';
import { authBounce } from './routes/auth/bounce';
import { Environment, parseEnvironment } from './utils/config';
import { Context, RoleypolyHandler } from './utils/context';
import { json, notFound, serverError } from './utils/response';
import { corsHeaders, json, notFound, serverError } from './utils/response';
const router = Router();
@ -42,8 +42,7 @@ router.delete(
);
router.put('/guilds/:guildId/roles', ...guildsCommon, guildsRolesPut);
// Slug is unauthenticated...
router.get('/guilds/slug/:guildId', injectParams, guildsSlug);
router.get('/guilds/:guildId/slug', injectParams, withSession, guildsSlug);
router.post('/interactions', handleInteraction);
@ -60,7 +59,23 @@ router.get('/', ((request: Request, { config }: Context) =>
meta: config.uiPublicURI,
})) as RoleypolyHandler);
router.any('*', () => notFound());
router.options('*', (request: Request) => {
return new Response(null, {
headers: {
...corsHeaders,
},
});
});
router.all('/*', notFound);
const scrubURL = (urlStr: string) => {
const url = new URL(urlStr);
url.searchParams.delete('code');
url.searchParams.delete('state');
return url.toString();
};
export default {
async fetch(request: Request, env: Environment, event: Context['fetchContext']) {
@ -68,13 +83,14 @@ export default {
const context: Context = {
config,
fetchContext: {
waitUntil: event.waitUntil,
waitUntil: event.waitUntil.bind(event),
},
authMode: {
type: 'anonymous',
},
params: {},
};
console.log(`${request.method} ${scrubURL(request.url)}`);
return router
.handle(request, context)
.catch((e: Error) => (!e ? notFound() : serverError(e)));

View file

@ -2,7 +2,7 @@ import { isAllowedCallbackHost } from '@roleypoly/api/src/routes/auth/bounce';
import { createSession } from '@roleypoly/api/src/sessions/create';
import { getStateSession } from '@roleypoly/api/src/sessions/state';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { AuthType, discordAPIBase, discordFetch } from '@roleypoly/api/src/utils/discord';
import { AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
import { dateFromID } from '@roleypoly/api/src/utils/id';
import { formDataRequest, getQuery } from '@roleypoly/api/src/utils/request';
import { seeOther } from '@roleypoly/api/src/utils/response';
@ -51,7 +51,7 @@ export const authCallback: RoleypolyHandler = async (
}
const response = await discordFetch<AuthTokenResponse>(
`${discordAPIBase}/oauth2/token`,
`/oauth2/token`,
'',
AuthType.None,
formDataRequest({

View file

@ -2,9 +2,10 @@ import {
getGuild,
getGuildData,
getGuildMember,
updateGuildMember,
} from '@roleypoly/api/src/guilds/getters';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
import { APIMember, AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
import {
engineeringProblem,
invalid,
@ -66,11 +67,14 @@ export const guildsRolesPut: RoleypolyHandler = async (
updateRequest,
});
if (isIdenticalArray(member.roles, newRoles)) {
if (
isIdenticalArray(member.roles, newRoles) ||
isIdenticalArray(updateRequest.knownState, newRoles)
) {
return invalid();
}
const patchMemberRoles = await discordFetch<Member>(
const patchMemberRoles = await discordFetch<APIMember>(
`/guilds/${guildID}/members/${userID}`,
context.config.botToken,
AuthType.Bot,
@ -90,7 +94,9 @@ export const guildsRolesPut: RoleypolyHandler = async (
return serverError(new Error('discord rejected the request'));
}
context.fetchContext.waitUntil(getGuildMember(context.config, guildID, userID, true));
context.fetchContext.waitUntil(
updateGuildMember(context.config, guildID, patchMemberRoles)
);
const updatedMember: Member = {
roles: patchMemberRoles.roles,

View file

@ -4,6 +4,7 @@ import {
getGuildMember,
} from '@roleypoly/api/src/guilds/getters';
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
import { getQuery } from '@roleypoly/api/src/utils/request';
import { json, notFound } from '@roleypoly/api/src/utils/response';
import { PresentableGuild } from '@roleypoly/types';
@ -11,7 +12,8 @@ export const guildsGuild: RoleypolyHandler = async (
request: Request,
context: Context
) => {
const guild = await getGuild(context.config, context.params!.guildId!);
const { noCache } = getQuery(request);
const guild = await getGuild(context.config, context.params!.guildId!, !!noCache);
if (!guild) {
return notFound();
@ -20,7 +22,8 @@ export const guildsGuild: RoleypolyHandler = async (
const member = await getGuildMember(
context.config,
context.params!.guildId!,
context.session!.user.id
context.session!.user.id,
!!noCache
);
if (!member) {

View file

@ -8,6 +8,13 @@ export const guildsSlug: RoleypolyHandler = async (
context: Context
) => {
const id = context.params.guildId!;
const guildInSession = context.session?.guilds.find((guild) => guild.id === id);
if (guildInSession) {
return json<GuildSlug>(guildInSession);
}
const guild = await getGuild(context.config, id);
if (!guild) {
return notFound();
@ -19,5 +26,6 @@ export const guildsSlug: RoleypolyHandler = async (
icon: guild.icon,
permissionLevel: UserGuildPermissions.User,
};
return json(slug);
return json<GuildSlug>(slug);
};

View file

@ -125,6 +125,9 @@ export type APIMember = {
roles: string[];
pending: boolean;
nick: string;
user: {
id: string;
};
};
export const parsePermissions = (

View file

@ -22,7 +22,7 @@ export const fetchLegacyServer = async (
config: Config,
id: string
): Promise<LegacyGuildData | null> => {
if (!config.interactionsSharedKey) {
if (!config.importSharedKey) {
return null;
}

View file

@ -1,14 +1,22 @@
export const json = (obj: any, init?: ResponseInit): Response => {
export const json = <T>(obj: T, init?: ResponseInit): Response => {
const body = JSON.stringify(obj);
return new Response(body, {
...init,
headers: {
...init?.headers,
'content-type': 'application/json; charset=utf-8',
...corsHeaders,
},
});
};
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, PATCH',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
export const noContent = () => new Response(null, { status: 204 });
export const seeOther = (url: string) =>
new Response(

View file

@ -6,6 +6,6 @@
},
"dependencies": {
"discord.js": "^13.6.0",
"dotenv": "^10.0.0"
"dotenv": "^14.3.2"
}
}

View file

@ -31,7 +31,7 @@ export const BreakpointsProvider = (props: { children: React.ReactNode }) => {
};
updateScreenSize();
setImmediate(() => updateScreenSize());
setTimeout(() => updateScreenSize(), 0);
mediaQueries.onDesktop.addEventListener('change', updateScreenSize);
mediaQueries.onTablet.addEventListener('change', updateScreenSize);

View file

@ -0,0 +1,5 @@
import '@testing-library/jest-dom';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import Enzyme from 'enzyme';
Enzyme.configure({ adapter: new Adapter() });

View file

@ -3,7 +3,11 @@ module.exports = {
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: 'jsdom',
reporters: ['default'],
setupFilesAfterEnv: ['jest-styled-components', '../../hack/jestSetup.ts'],
setupFilesAfterEnv: [
'jest-styled-components',
'../../hack/jestSetup.ts',
'./hack/jestSetup.ts',
],
snapshotSerializers: ['enzyme-to-json/serializer'],
globals: {
'ts-jest': {

View file

@ -6,7 +6,7 @@ import {
} from '@roleypoly/design-system/molecules/server-utilities/ServerUtilities.styled';
import { hasFeature } from '@roleypoly/misc-utils/hasFeature';
import { Features, GuildData } from '@roleypoly/types';
import { GoArchive, GoChevronRight, GoReport, GoShield, GoSync } from 'react-icons/go';
import { GoArchive, GoChevronRight, GoReport, GoShield } from 'react-icons/go';
type Props = {
guildData: GuildData;
@ -54,25 +54,17 @@ export const ServerUtilities = (props: Props) => (
link={`/s/${props.guildData.id}/edit/audit-logging`}
/>
)}
<Utility
title={
<>
<GoSync />
&nbsp;&nbsp;Import from Roleypoly Legacy
</>
}
description="Used Roleypoly before and don't see your categories?"
link={`/s/${props.guildData.id}/edit/import-from-legacy`}
/>
<Utility
title={
<>
<GoArchive />
&nbsp;&nbsp;Manage your Data
</>
}
description="Export or delete all of your Roleypoly data."
link={`/s/${props.guildData.id}/edit/data`}
/>
{hasFeature(props.guildData.features, Features.Preview) && (
<Utility
title={
<>
<GoArchive />
&nbsp;&nbsp;Manage your Data
</>
}
description="Export or delete all of your Roleypoly data."
link={`/s/${props.guildData.id}/edit/data`}
/>
)}
</div>
);

View file

@ -11,12 +11,12 @@ import {
Role,
RoleSafety,
} from '@roleypoly/types';
import KSUID from 'ksuid';
import { flatten, sortBy } from 'lodash';
import React from 'react';
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
import { CgReorder } from 'react-icons/cg';
import { GoArrowDown, GoArrowUp, GoCheck, GoGrabber, GoPlus } from 'react-icons/go';
import { ulid } from 'ulidx';
import {
CategoryActions,
ReorderButton,
@ -70,7 +70,7 @@ export const ServerCategoryEditor = (props: Props) => {
const newCategory: Category = {
...defaultCategory,
id: KSUID.randomSync().string,
id: ulid(),
position: categories.length,
};
@ -96,7 +96,7 @@ export const ServerCategoryEditor = (props: Props) => {
if (c.id === category.id) {
return {
...defaultCategory,
id: KSUID.randomSync().string,
id: ulid(),
position: category.position,
};
}

View file

@ -12,7 +12,6 @@
"chroma-js": "^2.3.0",
"deep-equal": "^2.0.5",
"isomorphic-unfetch": "^3.1.0",
"ksuid": "^3.0.0",
"lodash": "^4.17.21",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
@ -23,7 +22,8 @@
"react-is": "^17.0.2",
"react-tooltip": "^4.2.21",
"styled-components": "^5.3.3",
"styled-normalize": "^8.0.7"
"styled-normalize": "^8.0.7",
"ulidx": "^0.3.0"
},
"devDependencies": {
"@icons/material": "^0.4.1",
@ -33,8 +33,13 @@
"@storybook/addons": "^6.4.16",
"@storybook/react": "^6.4.16",
"@storybook/theming": "^6.4.16",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/chroma-js": "^2.1.3",
"@types/deep-equal": "^1.0.1",
"@types/enzyme": "^3.10.11",
"@types/lodash": "^4.14.178",
"@types/node": "^17.0.13",
"@types/react": "^17.0.38",
"@types/react-beautiful-dnd": "^13.1.2",
@ -47,7 +52,7 @@
"babel-plugin-styled-components": "^2.0.2",
"change-case": "^4.1.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.6",
"enzyme-to-json": "^3.6.2",
"jest": "^27.4.7",
"jest-styled-components": "^7.0.8",
"typescript": "^4.5.5"

View file

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx"
}
// "exclude": ["node_modules"]
}

View file

@ -15,6 +15,10 @@ module.exports = {
: [match.loader.include];
match.loader.include = [...include, ...includePaths];
}
webpackConfig.resolve.fallback = {
crypto: false,
};
return webpackConfig;
},
},

View file

@ -8,7 +8,7 @@ export const getDefaultApiUrl = memoizeOne.default((host: string) => {
/^stage\.roleypoly\.com$/.test(host)
) {
return 'https://api-stage.roleypoly.com';
} else if (/\blocalhost|127\.0\.0\.1\b/.test(host)) {
} else if (/\blocalhost|127\.0\.0\.1|172.21.92\b/.test(host)) {
return 'http://localhost:6609';
} else {
return 'https://api-prod.roleypoly.com';

View file

@ -1,6 +1,7 @@
import { GuildSlug, PresentableGuild } from '@roleypoly/types';
import React from 'react';
import { useApiContext } from '../api/ApiContext';
import { useAuthedFetch } from '../session/AuthedFetchContext';
import { useSessionContext } from '../session/SessionContext';
const CACHE_HOLD_TIME = 2 * 60 * 1000; // 2 minutes
@ -29,7 +30,8 @@ export const GuildContext = React.createContext<GuildContextT>({
export const useGuildContext = () => React.useContext(GuildContext);
export const GuildProvider = (props: { children: React.ReactNode }) => {
const { session, authedFetch } = useSessionContext();
const { session } = useSessionContext();
const { authedFetch } = useAuthedFetch();
const { fetch } = useApiContext();
const guildContextValue: GuildContextT = {

View file

@ -0,0 +1,34 @@
import React from 'react';
import { useApiContext } from '../api/ApiContext';
import { useSessionContext } from './SessionContext';
type AuthedFetchContextT = {
authedFetch: (url: string, options?: RequestInit) => Promise<Response>;
};
export const AuthedFetchContext = React.createContext<AuthedFetchContextT>({
authedFetch: () => Promise.reject(new Error('AuthedFetchContext not initialized')),
});
export const useAuthedFetch = () => React.useContext(AuthedFetchContext);
export const AuthedFetchProvider = (props: { children: React.ReactNode }) => {
const { fetch } = useApiContext();
const { sessionID } = useSessionContext();
const authedFetch = (url: string, options?: RequestInit) => {
return fetch(url, {
...options,
headers: {
...options?.headers,
Authorization: sessionID ? `Bearer ${sessionID}` : undefined,
},
});
};
return (
<AuthedFetchContext.Provider value={{ authedFetch }}>
{props.children}
</AuthedFetchContext.Provider>
);
};

View file

@ -2,12 +2,6 @@ import { SessionData } from '@roleypoly/types';
import * as React from 'react';
import { useApiContext } from '../api/ApiContext';
enum SessionState {
NoAuth,
HalfAuth,
FullAuth,
}
type SavedSession = {
sessionID: SessionData['sessionID'];
@ -18,10 +12,8 @@ type SavedSession = {
};
type SessionContextT = {
setupSession: (sessionID: string) => void;
authedFetch: (url: string, opts?: RequestInit) => Promise<Response>;
setupSession: (sessionID: string | null) => void;
isAuthenticated: boolean;
sessionState: SessionState;
sessionID?: SessionData['sessionID'];
session: {
user?: SessionData['user'];
@ -30,149 +22,125 @@ type SessionContextT = {
};
const SessionContext = React.createContext<SessionContextT>({
sessionState: SessionState.NoAuth,
sessionID: undefined,
isAuthenticated: false,
session: {
user: undefined,
guilds: undefined,
},
isAuthenticated: false,
setupSession: () => {},
authedFetch: async () => {
return new Response();
},
});
export const useSessionContext = () => React.useContext(SessionContext);
export const SessionContextProvider = (props: { children: React.ReactNode }) => {
const { fetch } = useApiContext();
const [sessionID, setSessionID] =
React.useState<SessionContextT['sessionID']>(undefined);
const [sessionState, setSessionState] = React.useState<SessionState>(
SessionState.NoAuth
);
const [session, setSession] = React.useState<SessionContextT['session']>({
user: undefined,
guilds: undefined,
const [locked, setLock] = React.useState(false);
const [session, setSession] = React.useState<SessionContextT>({
sessionID: undefined,
session: {
user: undefined,
guilds: undefined,
},
isAuthenticated: false,
setupSession: (key: string | null) => {
if (key) {
saveSessionKey(key);
setSession({
...session,
sessionID: key,
});
} else {
deleteSessionKey();
deleteSessionData();
setSession({
...session,
sessionID: undefined,
isAuthenticated: false,
});
}
},
});
const [lock, setLock] = React.useState(false);
// Possible flows:
/*
if no key, check if key available in LS
No session:
no key
isAuth = false
Half session:
have key
lock = true
isAuth = false
fetch cached in SS _OR_ syncSession()
lock = false
Full session
have session
isAuth = true
*/
const sessionContextValue: SessionContextT = {
sessionID,
session,
sessionState,
isAuthenticated: sessionState === SessionState.FullAuth,
setupSession: async (newID: string) => {
setSessionID(newID);
setSessionState(SessionState.HalfAuth);
saveSessionKey(newID);
},
authedFetch: async (url: string, init?: RequestInit): Promise<Response> => {
if (sessionID) {
init = {
...init,
headers: {
...init?.headers,
authorization: `Bearer ${sessionID}`,
},
};
React.useEffect(() => {
const fetchSession = async (sessionID: string): Promise<ServerSession | null> => {
const sessionResponse = await fetch('/auth/session', {
headers: {
Authorization: `Bearer ${sessionID}`,
},
});
if (sessionResponse.status !== 200) {
return null;
}
return fetch(url, init);
},
};
const { guilds, user }: ServerSession = await sessionResponse.json();
return {
sessionID,
guilds,
user,
};
};
const { setupSession, authedFetch } = sessionContextValue;
// Local storage sync on NoAuth
React.useEffect(() => {
if (!sessionID) {
const storedKey = getSessionKey();
if (!storedKey) {
return;
}
setupSession(storedKey);
}
}, [sessionID, setupSession]);
// Sync session data on HalfAuth
React.useEffect(() => {
if (lock) {
console.warn('hit syncSession lock');
if (locked) {
console.warn('Session locked, skipping update');
return;
}
if (!session.sessionID) {
const sessionKey = getSessionKey();
if (sessionKey) {
session.setupSession(sessionKey);
}
}
if (sessionState === SessionState.HalfAuth) {
setLock(true);
// Use cached session
const storedData = getSessionData();
if (storedData && storedData?.sessionID === sessionID) {
setSession(storedData.session);
setSessionState(SessionState.FullAuth);
setLock(false);
return;
if (session.sessionID && !session.session.user) {
// Lets see if session is in session storage...
const sessionData = getSessionData();
if (sessionData) {
setSession({
...session,
isAuthenticated: true,
session: {
user: sessionData.session.user,
guilds: sessionData.session.guilds,
},
});
}
// If no cached session, let's grab it from server
const syncSession = async () => {
try {
const serverSession = await fetchSession(authedFetch);
if (!serverSession) {
// Not found, lets reset.
deleteSessionKey();
setSessionID(undefined);
setSessionState(SessionState.NoAuth);
// If not, lets fetch it from the server
setLock(true);
fetchSession(session.sessionID)
.then((sessionData) => {
if (sessionData) {
setSession({
...session,
isAuthenticated: true,
session: {
user: sessionData.user,
guilds: sessionData.guilds,
},
});
saveSessionData({
sessionID: session.sessionID!,
session: {
user: sessionData.user,
guilds: sessionData.guilds,
},
});
} else {
session.setupSession(null);
setLock(false);
return;
}
const newSession = {
user: serverSession.user,
guilds: serverSession.guilds,
};
saveSessionData({ sessionID: sessionID || '', session: newSession });
setSession(newSession);
setSessionState(SessionState.FullAuth);
})
.catch((e) => {
console.error(e);
session.setupSession(null);
setLock(false);
} catch (e) {
console.error('syncSession failed', e);
deleteSessionKey();
setTimeout(() => setLock(false), 1000); // Unlock after 1s to prevent loop flood
}
};
syncSession();
});
}
}, [sessionState, sessionID, authedFetch, lock]);
}, [session, locked, setLock, fetch]);
return (
<SessionContext.Provider value={sessionContextValue}>
{props.children}
</SessionContext.Provider>
<SessionContext.Provider value={session}>{props.children}</SessionContext.Provider>
);
};
@ -181,23 +149,9 @@ const deleteSessionKey = () => localStorage.removeItem('rp_session_key');
const getSessionKey = () => localStorage.getItem('rp_session_key');
type ServerSession = Omit<Omit<SessionData, 'tokens'>, 'flags'>;
const fetchSession = async (
authedFetch: SessionContextT['authedFetch']
): Promise<ServerSession | null> => {
const sessionResponse = await authedFetch('/auth/session');
if (sessionResponse.status !== 200) {
return null;
}
const { sessionID, guilds, user }: ServerSession = await sessionResponse.json();
return {
sessionID,
guilds,
user,
};
};
const saveSessionData = (data: SavedSession) =>
sessionStorage.setItem('rp_session_data', JSON.stringify(data));
const getSessionData = (): SavedSession | null =>
JSON.parse(sessionStorage.getItem('rp_session_data') || 'null');
const deleteSessionData = () => localStorage.removeItem('rp_session_data');

View file

@ -6,6 +6,7 @@ import { ApiContextProvider } from './contexts/api/ApiContext';
import { AppShellPropsProvider } from './contexts/app-shell/AppShellContext';
import { GuildProvider } from './contexts/guild/GuildContext';
import { RecentGuildsProvider } from './contexts/recent-guilds/RecentGuildsContext';
import { AuthedFetchProvider } from './contexts/session/AuthedFetchContext';
import { SessionContextProvider } from './contexts/session/SessionContext';
const ProviderProvider = (props: {
@ -24,6 +25,7 @@ ReactDOM.render(
providerChain={[
ApiContextProvider,
SessionContextProvider,
AuthedFetchProvider,
RecentGuildsProvider,
AppShellPropsProvider,
BreakpointsProvider,

View file

@ -10,6 +10,7 @@ import * as React from 'react';
import { useAppShellProps } from '../contexts/app-shell/AppShellContext';
import { useGuildContext } from '../contexts/guild/GuildContext';
import { useRecentGuilds } from '../contexts/recent-guilds/RecentGuildsContext';
import { useAuthedFetch } from '../contexts/session/AuthedFetchContext';
import { useSessionContext } from '../contexts/session/SessionContext';
import { Title } from '../utils/metaTitle';
@ -20,10 +21,11 @@ type EditorProps = {
const Editor = (props: EditorProps) => {
const { serverID } = props;
const { session, authedFetch, isAuthenticated } = useSessionContext();
const { session, isAuthenticated } = useSessionContext();
const { authedFetch } = useAuthedFetch();
const { pushRecentGuild } = useRecentGuilds();
const appShellProps = useAppShellProps();
const { getFullGuild } = useGuildContext();
const { getFullGuild, uncacheGuild } = useGuildContext();
const [guild, setGuild] = React.useState<PresentableGuild | null | false>(null);
const [pending, setPending] = React.useState(false);
@ -96,6 +98,7 @@ const Editor = (props: EditorProps) => {
if (response.status === 200) {
setGuild(guild);
uncacheGuild(serverID);
navigate(`/s/${props.serverID}`);
}

View file

@ -11,10 +11,12 @@ import React from 'react';
import { useAppShellProps } from '../../contexts/app-shell/AppShellContext';
import { useGuildContext } from '../../contexts/guild/GuildContext';
import { useRecentGuilds } from '../../contexts/recent-guilds/RecentGuildsContext';
import { useAuthedFetch } from '../../contexts/session/AuthedFetchContext';
import { useSessionContext } from '../../contexts/session/SessionContext';
const AccessControlPage = (props: { serverID: string; path: string }) => {
const { session, isAuthenticated, authedFetch } = useSessionContext();
const { session, isAuthenticated } = useSessionContext();
const { authedFetch } = useAuthedFetch();
const { pushRecentGuild } = useRecentGuilds();
const { getFullGuild, uncacheGuild } = useGuildContext();
const appShellProps = useAppShellProps();

View file

@ -7,6 +7,7 @@ import * as React from 'react';
import { useAppShellProps } from '../contexts/app-shell/AppShellContext';
import { useGuildContext } from '../contexts/guild/GuildContext';
import { useRecentGuilds } from '../contexts/recent-guilds/RecentGuildsContext';
import { useAuthedFetch } from '../contexts/session/AuthedFetchContext';
import { useSessionContext } from '../contexts/session/SessionContext';
import { Title } from '../utils/metaTitle';
import { makeRoleTransactions } from '../utils/roleTransactions';
@ -17,7 +18,8 @@ type PickerProps = {
};
const Picker = (props: PickerProps) => {
const { session, authedFetch, isAuthenticated } = useSessionContext();
const { session, isAuthenticated } = useSessionContext();
const { authedFetch } = useAuthedFetch();
const { pushRecentGuild } = useRecentGuilds();
const appShellProps = useAppShellProps();
const { getFullGuild, uncacheGuild } = useGuildContext();

2564
yarn.lock

File diff suppressed because it is too large Load diff