Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions app/components/login.tsx

This file was deleted.

18 changes: 0 additions & 18 deletions app/components/logout.tsx

This file was deleted.

12 changes: 1 addition & 11 deletions app/features/ServerOverview/ServerOverview.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
import { Logout } from "#~/basics/logout";

export function ServerOverview() {
return (
<main className="">
<nav>
<Logout />
</nav>
<section></section>
<footer></footer>
</main>
);
return <div>butts (ServerOverview)</div>;
}
5 changes: 5 additions & 0 deletions app/helpers/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ export const partition = <Data>(predicate: (d: Data) => boolean, xs: Data[]) =>
},
[[], []] as [Data[], Data[]],
);

/**
* ElementType is a helper type to turn `Array<T>` into `T`
*/
export type ElementType<T> = T extends (infer U)[] ? U : T;
36 changes: 36 additions & 0 deletions app/helpers/sets.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,38 @@
export const difference = <T>(a: Set<T>, b: Set<T>) =>
new Set(Array.from(a).filter((x) => !b.has(x)));

/**
* Returns the intersection of two sets - elements that exist in both sets
* @param setA First set
* @param setB Second set
* @returns A new Set containing elements present in both input sets
*/
export function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
const result = new Set<T>();

for (const elem of setA) {
if (setB.has(elem)) {
result.add(elem);
}
}

return result;
}

/**
* Returns the complement (difference) of two sets - elements in setA that are not in setB
* @param setA First set
* @param setB Second set
* @returns A new Set containing elements present in setA but not in setB
*/
export function complement<T>(setA: Set<T>, setB: Set<T>): Set<T> {
const result = new Set<T>();

for (const elem of setA) {
if (!setB.has(elem)) {
result.add(elem);
}
}

return result;
}
110 changes: 109 additions & 1 deletion app/models/discord.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import type { GuildMember } from "discord.js";
import {
Routes,
type APIGuild,
PermissionFlagsBits,
} from "discord-api-types/v10";

import type { REST } from "@discordjs/rest";
import { type GuildMember } from "discord.js";

import { complement, intersection } from "#~/helpers/sets.js";

import type { AccessToken } from "simple-oauth2";
import { fetchSettings, SETTINGS } from "#~/models/guilds.server";

Expand Down Expand Up @@ -88,3 +98,101 @@ export const timeout = async (member: GuildMember | null) => {
}
return member.timeout(OVERNIGHT);
};

const authzRoles = {
mod: "MOD",
admin: "ADMIN",
manager: "MANAGER",
manageChannels: "MANAGE_CHANNELS",
manageGuild: "MANAGE_GUILD",
manageRoles: "MANAGE_ROLES",
} as const;

const isUndefined = (x: unknown): x is undefined => typeof x === "undefined";

const processGuild = (g: APIGuild) => {
const perms = BigInt(g.permissions || 0);
const authz = new Set<(typeof authzRoles)[keyof typeof authzRoles]>();

if (perms & PermissionFlagsBits.Administrator) {
authz.add(authzRoles.admin);
}
if (perms & PermissionFlagsBits.ModerateMembers) {
authz.add(authzRoles.mod);
}
if (perms & PermissionFlagsBits.ManageChannels) {
authz.add(authzRoles.manageChannels);
authz.add(authzRoles.manager);
}
if (perms & PermissionFlagsBits.ManageGuild) {
authz.add(authzRoles.manageGuild);
authz.add(authzRoles.manager);
}
if (perms & PermissionFlagsBits.ManageRoles) {
authz.add(authzRoles.manageRoles);
authz.add(authzRoles.manager);
}

return {
id: g.id as string,
icon: g.icon,
name: g.name as string,
authz: [...authz.values()],
};
};

export interface Guild extends ReturnType<typeof processGuild> {
hasBot: boolean;
}

