mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 19:39:11 +00:00
update web, fix integration issues
This commit is contained in:
parent
2fb721078e
commit
e162096c03
30 changed files with 476 additions and 2574 deletions
14
README.md
14
README.md
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -125,6 +125,9 @@ export type APIMember = {
|
|||
roles: string[];
|
||||
pending: boolean;
|
||||
nick: string;
|
||||
user: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const parsePermissions = (
|
||||
|
|
|
@ -22,7 +22,7 @@ export const fetchLegacyServer = async (
|
|||
config: Config,
|
||||
id: string
|
||||
): Promise<LegacyGuildData | null> => {
|
||||
if (!config.interactionsSharedKey) {
|
||||
if (!config.importSharedKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"discord.js": "^13.6.0",
|
||||
"dotenv": "^10.0.0"
|
||||
"dotenv": "^14.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
5
packages/design-system/hack/jestSetup.ts
Normal file
5
packages/design-system/hack/jestSetup.ts
Normal 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() });
|
|
@ -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': {
|
||||
|
|
|
@ -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 />
|
||||
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 />
|
||||
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 />
|
||||
Manage your Data
|
||||
</>
|
||||
}
|
||||
description="Export or delete all of your Roleypoly data."
|
||||
link={`/s/${props.guildData.id}/edit/data`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
7
packages/design-system/tsconfig.test.json
Normal file
7
packages/design-system/tsconfig.test.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
// "exclude": ["node_modules"]
|
||||
}
|
|
@ -15,6 +15,10 @@ module.exports = {
|
|||
: [match.loader.include];
|
||||
match.loader.include = [...include, ...includePaths];
|
||||
}
|
||||
|
||||
webpackConfig.resolve.fallback = {
|
||||
crypto: false,
|
||||
};
|
||||
return webpackConfig;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 = {
|
||||
|
|
34
packages/web/src/contexts/session/AuthedFetchContext.tsx
Normal file
34
packages/web/src/contexts/session/AuthedFetchContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Reference in a new issue