diff --git a/packages/design-system/organisms/masthead/Authed.tsx b/packages/design-system/organisms/masthead/Authed.tsx index 7b176cc..bb21c09 100644 --- a/packages/design-system/organisms/masthead/Authed.tsx +++ b/packages/design-system/organisms/masthead/Authed.tsx @@ -32,7 +32,7 @@ export const Authed = (props: Props) => { - + ( - + - + + +
+ Quick Settings: + + + +
+ + ); +}; + +export default SetApi; diff --git a/packages/web/src/pages/landing.tsx b/packages/web/src/pages/landing.tsx index 659f5c6..b5b3d86 100644 --- a/packages/web/src/pages/landing.tsx +++ b/packages/web/src/pages/landing.tsx @@ -1,7 +1,15 @@ +import { Redirect } from '@reach/router'; import { LandingTemplate } from '@roleypoly/design-system/templates/landing'; import * as React from 'react'; +import { useSessionContext } from '../session-context/SessionContext'; const Landing = () => { + const { isAuthenticated } = useSessionContext(); + + if (isAuthenticated) { + return ; + } + return ; }; diff --git a/packages/web/src/pages/machinery/new-session.tsx b/packages/web/src/pages/machinery/new-session.tsx new file mode 100644 index 0000000..5075e2f --- /dev/null +++ b/packages/web/src/pages/machinery/new-session.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; + +const NewSession = () => { + React.useEffect(() => { + const url = new URL(window.location.href); + const id = url.searchParams.get('session_id'); + if (id) { + window.location.href = '/'; + localStorage.setItem('rp_session_key', id); + } + }); + + return
Redirecting you...
; +}; + +export default NewSession; diff --git a/packages/web/src/pages/picker.tsx b/packages/web/src/pages/picker.tsx new file mode 100644 index 0000000..2368c2f --- /dev/null +++ b/packages/web/src/pages/picker.tsx @@ -0,0 +1,77 @@ +import { Redirect } from '@reach/router'; +import { RolePickerTemplate } from '@roleypoly/design-system/templates/role-picker'; +import { PresentableGuild, RoleUpdate, UserGuildPermissions } from '@roleypoly/types'; +import * as React from 'react'; +import { useSessionContext } from '../session-context/SessionContext'; +import { makeRoleTransactions } from '../utils/roleTransactions'; + +type PickerProps = { + serverID: string; +}; + +const Picker = (props: PickerProps) => { + const { session, authedFetch, isAuthenticated } = useSessionContext(); + + const [pickerData, setPickerData] = React.useState(null); + const [pending, setPending] = React.useState(false); + + React.useEffect(() => { + const fetchPickerData = async () => { + const response = await authedFetch(`/get-picker-data/${props.serverID}`); + const data = await response.json(); + + setPickerData(data); + }; + + fetchPickerData(); + }, [props.serverID, authedFetch]); + + if (!isAuthenticated) { + return ; + } + + if (pickerData === null) { + return
Loading...
; + } + + const onSubmit = async (submittedRoles: string[]) => { + if (pending === true) { + return; + } + + setPending(true); + const updatePayload: RoleUpdate = { + knownState: pickerData.member.roles, + transactions: makeRoleTransactions(pickerData.member.roles, submittedRoles), + }; + + const response = await authedFetch(`/update-roles/${props.serverID}`, { + method: 'PATCH', + body: JSON.stringify(updatePayload), + }); + if (response.status === 200) { + setPickerData({ + ...pickerData, + member: { ...pickerData.member, roles: (await response.json()).roles }, + }); + } + + setPending(false); + }; + + return ( + UserGuildPermissions.User} + onSubmit={onSubmit} + /> + ); +}; + +export default Picker; diff --git a/packages/web/src/pages/servers.tsx b/packages/web/src/pages/servers.tsx new file mode 100644 index 0000000..a362cbe --- /dev/null +++ b/packages/web/src/pages/servers.tsx @@ -0,0 +1,15 @@ +import { Redirect } from '@reach/router'; +import { ServersTemplate } from '@roleypoly/design-system/templates/servers'; +import * as React from 'react'; +import { useSessionContext } from '../session-context/SessionContext'; + +const ServersPage = () => { + const { isAuthenticated, session } = useSessionContext(); + if (!isAuthenticated || !session) { + return ; + } + + return ; +}; + +export default ServersPage; diff --git a/packages/web/src/session-context/SessionContext.tsx b/packages/web/src/session-context/SessionContext.tsx new file mode 100644 index 0000000..511458f --- /dev/null +++ b/packages/web/src/session-context/SessionContext.tsx @@ -0,0 +1,116 @@ +import { SessionData } from '@roleypoly/types'; +import * as React from 'react'; +import { useApiContext } from '../api-context/ApiContext'; + +type SessionContextT = { + session?: Omit, 'tokens'>; + setSession: (session?: SessionContextT['session']) => void; + authedFetch: (url: string, opts?: RequestInit) => Promise; + isAuthenticated: boolean; +}; + +const SessionContext = React.createContext({ + isAuthenticated: false, + setSession: () => {}, + authedFetch: async () => { + return new Response(); + }, +}); + +export const useSessionContext = () => React.useContext(SessionContext); + +export const SessionContextProvider = (props: { children: React.ReactNode }) => { + const api = useApiContext(); + const [session, setSession] = React.useState(undefined); + + const sessionContextValue: SessionContextT = React.useMemo( + () => ({ + session, + setSession, + authedFetch: (url: string, opts?: RequestInit) => { + return api.fetch(url, { + ...opts, + headers: { + ...opts?.headers, + authorization: session?.sessionID + ? `Bearer ${session?.sessionID}` + : undefined, + }, + }); + }, + isAuthenticated: !!session && !!session.sessionID && !!session.user, + }), + [session, api] + ); + + React.useEffect(() => { + // No session is set, do we have one available? + if (!sessionContextValue.session || !sessionContextValue.session.sessionID) { + // We may have the full state in session storage... + const storedSessionData = sessionStorage.getItem('rp_session_data'); + if (storedSessionData) { + try { + setSession(JSON.parse(storedSessionData)); + return; + } catch (e) { + // Oops, this data is wrong. + } + } + + // But if not, we have the key, maybe? + const storedSessionID = localStorage.getItem('rp_session_key'); + if (storedSessionID && storedSessionID !== '') { + setSession({ sessionID: storedSessionID }); + return; + } + + // If we hit here, we're definitely not authenticated. + return; + } + + // If a session is set and it's not stored, set it now. + if ( + localStorage.getItem('rp_session_key') !== + sessionContextValue.session.sessionID + ) { + localStorage.setItem( + 'rp_session_key', + sessionContextValue.session.sessionID || '' + ); + } + + // Session is set, but we don't have data. Server sup? + if (sessionContextValue.session.sessionID && !sessionContextValue.session.user) { + const syncSession = async () => { + const response = await sessionContextValue.authedFetch('/get-session'); + if (response.status !== 200) { + console.error('get-session failed', { response }); + clearSessionData(); + return; + } + + const serverSession: SessionContextT['session'] = await response.json(); + + setSession(serverSession); + sessionStorage.setItem('rp_session_data', JSON.stringify(serverSession)); + }; + + syncSession(); + } + }, [ + sessionContextValue.session?.user, + sessionContextValue.session?.sessionID, + sessionContextValue, + ]); + + return ( + + {props.children} + + ); +}; + +const clearSessionData = () => { + sessionStorage.removeItem('rp_session_data'); + localStorage.removeItem('rp_session_key'); +}; diff --git a/packages/web/src/utils/roleTransactions.spec.ts b/packages/web/src/utils/roleTransactions.spec.ts new file mode 100644 index 0000000..6c17663 --- /dev/null +++ b/packages/web/src/utils/roleTransactions.spec.ts @@ -0,0 +1,25 @@ +import { RoleTransaction, TransactionType } from '@roleypoly/types'; +import { makeRoleTransactions } from './roleTransactions'; + +it('creates a transactional diff of two sets of roles', () => { + const currentRoles = ['aaa', 'bbb', 'ccc', 'ddd']; + const nextRoles = ['bbb', 'ccc', 'ddd', 'eee', 'fff']; // removes aaa, adds eee + fff + + const transactions = makeRoleTransactions(currentRoles, nextRoles); + expect(transactions).toEqual( + expect.arrayContaining([ + { + id: 'aaa', + action: TransactionType.Remove, + }, + { + id: 'fff', + action: TransactionType.Add, + }, + { + id: 'eee', + action: TransactionType.Add, + }, + ]) + ); +}); diff --git a/packages/web/src/utils/roleTransactions.ts b/packages/web/src/utils/roleTransactions.ts new file mode 100644 index 0000000..174b181 --- /dev/null +++ b/packages/web/src/utils/roleTransactions.ts @@ -0,0 +1,30 @@ +import { Role, RoleTransaction, TransactionType } from '@roleypoly/types'; + +export const makeRoleTransactions = ( + oldRoles: Role['id'][], + newRoles: Role['id'][] +): RoleTransaction[] => { + const transactions: RoleTransaction[] = []; + + // Removes: old roles not in new roles + for (let oldID of oldRoles) { + if (!newRoles.includes(oldID)) { + transactions.push({ + id: oldID, + action: TransactionType.Remove, + }); + } + } + + // Adds: new roles not in old roles + for (let newID of newRoles) { + if (!oldRoles.includes(newID)) { + transactions.push({ + id: newID, + action: TransactionType.Add, + }); + } + } + + return transactions; +};