export const fetchGuilds = async (
userRest: REST,
botRest: REST,
): Promise<Guild[]> => {
const [rawUserGuilds, rawBotGuilds] = (await Promise.all([
userRest.get(Routes.userGuilds()),
botRest.get(Routes.userGuilds()),
])) as [APIGuild[], APIGuild[]];

const botGuilds = new Map(
rawBotGuilds.reduce(
(accum, val) => {
const guild = processGuild(val);
if (guild.authz.length > 0) {
accum.push([val.id, guild]);
}
return accum;
},
[] as [string, Omit<Guild, "hasBot">][],
),
);
const userGuilds = new Map(
rawUserGuilds.reduce(
(accum, val) => {
const guild = processGuild(val);
if (guild.authz.includes("MANAGER")) {
accum.push([val.id, guild]);
}
return accum;
},
[] as [string, Omit<Guild, "hasBot">][],
),
);

const botGuildIds = new Set(botGuilds.keys());
const userGuildIds = new Set(userGuilds.keys());

const manageableGuilds = intersection(botGuildIds, userGuildIds);
const invitableGuilds = complement(userGuildIds, botGuildIds);

return [
...[...manageableGuilds].map((gId) => {
const guild = botGuilds.get(gId);
return guild ? { ...guild, hasBot: true } : undefined;
}),
...[...invitableGuilds].map((gId) => {
const guild = botGuilds.get(gId);
return guild ? { ...guild, hasBot: false } : undefined;
}),
].filter((g) => !isUndefined(g));
};
25 changes: 12 additions & 13 deletions app/models/session.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,18 @@ const {
export type DbSession = Awaited<ReturnType<typeof getDbSession>>;

export const CookieSessionKeys = {
userId: "userId",
discordToken: "discordToken",
authState: "state",
authRedirect: "redirectTo",
} as const;

export const DbSessionKeys = {
authState: "state",
authRedirect: "redirectTo",
userId: "userId",
} as const;

async function getUserId(request: Request): Promise<string | undefined> {
const session = await getCookieSession(request.headers.get("Cookie"));
const userId = session.get(CookieSessionKeys.userId);
const session = await getDbSession(request.headers.get("Cookie"));
const userId = session.get(DbSessionKeys.userId);
return userId;
}

Expand Down Expand Up @@ -162,14 +162,14 @@ export async function initOauthLogin({
redirectTo?: string;
}) {
const { origin } = new URL(request.url);
const dbSession = await getDbSession(request.headers.get("Cookie"));
const cookieSession = await getCookieSession(request.headers.get("Cookie"));

const state = randomUUID();
dbSession.set(DbSessionKeys.authState, state);
cookieSession.set(CookieSessionKeys.authState, state);
if (redirectTo) {
dbSession.set(DbSessionKeys.authRedirect, redirectTo);
cookieSession.set(CookieSessionKeys.authRedirect, redirectTo);
}
const cookie = await commitDbSession(dbSession, {
const cookie = await commitCookieSession(cookieSession, {
maxAge: 60 * 60 * 1, // 1 hour
});
return redirect(
Expand Down Expand Up @@ -221,17 +221,16 @@ export async function completeOauthLogin(
getDbSession(reqCookie),
]);

const dbState = dbSession.get(DbSessionKeys.authState);
const dbState = cookieSession.get(CookieSessionKeys.authState);
// Redirect to login if the state arg doesn't match
if (dbState !== state) {
console.error("DB state didn’t match cookie state");
throw redirect("/login");
}

cookieSession.set(CookieSessionKeys.userId, userId);
dbSession.set(DbSessionKeys.userId, userId);
// @ts-expect-error token.toJSON() isn't in the types but it works
cookieSession.set(CookieSessionKeys.discordToken, token.toJSON());
dbSession.unset(DbSessionKeys.authState);
const [cookie, dbCookie] = await Promise.all([
commitCookieSession(cookieSession, {
maxAge: 60 * 60 * 24 * 7, // 7 days
Expand All @@ -242,7 +241,7 @@ export async function completeOauthLogin(
headers.append("Set-Cookie", cookie);
headers.append("Set-Cookie", dbCookie);

return redirect(dbSession.get(DbSessionKeys.authRedirect) ?? "/", {
return redirect(cookieSession.get(CookieSessionKeys.authRedirect) ?? "/", {
headers,
});
}
Expand Down
2 changes: 1 addition & 1 deletion app/routes/__auth.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Route } from "./+types/__auth";
import { Outlet, useLocation, type LoaderFunctionArgs } from "react-router";
import { Login } from "#~/components/login";
import { Login } from "#~/basics/login";
import { isProd } from "#~/helpers/env.server";
import { getUser } from "#~/models/session.server";
import { useOptionalUser } from "#~/utils";
Expand Down
2 changes: 1 addition & 1 deletion app/routes/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Route } from "./+types/auth";
import { redirect } from "react-router";

import { initOauthLogin } from "#~/models/session.server";
import { Login } from "#~/components/login";
import { Login } from "#~/basics/login";

// eslint-disable-next-line no-empty-pattern
export async function loader({}: Route.LoaderArgs) {
Expand Down
Loading
Loading