mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-16 01:29:09 +00:00
port full auth flow to cf workers
This commit is contained in:
parent
9eeb946389
commit
aad0987dce
50 changed files with 551 additions and 1167 deletions
5
src/backend-worker/bindings.d.ts
vendored
5
src/backend-worker/bindings.d.ts
vendored
|
@ -6,5 +6,8 @@ declare global {
|
|||
const UI_PUBLIC_URI: string;
|
||||
const API_PUBLIC_URI: string;
|
||||
const ROOT_USERS: string;
|
||||
const KV_PREFIX: string;
|
||||
|
||||
const KV_SESSIONS: KVNamespace;
|
||||
const KV_GUILDS: KVNamespace;
|
||||
const KV_GUILD_DATA: KVNamespace;
|
||||
}
|
||||
|
|
31
src/backend-worker/handlers/get-session.ts
Normal file
31
src/backend-worker/handlers/get-session.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { SessionData } from 'roleypoly/common/types';
|
||||
import { getSessionID, respond } from '../utils/api-tools';
|
||||
import { Sessions } from '../utils/kv';
|
||||
|
||||
const NotAuthenticated = (extra?: string) =>
|
||||
respond(
|
||||
{
|
||||
err: extra || 'not authenticated',
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
|
||||
export const GetSession = async (request: Request): Promise<Response> => {
|
||||
const sessionID = getSessionID(request);
|
||||
if (!sessionID) {
|
||||
return NotAuthenticated('missing auth header');
|
||||
}
|
||||
|
||||
console.log(sessionID);
|
||||
|
||||
const sessionData = await Sessions.get<SessionData>(sessionID.id);
|
||||
if (!sessionData) {
|
||||
return NotAuthenticated('session not found');
|
||||
}
|
||||
|
||||
const { tokens, ...withoutTokens } = sessionData;
|
||||
|
||||
return respond({
|
||||
...withoutTokens,
|
||||
});
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import KSUID from 'ksuid';
|
||||
import { Bounce } from '../utils/bounce';
|
||||
import { apiPublicURI, botClientID } from '../utils/config';
|
||||
|
||||
type URLParams = {
|
||||
clientID: string;
|
||||
|
@ -14,10 +15,10 @@ const buildURL = (params: URLParams) =>
|
|||
params.redirectURI
|
||||
)}&state=${params.state}`;
|
||||
|
||||
export const LoginBounce = (request: Request): Response => {
|
||||
const state = uuidv4();
|
||||
const redirectURI = `${API_PUBLIC_URI}/login-callback`;
|
||||
const clientID = BOT_CLIENT_ID;
|
||||
export const LoginBounce = async (request: Request): Promise<Response> => {
|
||||
const state = await KSUID.random();
|
||||
const redirectURI = `${apiPublicURI}/login-callback`;
|
||||
const clientID = botClientID;
|
||||
|
||||
return Bounce(buildURL({ state, redirectURI, clientID }));
|
||||
return Bounce(buildURL({ state: state.string, redirectURI, clientID }));
|
||||
};
|
||||
|
|
|
@ -1,3 +1,134 @@
|
|||
export const LoginCallback = (request: Request): Response => {
|
||||
return new Response('login-callback!');
|
||||
import KSUID from 'ksuid';
|
||||
import {
|
||||
DiscordUser,
|
||||
GuildSlug,
|
||||
SessionData,
|
||||
AuthTokenResponse,
|
||||
} from '../../common/types';
|
||||
import { formData, resolveFailures, parsePermissions } from '../utils/api-tools';
|
||||
import { Bounce } from '../utils/bounce';
|
||||
import { apiPublicURI, botClientID, botClientSecret, uiPublicURI } from '../utils/config';
|
||||
import { Sessions } from '../utils/kv';
|
||||
|
||||
const AuthErrorResponse = (extra?: string) =>
|
||||
Bounce(
|
||||
uiPublicURI +
|
||||
`/machinery/error?error_code=authFailure${extra ? `&extra=${extra}` : ''}`
|
||||
);
|
||||
|
||||
export const LoginCallback = resolveFailures(
|
||||
AuthErrorResponse(),
|
||||
async (request: Request): Promise<Response> => {
|
||||
const query = new URL(request.url).searchParams;
|
||||
|
||||
const stateValue = query.get('state');
|
||||
|
||||
if (stateValue === null) {
|
||||
return AuthErrorResponse('state missing');
|
||||
}
|
||||
|
||||
try {
|
||||
const state = KSUID.parse(stateValue);
|
||||
const stateExpiry = state.date.getTime() + 1000 * 60 * 5;
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (currentTime > stateExpiry) {
|
||||
return AuthErrorResponse('state expired');
|
||||
}
|
||||
} catch (e) {
|
||||
return AuthErrorResponse('state invalid');
|
||||
}
|
||||
|
||||
const code = query.get('code');
|
||||
if (!code) {
|
||||
return AuthErrorResponse('code missing');
|
||||
}
|
||||
|
||||
const tokenRequest = {
|
||||
client_id: botClientID,
|
||||
client_secret: botClientSecret,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: apiPublicURI + '/login-callback',
|
||||
scope: 'identify guilds',
|
||||
code,
|
||||
};
|
||||
|
||||
const tokenFetch = await fetch('https://discord.com/api/v8/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData(tokenRequest),
|
||||
});
|
||||
|
||||
const tokens = (await tokenFetch.json()) as AuthTokenResponse;
|
||||
|
||||
if (!tokens.access_token) {
|
||||
console.info({ tokens });
|
||||
return AuthErrorResponse('token response invalid');
|
||||
}
|
||||
|
||||
const [sessionID, user, guilds] = await Promise.all([
|
||||
KSUID.random(),
|
||||
getUser(tokens.access_token),
|
||||
getGuilds(tokens.access_token),
|
||||
]);
|
||||
|
||||
const sessionData: SessionData = {
|
||||
tokens,
|
||||
sessionID: sessionID.string,
|
||||
user,
|
||||
guilds,
|
||||
};
|
||||
|
||||
await Sessions.put(sessionID.string, sessionData, 60 * 60 * 6);
|
||||
|
||||
return Bounce(
|
||||
uiPublicURI + '/machinery/new-session?session_id=' + sessionID.string
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const discordFetch = async <T>(url: string, auth: string): Promise<T> => {
|
||||
const response = await fetch('https://discord.com/api/v8' + url, {
|
||||
headers: {
|
||||
authorization: 'Bearer ' + auth,
|
||||
},
|
||||
});
|
||||
|
||||
return (await response.json()) as T;
|
||||
};
|
||||
|
||||
const getUser = async (accessToken: string): Promise<DiscordUser> => {
|
||||
const { id, username, discriminator, bot, avatar } = await discordFetch<DiscordUser>(
|
||||
'/users/@me',
|
||||
accessToken
|
||||
);
|
||||
|
||||
return { id, username, discriminator, bot, avatar };
|
||||
};
|
||||
|
||||
type UserGuildsPayload = {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
owner: boolean;
|
||||
permissions: number;
|
||||
features: string[];
|
||||
}[];
|
||||
|
||||
const getGuilds = async (accessToken: string) => {
|
||||
const guilds = await discordFetch<UserGuildsPayload>(
|
||||
'/users/@me/guilds',
|
||||
accessToken
|
||||
);
|
||||
|
||||
const guildSlugs = guilds.map<GuildSlug>((guild) => ({
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
icon: guild.icon,
|
||||
permissionLevel: parsePermissions(guild.permissions, guild.owner),
|
||||
}));
|
||||
|
||||
return guildSlugs;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { BotJoin } from './handlers/bot-join';
|
||||
import { GetSession } from './handlers/get-session';
|
||||
import { LoginBounce } from './handlers/login-bounce';
|
||||
import { LoginCallback } from './handlers/login-callback';
|
||||
import { Router } from './router';
|
||||
|
@ -8,6 +9,7 @@ const router = new Router();
|
|||
router.add('GET', 'bot-join', BotJoin);
|
||||
router.add('GET', 'login-bounce', LoginBounce);
|
||||
router.add('GET', 'login-callback', LoginCallback);
|
||||
router.add('GET', 'get-session', GetSession);
|
||||
|
||||
addEventListener('fetch', (event: FetchEvent) => {
|
||||
event.respondWith(router.handle(event.request));
|
||||
|
|
55
src/backend-worker/utils/api-tools.ts
Normal file
55
src/backend-worker/utils/api-tools.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { UserGuildPermissions } from '../../common/types';
|
||||
import {
|
||||
evaluatePermission,
|
||||
permissions as Permissions,
|
||||
} from '../../common/utils/hasPermission';
|
||||
|
||||
export const formData = (obj: Record<string, any>): string => {
|
||||
return Object.keys(obj)
|
||||
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
|
||||
.join('&');
|
||||
};
|
||||
|
||||
export const respond = (obj: Record<string, any>, init?: ResponseInit) =>
|
||||
new Response(JSON.stringify(obj), init);
|
||||
|
||||
export const resolveFailures = (
|
||||
handleWith: Response,
|
||||
handler: (request: Request) => Promise<Response> | Response
|
||||
) => async (request: Request): Promise<Response> | Response => {
|
||||
try {
|
||||
return handler(request);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return handleWith;
|
||||
}
|
||||
};
|
||||
|
||||
export const parsePermissions = (
|
||||
permissions: number,
|
||||
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;
|
||||
};
|
||||
|
||||
export const getSessionID = (request: Request): { type: string; id: string } | null => {
|
||||
const sessionID = request.headers.get('authorization');
|
||||
if (!sessionID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [type, id] = sessionID.split(' ');
|
||||
if (type !== 'Bearer') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { type, id };
|
||||
};
|
11
src/backend-worker/utils/config.ts
Normal file
11
src/backend-worker/utils/config.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
const self = (global as any) as Record<string, string>;
|
||||
|
||||
const safeURI = (x: string) => x.replace(/\/$/, '');
|
||||
const list = (x: string) => x.split(',');
|
||||
|
||||
export const botClientID = self.BOT_CLIENT_ID;
|
||||
export const botClientSecret = self.BOT_CLIENT_SECRET;
|
||||
export const uiPublicURI = safeURI(self.UI_PUBLIC_URI);
|
||||
export const apiPublicURI = safeURI(self.API_PUBLIC_URI);
|
||||
export const rootUsers = list(self.ROOT_USERS);
|
||||
export const kvPrefix = self.KV_PREFIX;
|
26
src/backend-worker/utils/kv.ts
Normal file
26
src/backend-worker/utils/kv.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
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,
|
||||
});
|
||||
}
|
||||
|
||||
list = this.kvNamespace.list;
|
||||
getWithMetadata = this.kvNamespace.getWithMetadata;
|
||||
delete = this.kvNamespace.delete;
|
||||
}
|
||||
|
||||
export const Sessions = new WrappedKVNamespace(KV_SESSIONS);
|
||||
export const GuildData = new WrappedKVNamespace(KV_GUILD_DATA);
|
||||
export const Guilds = new WrappedKVNamespace(KV_GUILDS);
|
|
@ -1,40 +0,0 @@
|
|||
package faas
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AuthLevel uint
|
||||
|
||||
const (
|
||||
AuthGuest = AuthLevel(iota)
|
||||
AuthUser
|
||||
AuthAdmin
|
||||
AuthSuper
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotAuthorized = errors.New("common/faas: session not authorized")
|
||||
)
|
||||
|
||||
func assertAuthLevel(err error, requiredAuthLevel AuthLevel, assertedAuthLevel AuthLevel) error {
|
||||
if requiredAuthLevel == assertedAuthLevel {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// AuthMustMatch will assert the current session's authorization group/level; only can match for Guest, User, and Super.
|
||||
func AuthMustMatch(request *http.Request, authLevel AuthLevel) error {
|
||||
_, err := request.Cookie("Authorization")
|
||||
if errors.Is(err, http.ErrNoCookie) {
|
||||
// No cookie is present, assert guest.
|
||||
return assertAuthLevel(ErrNotAuthorized, authLevel, AuthGuest)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
package faas
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var bounceHTML = template.Must(template.New("bounceHTML").Parse(
|
||||
`<!doctype html>
|
||||
<meta charset="utf8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0;URL='{{.Location}}'">
|
||||
<style>
|
||||
body {
|
||||
background-color: #453E3D;
|
||||
color: #AB9B9A;
|
||||
}
|
||||
a {
|
||||
color: #AB9B9A;
|
||||
}
|
||||
</style>
|
||||
<p>
|
||||
Redirecting you to <a href="{{.Location}}">{{.Location}}</a>
|
||||
</p>
|
||||
`,
|
||||
))
|
||||
|
||||
type bounceData struct {
|
||||
Location string
|
||||
}
|
||||
|
||||
// Bounce will do a 303 See Other response with url.
|
||||
func Bounce(rw http.ResponseWriter, url string) {
|
||||
rw.Header().Add("location", url)
|
||||
rw.WriteHeader(303)
|
||||
bounceHTML.Execute(rw, bounceData{Location: url})
|
||||
}
|
||||
|
||||
// Stash will save the specified URL for later use in Unstash(), e.g. after an OAuth bounce
|
||||
func Stash(rw http.ResponseWriter, url string) {
|
||||
if url == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cookie := http.Cookie{
|
||||
Name: "rp_stashed_url",
|
||||
Value: url,
|
||||
HttpOnly: true,
|
||||
Expires: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
|
||||
rw.Header().Add("set-cookie", cookie.String())
|
||||
}
|
||||
|
||||
// Unstash will redirect/Bounce() to a previously stashed URL or the defaultURL, whichever is available.
|
||||
func Unstash(rw http.ResponseWriter, req *http.Request, defaultURL string) {
|
||||
redirectURL := defaultURL
|
||||
cookie, _ := req.Cookie("rp_stashed_url")
|
||||
|
||||
if cookie != nil && cookie.Expires.After(time.Now()) && cookie.Value != "" {
|
||||
redirectURL = cookie.Value
|
||||
}
|
||||
|
||||
unsetter := http.Cookie{
|
||||
Name: "rp_stashed_url",
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
}
|
||||
|
||||
rw.Header().Set("set-cookie", unsetter.String())
|
||||
|
||||
Bounce(rw, redirectURL)
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package faas
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common/types"
|
||||
)
|
||||
|
||||
func Fingerprint(req *http.Request) types.Fingerprint {
|
||||
return types.Fingerprint{
|
||||
UserAgent: req.UserAgent(),
|
||||
ClientIP: req.RemoteAddr,
|
||||
ForwardedFor: req.Header.Get("x-forwarded-for"),
|
||||
}
|
||||
}
|
|
@ -34,3 +34,16 @@ export type PresentableGuild = {
|
|||
export type GuildEnumeration = {
|
||||
guildsList: PresentableGuild[];
|
||||
};
|
||||
|
||||
export enum UserGuildPermissions {
|
||||
User,
|
||||
Manager,
|
||||
Admin,
|
||||
}
|
||||
|
||||
export type GuildSlug = {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
permissionLevel: UserGuildPermissions;
|
||||
};
|
||||
|
|
18
src/common/types/Session.ts
Normal file
18
src/common/types/Session.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { GuildSlug } from './Guild';
|
||||
import { DiscordUser } from './User';
|
||||
|
||||
export type AuthTokenResponse = {
|
||||
access_token: string;
|
||||
token_type: 'Bearer';
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
};
|
||||
|
||||
export type SessionData = {
|
||||
/** sessionID is a KSUID */
|
||||
sessionID: string;
|
||||
tokens: AuthTokenResponse;
|
||||
user: DiscordUser;
|
||||
guilds: GuildSlug[];
|
||||
};
|
|
@ -2,3 +2,4 @@ export * from './Role';
|
|||
export * from './Category';
|
||||
export * from './Guild';
|
||||
export * from './User';
|
||||
export * from './Session';
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { Role } from 'roleypoly/common/types';
|
||||
import { Role } from '../types';
|
||||
|
||||
export const evaluatePermission = (haystack: number, needle: number): boolean => {
|
||||
return (haystack & needle) === needle;
|
||||
};
|
||||
|
||||
export const hasPermission = (roles: Role[], permission: number): boolean => {
|
||||
const aggregateRoles = roles.reduce((acc, role) => acc | role.permissions, 0);
|
||||
return (aggregateRoles & permission) === permission;
|
||||
return evaluatePermission(aggregateRoles, permission);
|
||||
};
|
||||
|
||||
export const hasPermissionOrAdmin = (roles: Role[], permission: number): boolean =>
|
||||
|
|
1
src/common/utils/isBrowser.ts
Normal file
1
src/common/utils/isBrowser.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const isBrowser = () => typeof window !== 'undefined';
|
|
@ -5,3 +5,6 @@ export const initialsFromName = (name: string) =>
|
|||
.map((x) => x[0])
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
|
||||
export const avatarHash = (id: string, hash: string) =>
|
||||
`https://cdn.discordapp.com/icons/${id}/${hash}.webp?size=256`;
|
||||
|
|
|
@ -125,8 +125,8 @@ export const Bi: Variant = {
|
|||
matchDay(new Date('2021-Sep-16'), new Date('2021-Sep-24'), currentDate),
|
||||
sharedProps: {
|
||||
circleFill: '#D60270',
|
||||
circleOuterFill: '#9B4F96',
|
||||
typeFill: '#0038A8',
|
||||
circleOuterFill: '#0038A8',
|
||||
typeFill: '#9B4F96',
|
||||
},
|
||||
tooltip: 'Being bi is a lot like a riding a bicycle since they can go both ways.',
|
||||
Logomark: (props: DynamicLogoProps) => <Logomark {...props} {...Bi.sharedProps} />,
|
||||
|
@ -276,7 +276,7 @@ export const BicycleDay: Variant = {
|
|||
export const Christmas: Variant = {
|
||||
// Dec 20-27
|
||||
activeIf: (currentDate?: Date) =>
|
||||
matchDay(new Date('2021-Dec-20'), new Date('2021-Dec-28'), currentDate),
|
||||
true || matchDay(new Date('2021-Dec-20'), new Date('2021-Dec-28'), currentDate),
|
||||
sharedProps: {
|
||||
circleFill: palette.green200,
|
||||
circleOuterFill: palette.red200,
|
||||
|
@ -284,10 +284,14 @@ export const Christmas: Variant = {
|
|||
},
|
||||
tooltip: 'Have yourself a merry little Christmas~',
|
||||
Logomark: (props: DynamicLogoProps) => (
|
||||
<Logomark {...props} {...Christmas.sharedProps} />
|
||||
<SparkleOverlay strokeColor={'white'}>
|
||||
<Logomark {...props} {...Christmas.sharedProps} />
|
||||
</SparkleOverlay>
|
||||
),
|
||||
Logotype: (props: DynamicLogoProps) => (
|
||||
<Logotype {...props} {...Christmas.sharedProps} />
|
||||
<SparkleOverlay strokeColor={'white'}>
|
||||
<Logotype {...props} {...Christmas.sharedProps} />
|
||||
</SparkleOverlay>
|
||||
),
|
||||
};
|
||||
|
||||
|
|
|
@ -2,52 +2,37 @@ import Link from 'next/link';
|
|||
import * as React from 'react';
|
||||
import { GoStar, GoZap } from 'react-icons/go';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { hasPermission, permissions } from 'roleypoly/common/utils/hasPermission';
|
||||
import { GuildSlug, UserGuildPermissions } from 'roleypoly/common/types';
|
||||
import { sortBy } from 'roleypoly/common/utils/sortBy';
|
||||
import { GuildEnumeration, PresentableGuild, Role } from 'roleypoly/common/types';
|
||||
import { NavSlug } from 'roleypoly/design-system/molecules/nav-slug';
|
||||
import { GuildNavItem } from './GuildNav.styled';
|
||||
|
||||
type Props = {
|
||||
guildEnumeration: GuildEnumeration;
|
||||
guilds: GuildSlug[];
|
||||
};
|
||||
|
||||
const tooltipId = 'guildnav';
|
||||
|
||||
const Badges = (props: { guild: PresentableGuild }) => {
|
||||
const Badges = (props: { guild: GuildSlug }) => {
|
||||
return React.useMemo(() => {
|
||||
if (!props.guild.member) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const roles = props.guild.member.rolesList
|
||||
.map((id) => {
|
||||
if (!props.guild.roles) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return props.guild.roles.rolesList.find((role) => role.id === id);
|
||||
})
|
||||
.filter((x) => !!x) as Role[];
|
||||
|
||||
if (hasPermission(roles, permissions.ADMINISTRATOR)) {
|
||||
if (props.guild.permissionLevel === UserGuildPermissions.Admin) {
|
||||
return <GoStar data-tip="Administrator" data-for={tooltipId} />;
|
||||
}
|
||||
|
||||
if (hasPermission(roles, permissions.MANAGE_ROLES)) {
|
||||
if (props.guild.permissionLevel === UserGuildPermissions.Manager) {
|
||||
return <GoZap data-tip="Role Editor" data-for={tooltipId} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [props.guild]);
|
||||
}, [props.guild.permissionLevel]);
|
||||
};
|
||||
|
||||
export const GuildNav = (props: Props) => (
|
||||
<div>
|
||||
{sortBy(props.guildEnumeration.guildsList, 'id').map((guild) => (
|
||||
{sortBy(props.guilds, 'id').map((guild) => (
|
||||
<Link href={`/s/${guild.id}`} passHref>
|
||||
<GuildNavItem>
|
||||
<NavSlug guild={guild.guild || null} key={guild.id} />
|
||||
<NavSlug guild={guild || null} key={guild.id} />
|
||||
<Badges guild={guild} />
|
||||
</GuildNavItem>
|
||||
</Link>
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
import * as React from 'react';
|
||||
import { Guild } from 'roleypoly/common/types';
|
||||
import { GoOrganization } from 'react-icons/go';
|
||||
import { GuildSlug } from 'roleypoly/common/types';
|
||||
import { Avatar, utils } from 'roleypoly/design-system/atoms/avatar';
|
||||
import { SlugContainer, SlugName } from './NavSlug.styled';
|
||||
import { GoOrganization } from 'react-icons/go';
|
||||
|
||||
type Props = {
|
||||
guild: Guild | null;
|
||||
guild: GuildSlug | null;
|
||||
};
|
||||
|
||||
export const NavSlug = (props: Props) => (
|
||||
<SlugContainer>
|
||||
<Avatar src={props.guild?.icon} deliberatelyEmpty={!props.guild} size={35}>
|
||||
<Avatar
|
||||
src={
|
||||
(props.guild && utils.avatarHash(props.guild.id, props.guild.icon)) ||
|
||||
undefined
|
||||
}
|
||||
deliberatelyEmpty={!props.guild}
|
||||
size={35}
|
||||
>
|
||||
{props.guild ? utils.initialsFromName(props.guild.name) : <GoOrganization />}
|
||||
</Avatar>
|
||||
<SlugName>{props.guild?.name || <>Your Guilds</>}</SlugName>
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import * as React from 'react';
|
||||
import * as Masthead from 'roleypoly/design-system/organisms/masthead';
|
||||
import { RoleypolyUser, GuildEnumeration } from 'roleypoly/common/types';
|
||||
import { Footer } from 'roleypoly/design-system/molecules/footer';
|
||||
import { Content, GlobalStyles } from './AppShell.styled';
|
||||
import { GlobalStyleColors } from 'roleypoly/design-system/atoms/colors';
|
||||
import { Scrollbars } from 'react-custom-scrollbars';
|
||||
import { DiscordUser, GuildSlug } from 'roleypoly/common/types';
|
||||
import { GlobalStyleColors } from 'roleypoly/design-system/atoms/colors';
|
||||
import { Footer } from 'roleypoly/design-system/molecules/footer';
|
||||
import * as Masthead from 'roleypoly/design-system/organisms/masthead';
|
||||
import { Content, GlobalStyles } from './AppShell.styled';
|
||||
|
||||
type AppShellProps = {
|
||||
export type AppShellProps = {
|
||||
children: React.ReactNode;
|
||||
user: RoleypolyUser | null;
|
||||
user?: DiscordUser;
|
||||
showFooter?: boolean;
|
||||
small?: boolean;
|
||||
activeGuildId?: string | null;
|
||||
guildEnumeration?: GuildEnumeration;
|
||||
guilds?: GuildSlug[];
|
||||
};
|
||||
|
||||
export const AppShell = (props: AppShellProps) => (
|
||||
<>
|
||||
<GlobalStyles />
|
||||
<GlobalStyleColors />
|
||||
{props.user !== null ? (
|
||||
{props.user ? (
|
||||
<Masthead.Authed
|
||||
guildEnumeration={props.guildEnumeration || { guildsList: [] }}
|
||||
guilds={props.guilds || []}
|
||||
activeGuildId={props.activeGuildId || null}
|
||||
user={props.user}
|
||||
/>
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import Link from 'next/link';
|
||||
import * as React from 'react';
|
||||
import { GoOrganization } from 'react-icons/go';
|
||||
import { GuildEnumeration, RoleypolyUser } from 'roleypoly/common/types';
|
||||
import { Guilds } from 'roleypoly/backend-worker/utils/kv';
|
||||
import {
|
||||
DiscordUser,
|
||||
GuildEnumeration,
|
||||
GuildSlug,
|
||||
RoleypolyUser,
|
||||
} from 'roleypoly/common/types';
|
||||
import { guildEnum } from 'roleypoly/common/types/storyData';
|
||||
import { DynamicLogomark } from 'roleypoly/design-system/atoms/branding';
|
||||
import { Popover } from 'roleypoly/design-system/atoms/popover';
|
||||
|
@ -20,9 +26,9 @@ import {
|
|||
} from './Masthead.styled';
|
||||
|
||||
type Props = {
|
||||
user: RoleypolyUser;
|
||||
user?: DiscordUser;
|
||||
activeGuildId: string | null;
|
||||
guildEnumeration: GuildEnumeration;
|
||||
guilds: GuildSlug[];
|
||||
};
|
||||
|
||||
export const Authed = (props: Props) => {
|
||||
|
@ -47,9 +53,9 @@ export const Authed = (props: Props) => {
|
|||
>
|
||||
<NavSlug
|
||||
guild={
|
||||
guildEnum.guildsList.find(
|
||||
(g) => g.id === props.activeGuildId
|
||||
)?.guild || null
|
||||
props.guilds.find(
|
||||
(guild) => guild.id === props.activeGuildId
|
||||
) || null
|
||||
}
|
||||
/>
|
||||
</InteractionBase>
|
||||
|
@ -65,7 +71,7 @@ export const Authed = (props: Props) => {
|
|||
active={serverPopoverState}
|
||||
onExit={() => setServerPopoverState(false)}
|
||||
>
|
||||
<GuildNav guildEnumeration={props.guildEnumeration} />
|
||||
<GuildNav guilds={props.guilds} />
|
||||
</Popover>
|
||||
</MastheadLeft>
|
||||
<MastheadRight>
|
||||
|
@ -76,9 +82,7 @@ export const Authed = (props: Props) => {
|
|||
}}
|
||||
hide={!userPopoverState}
|
||||
>
|
||||
{props.user.discorduser && (
|
||||
<UserAvatarGroup user={props.user.discorduser} />
|
||||
)}
|
||||
{props.user && <UserAvatarGroup user={props.user} />}
|
||||
</InteractionBase>
|
||||
<Popover
|
||||
headContent={<></>}
|
||||
|
@ -87,9 +91,7 @@ export const Authed = (props: Props) => {
|
|||
active={userPopoverState}
|
||||
onExit={() => setUserPopoverState(false)}
|
||||
>
|
||||
{props.user.discorduser && (
|
||||
<UserPopover user={props.user.discorduser} />
|
||||
)}
|
||||
{props.user && <UserPopover user={props.user} />}
|
||||
</Popover>
|
||||
</MastheadRight>
|
||||
</MastheadAlignment>
|
||||
|
|
|
@ -6,7 +6,7 @@ import * as React from 'react';
|
|||
export type AuthLoginProps = PreauthProps;
|
||||
|
||||
export const AuthLogin = (props: AuthLoginProps) => (
|
||||
<AppShell showFooter user={null}>
|
||||
<AppShell showFooter user={undefined}>
|
||||
<Hero topSpacing={100} bottomSpacing={175}>
|
||||
<Preauth {...props} />
|
||||
</Hero>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import * as React from 'react';
|
||||
import { AppShell } from 'roleypoly/design-system/organisms/app-shell';
|
||||
import { HelpPageBase } from 'roleypoly/design-system/molecules/help-page-base';
|
||||
import { RoleypolyUser } from 'roleypoly/common/types';
|
||||
import { DiscordUser } from 'roleypoly/common/types';
|
||||
|
||||
type HelpPageProps = {
|
||||
user: RoleypolyUser | null;
|
||||
user: DiscordUser | null;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const HelpPageTemplate = (props: HelpPageProps) => (
|
||||
<AppShell user={props.user || null}>
|
||||
<AppShell user={props.user || undefined}>
|
||||
<HelpPageBase>{props.children}</HelpPageBase>
|
||||
</AppShell>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { AppShell } from 'roleypoly/design-system/organisms/app-shell';
|
|||
import { Landing } from 'roleypoly/design-system/organisms/landing';
|
||||
|
||||
export const LandingTemplate = () => (
|
||||
<AppShell showFooter user={null}>
|
||||
<AppShell showFooter>
|
||||
<Landing />
|
||||
</AppShell>
|
||||
);
|
||||
|
|
|
@ -6,11 +6,7 @@ import {
|
|||
RolePickerProps,
|
||||
} from 'roleypoly/design-system/organisms/role-picker';
|
||||
|
||||
export type RolePickerTemplateProps = RolePickerProps & {
|
||||
user: RoleypolyUser;
|
||||
guildEnumeration?: GuildEnumeration;
|
||||
activeGuildId?: string;
|
||||
};
|
||||
export type RolePickerTemplateProps = RolePickerProps & AppShellProps;
|
||||
|
||||
export const RolePickerTemplate = (props: RolePickerTemplateProps) => {
|
||||
const { user, ...pickerProps } = props;
|
||||
|
@ -18,7 +14,8 @@ export const RolePickerTemplate = (props: RolePickerTemplateProps) => {
|
|||
<AppShell
|
||||
guildEnumeration={props.guildEnumeration}
|
||||
activeGuildId={props.activeGuildId}
|
||||
user={user}
|
||||
user={user.discorduser}
|
||||
guilds={user.guilds}
|
||||
small
|
||||
>
|
||||
<RolePicker {...pickerProps} />
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
# Bot Join Bounce Service
|
||||
|
||||
This function sends a user to the necessary Discord.com Bot OAuth flow.
|
||||
|
||||
Two cases may be present:
|
||||
|
||||
- General: The user will be sent to allow any of their relevant servers to join
|
||||
- This flow would be triggered from a generalized button
|
||||
- Specific: The user will be sent to join one of their servers.
|
||||
- This flow would be triggered from server picker
|
|
@ -1,45 +0,0 @@
|
|||
package botjoin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"text/template"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
"github.com/roleypoly/roleypoly/src/common/faas"
|
||||
)
|
||||
|
||||
var (
|
||||
validGuildID = regexp.MustCompile(`^[0-9]+$`)
|
||||
redirectPathTemplate = template.Must(
|
||||
template.New("redirect").Parse(
|
||||
`https://discord.com/api/oauth2/authorize?client_id={{.ClientID}}&scope=bot&permissions={{.Permissions}}{{if .GuildID}}&guild_id={{.GuildID}}&disable_guild_select=true{{end}}`,
|
||||
),
|
||||
)
|
||||
clientID = common.Getenv("BOT_CLIENT_ID").String()
|
||||
)
|
||||
|
||||
type redirectPathData struct {
|
||||
ClientID string
|
||||
Permissions int
|
||||
GuildID string
|
||||
}
|
||||
|
||||
func BotJoin(rw http.ResponseWriter, r *http.Request) {
|
||||
guildID := r.URL.Query().Get("guild")
|
||||
if !validGuildID.MatchString(guildID) {
|
||||
guildID = ""
|
||||
}
|
||||
|
||||
pathData := redirectPathData{
|
||||
ClientID: clientID,
|
||||
Permissions: 268435456, // MANAGE_ROLES
|
||||
GuildID: guildID,
|
||||
}
|
||||
|
||||
pathBuffer := bytes.Buffer{}
|
||||
redirectPathTemplate.Execute(&pathBuffer, pathData)
|
||||
|
||||
faas.Bounce(rw, pathBuffer.String())
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package botjoin_test
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
botjoin "github.com/roleypoly/roleypoly/src/functions/bot-join"
|
||||
)
|
||||
|
||||
func TestGeneral(t *testing.T) {
|
||||
O := gomega.NewWithT(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "/bot-join", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
botjoin.BotJoin(resp, req)
|
||||
|
||||
result := resp.Result()
|
||||
O.Expect(result.StatusCode).Should(gomega.BeIdenticalTo(303))
|
||||
O.Expect(result.Header.Get("location")).ShouldNot(gomega.ContainSubstring("guild_id"))
|
||||
|
||||
}
|
||||
|
||||
func TestGeneralSpecific(t *testing.T) {
|
||||
O := gomega.NewWithT(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "/bot-join?guild=386659935687147521", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
botjoin.BotJoin(resp, req)
|
||||
|
||||
result := resp.Result()
|
||||
O.Expect(result.StatusCode).Should(gomega.BeIdenticalTo(303))
|
||||
O.Expect(result.Header.Get("location")).Should(gomega.ContainSubstring("guild_id=386659935687147521"))
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
package createsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/firestore"
|
||||
"k8s.io/klog"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
"github.com/roleypoly/roleypoly/src/common/types"
|
||||
)
|
||||
|
||||
var (
|
||||
firestoreClient *firestore.Client
|
||||
projectID = common.Getenv("GCP_PROJECT_ID").String()
|
||||
firestoreEndpoint = common.Getenv("FIRESTORE_ENDPOINT").String()
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error // shadow avoidance
|
||||
|
||||
ctx := context.Background()
|
||||
firestoreClient, err = firestore.NewClient(ctx, projectID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateSession(rw http.ResponseWriter, req *http.Request) {
|
||||
requestData := types.CreateSessionRequest{}
|
||||
json.NewDecoder(req.Body).Decode(&requestData)
|
||||
|
||||
klog.Info("Creating session...")
|
||||
|
||||
sessionData := types.SessionData{
|
||||
AccessTokens: requestData.AccessTokenResponse,
|
||||
Fingerprint: requestData.Fingerprint,
|
||||
UserData: types.UserData{},
|
||||
}
|
||||
|
||||
ctx, ctxCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer ctxCancel()
|
||||
|
||||
docRef, _, err := firestoreClient.Collection("sessions").Add(ctx, sessionData)
|
||||
if err != nil {
|
||||
rw.WriteHeader(500)
|
||||
klog.Error("error: create session in firestore: ", err)
|
||||
}
|
||||
|
||||
klog.Info("Created session ", docRef.ID)
|
||||
|
||||
responseData := types.CreateSessionResponse{
|
||||
SessionID: docRef.ID,
|
||||
}
|
||||
json.NewEncoder(rw).Encode(responseData)
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package helloworld
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func HelloWorld(rw http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintln(rw, "Hello "+req.RemoteAddr+"!")
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package loginbounce
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"github.com/segmentio/ksuid"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
"github.com/roleypoly/roleypoly/src/common/faas"
|
||||
)
|
||||
|
||||
var (
|
||||
redirectPathTemplate = template.Must(
|
||||
template.New("redirect").Parse(
|
||||
`https://discord.com/api/oauth2/authorize?client_id={{.ClientID}}&response_type=code&scope=identify%20guilds&redirect_uri={{urlquery .RedirectURI}}&state={{.State}}`,
|
||||
),
|
||||
)
|
||||
clientID = common.Getenv("BOT_CLIENT_ID").String()
|
||||
redirectURI = common.Getenv("OAUTH_REDIRECT_URI").String()
|
||||
)
|
||||
|
||||
type redirectPathData struct {
|
||||
ClientID string
|
||||
RedirectURI string
|
||||
State string
|
||||
}
|
||||
|
||||
func LoginBounce(rw http.ResponseWriter, r *http.Request) {
|
||||
faas.Stash(rw, r.URL.Query().Get("redirect_url"))
|
||||
|
||||
pathData := redirectPathData{
|
||||
ClientID: clientID,
|
||||
RedirectURI: redirectURI,
|
||||
State: ksuid.New().String(),
|
||||
}
|
||||
|
||||
pathBuffer := bytes.Buffer{}
|
||||
redirectPathTemplate.Execute(&pathBuffer, pathData)
|
||||
|
||||
faas.Bounce(rw, pathBuffer.String())
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package loginbounce_test
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
loginbounce "github.com/roleypoly/roleypoly/src/functions/login-bounce"
|
||||
)
|
||||
|
||||
func TestBounce(t *testing.T) {
|
||||
O := gomega.NewWithT(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "/login-bounce?redirect_url=https://localhost:6600/test", nil)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
loginbounce.LoginBounce(rw, req)
|
||||
|
||||
resp := rw.Result()
|
||||
|
||||
O.Expect(resp.StatusCode).Should(gomega.BeIdenticalTo(303))
|
||||
O.Expect(resp.Header.Get("location")).Should(gomega.ContainSubstring("identify,guild"))
|
||||
O.Expect(resp.Header.Get("set-cookie")).Should(gomega.ContainSubstring("https://localhost:6600/test"))
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package loginhandler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
"github.com/roleypoly/roleypoly/src/common/faas"
|
||||
"github.com/roleypoly/roleypoly/src/common/types"
|
||||
"github.com/segmentio/ksuid"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
var (
|
||||
// HTTPClient is an overridable HTTP client with a 15 second timeout
|
||||
HTTPClient = http.Client{Timeout: 15 * time.Second}
|
||||
uiPath = common.Getenv("UI_PUBLIC_URI").SafeURL()
|
||||
apiPath = common.Getenv("API_PUBLIC_URI").SafeURL()
|
||||
clientID = common.Getenv("BOT_CLIENT_ID").String()
|
||||
clientSecret = common.Getenv("BOT_CLIENT_SECRET").String()
|
||||
redirectURI = common.Getenv("OAUTH_REDIRECT_URI").String()
|
||||
)
|
||||
|
||||
func LoginHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
query := req.URL.Query()
|
||||
|
||||
state := query.Get("state")
|
||||
stateID, err := ksuid.Parse(state)
|
||||
if err != nil || stateID.IsNil() || stateID.Time().Add(5*time.Minute).Before(time.Now()) {
|
||||
faas.Bounce(rw, uiPath+"/machinery/error?error_code=auth_failure")
|
||||
return
|
||||
}
|
||||
|
||||
code := query.Get("code")
|
||||
|
||||
body := url.Values{}
|
||||
body.Set("client_id", clientID)
|
||||
body.Set("client_secret", clientSecret)
|
||||
body.Set("grant_type", "authorization_code")
|
||||
body.Set("code", code)
|
||||
body.Set("redirect_uri", redirectURI)
|
||||
body.Set("scope", "identify guilds")
|
||||
|
||||
response, err := HTTPClient.PostForm("https://discord.com/api/v8/oauth2/token", body)
|
||||
if err != nil {
|
||||
klog.Error("token fetch failed: ", err)
|
||||
faas.Bounce(rw, uiPath+"/machinery/error?error_code=auth_failure")
|
||||
return
|
||||
}
|
||||
|
||||
tokens := types.AccessTokenResponse{}
|
||||
json.NewDecoder(response.Body).Decode(&tokens)
|
||||
|
||||
sessionRequest := types.CreateSessionRequest{
|
||||
AccessTokenResponse: tokens,
|
||||
Fingerprint: faas.Fingerprint(req),
|
||||
}
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
json.NewEncoder(&buf).Encode(sessionRequest)
|
||||
|
||||
response, err = HTTPClient.Post(apiPath+"/create-session", "application/json", &buf)
|
||||
if err != nil {
|
||||
klog.Error("create session failed: ", err)
|
||||
faas.Bounce(rw, uiPath+"/machinery/error?error_code=auth_failure")
|
||||
return
|
||||
}
|
||||
|
||||
session := types.CreateSessionResponse{}
|
||||
json.NewDecoder(response.Body).Decode(&session)
|
||||
|
||||
faas.Bounce(rw, uiPath+"/machinery/new-session?session_id="+session.SessionID)
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package sessiondata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func SessionData(rw http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(rw, "Hello world!")
|
||||
}
|
|
@ -1,11 +1,36 @@
|
|||
import { AppProps } from 'next/app';
|
||||
import NextApp, { AppContext, AppProps } from 'next/app';
|
||||
import * as React from 'react';
|
||||
import { InjectTypekitFont } from 'roleypoly/design-system/atoms/fonts';
|
||||
import nookies from 'nookies';
|
||||
import { AuthProvider } from 'roleypoly/providers/auth/AuthContext';
|
||||
|
||||
const App = (props: AppProps) => (
|
||||
type Props = AppProps & {
|
||||
sessionKey: string | null;
|
||||
};
|
||||
|
||||
const App = (props: Props) => (
|
||||
<>
|
||||
<InjectTypekitFont />
|
||||
<props.Component {...props.pageProps} />
|
||||
<AuthProvider sessionKey={props.sessionKey}>
|
||||
<props.Component {...props.pageProps} />
|
||||
</AuthProvider>
|
||||
</>
|
||||
);
|
||||
export default App;
|
||||
|
||||
export const getInitialProps = async (context: AppContext) => {
|
||||
let sessionKey: string | null = null;
|
||||
|
||||
if (context.ctx.req) {
|
||||
const key = nookies.get(context.ctx)['rp_session_key'];
|
||||
if (key) {
|
||||
sessionKey = key;
|
||||
}
|
||||
} else {
|
||||
sessionKey = sessionStorage.getItem('session_key');
|
||||
}
|
||||
|
||||
const pageProps = await NextApp.getInitialProps(context);
|
||||
|
||||
return { ...pageProps, sessionKey };
|
||||
};
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { NextPageContext } from 'next';
|
||||
import * as React from 'react';
|
||||
import nookies from 'nookies';
|
||||
import { AppShell } from 'roleypoly/design-system/organisms/app-shell';
|
||||
import { Hero } from 'roleypoly/design-system/atoms/hero';
|
||||
import { AccentTitle } from 'roleypoly/design-system/atoms/typography';
|
||||
|
||||
type Props = {
|
||||
sessionID: string;
|
||||
|
@ -9,19 +13,33 @@ const NewSession = (props: Props) => {
|
|||
const { sessionID } = props;
|
||||
React.useEffect(() => {
|
||||
sessionStorage.setItem('session_key', sessionID);
|
||||
|
||||
location.href = '/';
|
||||
}, [sessionID]);
|
||||
|
||||
return <div>Logging you in...</div>;
|
||||
return (
|
||||
<AppShell>
|
||||
<Hero>
|
||||
<AccentTitle>Logging you in...</AccentTitle>
|
||||
</Hero>
|
||||
</AppShell>
|
||||
);
|
||||
};
|
||||
|
||||
NewSession.getInitialProps = (context: NextPageContext): Props => {
|
||||
const sessionID = context.query.session_id;
|
||||
export const getServerSideProps = (context: NextPageContext): { props: Props } => {
|
||||
const sessionID = context.query.session_id as string;
|
||||
if (!sessionID) {
|
||||
throw new Error("I shouldn't be here today.");
|
||||
}
|
||||
|
||||
return { sessionID: sessionID as string };
|
||||
nookies.set(context, 'rp_session_key', sessionID, {
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 60 * 6,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
return { props: { sessionID } };
|
||||
};
|
||||
|
||||
export default NewSession;
|
||||
|
|
40
src/providers/auth/AuthContext.tsx
Normal file
40
src/providers/auth/AuthContext.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import * as React from 'react';
|
||||
|
||||
type AuthContextType = {
|
||||
sessionKey: string | null;
|
||||
setSessionKey: (value: string | null) => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
sessionKey: string | null;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const AuthContext = React.createContext<AuthContextType>({
|
||||
sessionKey: null,
|
||||
setSessionKey: () => {},
|
||||
});
|
||||
|
||||
export const AuthProvider = (props: Props) => {
|
||||
const [sessionKey, setSessionKey] = React.useState(props.sessionKey);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ sessionKey, setSessionKey }}>
|
||||
{props.children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const authCtx = React.useContext(AuthContext);
|
||||
if (!authCtx) {
|
||||
throw new Error('useAuth used without AuthProvider');
|
||||
}
|
||||
|
||||
return authCtx;
|
||||
};
|
||||
|
||||
export const isAuthenticated = () => {
|
||||
const authCtx = useAuth();
|
||||
return authCtx.sessionKey !== null;
|
||||
};
|
1
src/providers/auth/index.ts
Normal file
1
src/providers/auth/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './AuthContext';
|
Loading…
Add table
Add a link
Reference in a new issue