feat: allow/denylist discord oauth

This commit is contained in:
diced
2025-06-07 11:36:23 -07:00
parent a1ee1178ae
commit fbf9e10e56
8 changed files with 63 additions and 24 deletions

View File

@@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `oauthDiscordWhitelistIds` on the `Zipline` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Zipline" DROP COLUMN "oauthDiscordWhitelistIds",
ADD COLUMN "oauthDiscordAllowedIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
ADD COLUMN "oauthDiscordDeniedIds" TEXT[] DEFAULT ARRAY[]::TEXT[];

View File

@@ -82,7 +82,8 @@ model Zipline {
oauthDiscordClientId String?
oauthDiscordClientSecret String?
oauthDiscordRedirectUri String?
oauthDiscordWhitelistIds String[] @default([])
oauthDiscordAllowedIds String[] @default([])
oauthDiscordDeniedIds String[] @default([])
oauthGoogleClientId String?
oauthGoogleClientSecret String?

View File

@@ -30,7 +30,8 @@ export default function Oauth({
oauthDiscordClientId: '',
oauthDiscordClientSecret: '',
oauthDiscordRedirectUri: '',
oauthDiscordWhitelistIds: '',
oauthDiscordAllowedIds: '',
oauthDiscordDeniedIds: '',
oauthGoogleClientId: '',
oauthGoogleClientSecret: '',
@@ -54,7 +55,14 @@ export default function Oauth({
const onSubmit = async (values: typeof form.values) => {
for (const key in values) {
if (!['oauthBypassLocalLogin', 'oauthLoginOnly', 'oauthDiscordWhitelistIds'].includes(key)) {
if (
![
'oauthBypassLocalLogin',
'oauthLoginOnly',
'oauthDiscordAllowedIds',
'oauthDiscordDeniedIds',
].includes(key)
) {
if ((values[key as keyof typeof form.values] as string)?.trim() === '') {
// @ts-ignore
values[key as keyof typeof form.values] = null;
@@ -66,11 +74,11 @@ export default function Oauth({
}
}
if (key === 'oauthDiscordWhitelistIds') {
if (Array.isArray(values['oauthDiscordWhitelistIds'])) continue;
if (key === 'oauthDiscordAllowedIds' || key === 'oauthDiscordDeniedIds') {
if (Array.isArray(values[key])) continue;
// @ts-ignore
values['oauthDiscordWhitelistIds'] = (values['oauthDiscordWhitelistIds'] as string)
values[key] = (values[key] as string)
.split(',')
.map((id) => id.trim())
.filter((id) => id !== '');
@@ -90,8 +98,11 @@ export default function Oauth({
oauthDiscordClientId: data.settings.oauthDiscordClientId ?? '',
oauthDiscordClientSecret: data.settings.oauthDiscordClientSecret ?? '',
oauthDiscordRedirectUri: data.settings.oauthDiscordRedirectUri ?? '',
oauthDiscordWhitelistIds: data.settings.oauthDiscordWhitelistIds
? data.settings.oauthDiscordWhitelistIds.join(', ')
oauthDiscordAllowedIds: data.settings.oauthDiscordAllowedIds
? data.settings.oauthDiscordAllowedIds.join(', ')
: '',
oauthDiscordDeniedIds: data.settings.oauthDiscordDeniedIds
? data.settings.oauthDiscordDeniedIds.join(', ')
: '',
oauthGoogleClientId: data.settings.oauthGoogleClientId ?? '',
@@ -147,9 +158,14 @@ export default function Oauth({
<TextInput label='Discord Client ID' {...form.getInputProps('oauthDiscordClientId')} />
<TextInput label='Discord Client Secret' {...form.getInputProps('oauthDiscordClientSecret')} />
<TextInput
label='Discord Whitelist IDs'
description='A comma-separated list of Discord user IDs that are allowed to log in. Leave empty to allow all users.'
{...form.getInputProps('oauthDiscordWhitelistIds')}
label='Discord Allowed IDs'
description='A comma-separated list of Discord user IDs that are allowed to log in. Leave empty to disable allow list.'
{...form.getInputProps('oauthDiscordAllowedIds')}
/>
<TextInput
label='Discord Denied IDs'
description='A comma-separated list of Discord user IDs that are denied from logging in. Leave empty to disable deny list.'
{...form.getInputProps('oauthDiscordDeniedIds')}
/>
<TextInput
label='Discord Redirect URL'

View File

@@ -70,7 +70,8 @@ export const DATABASE_TO_PROP = {
oauthDiscordClientId: 'oauth.discord.clientId',
oauthDiscordClientSecret: 'oauth.discord.clientSecret',
oauthDiscordRedirectUri: 'oauth.discord.redirectUri',
oauthDiscordWhitelistIds: 'oauth.discord.whitelistIds',
oauthDiscordAllowedIds: 'oauth.discord.allowedIds',
oauthDiscordDeniedIds: 'oauth.discord.deniedIds',
oauthGoogleClientId: 'oauth.google.clientId',
oauthGoogleClientSecret: 'oauth.google.clientSecret',

View File

@@ -95,7 +95,8 @@ export const ENVS = [
env('oauth.discord.clientId', 'OAUTH_DISCORD_CLIENT_ID', 'string', true),
env('oauth.discord.clientSecret', 'OAUTH_DISCORD_CLIENT_SECRET', 'string', true),
env('oauth.discord.redirectUri', 'OAUTH_DISCORD_REDIRECT_URI', 'string', true),
env('oauth.discord.whitelistIds', 'OAUTH_DISCORD_WHITELIST_IDS', 'string[]', true),
env('oauth.discord.allowedIds', 'OAUTH_DISCORD_ALLOWED_IDS', 'string[]', true),
env('oauth.discord.deniedIds', 'OAUTH_DISCORD_DENIED_IDS', 'string[]', true),
env('oauth.google.clientId', 'OAUTH_GOOGLE_CLIENT_ID', 'string', true),
env('oauth.google.clientSecret', 'OAUTH_GOOGLE_CLIENT_SECRET', 'string', true),

View File

@@ -222,14 +222,16 @@ export const schema = z.object({
clientId: z.string(),
clientSecret: z.string(),
redirectUri: z.string().url().nullable().default(null),
whitelistIds: z.array(z.string()).default([]),
allowedIds: z.array(z.string()).default([]),
deniedIds: z.array(z.string()).default([]),
})
.or(
z.object({
clientId: z.undefined(),
clientSecret: z.undefined(),
redirectUri: z.undefined(),
whitelistIds: z.undefined().or(z.array(z.string()).default([])),
allowedIds: z.undefined().or(z.array(z.string()).default([])),
deniedIds: z.undefined().or(z.array(z.string()).default([])),
}),
),
github: z

View File

@@ -80,14 +80,12 @@ async function discordOauth({ code, host, state }: OAuthQuery, logger: Logger):
logger.debug('user', { '@me': userJson });
if (config.oauth.discord.whitelistIds?.length) {
if (!config.oauth.discord.whitelistIds.includes(userJson.id)) {
logger.warn('Discord user not whitelisted', { userId: userJson.id });
return {
error: 'You are not whitelisted to use Discord OAuth',
error_code: 403,
};
}
// handle config.oauth.discord.allowedIds and config.oauth.discord.deniedIds
if (config.oauth.discord.allowedIds && !config.oauth.discord.allowedIds.includes(userJson.id)) {
return { error: 'You are not allowed to log in with Discord.' };
}
if (config.oauth.discord.deniedIds && config.oauth.discord.deniedIds.includes(userJson.id)) {
return { error: 'You are not allowed to log in with Discord.' };
}
const avatar = userJson.avatar

View File

@@ -222,7 +222,17 @@ export default fastifyPlugin(
oauthDiscordClientId: z.string().nullable(),
oauthDiscordClientSecret: z.string().nullable(),
oauthDiscordRedirectUri: z.string().url().endsWith('/api/auth/oauth/discord').nullable(),
oauthDiscordWhitelistIds: z
oauthDiscordAllowedIds: z
.union([
z.array(z.string().refine((s) => /^\d+$/.test(s), 'Discord ID must be a number')),
z
.string()
.refine((s) => s === '' || /^\d+(,\d+)*$/.test(s), 'Discord IDs must be comma-separated'),
])
.transform((value) =>
typeof value === 'string' ? value.split(',').map((id) => id.trim()) : value,
),
oauthDiscordDeniedIds: z
.union([
z.array(z.string().refine((s) => /^\d+$/.test(s), 'Discord ID must be a number')),
z