diff --git a/prisma/migrations/20221207035224_add_oauth_user_id/migration.sql b/prisma/migrations/20221207035224_add_oauth_user_id/migration.sql new file mode 100644 index 0000000..2cc9785 --- /dev/null +++ b/prisma/migrations/20221207035224_add_oauth_user_id/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[provider,oauthId]` on the table `OAuth` will be added. If there are existing duplicate values, this will fail. + - Added the required column `oauthId` to the `OAuth` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "OAuth" ADD COLUMN "oauthId" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "OAuth_provider_oauthId_key" ON "OAuth"("provider", "oauthId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 668af1a..f4a4863 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -100,8 +100,11 @@ model OAuth { user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int username String + oauthId String token String refresh String? + + @@unique([provider, oauthId]) } enum OauthProviders { diff --git a/src/lib/config/Config.ts b/src/lib/config/Config.ts index 2f92be8..8cedd10 100644 --- a/src/lib/config/Config.ts +++ b/src/lib/config/Config.ts @@ -102,6 +102,7 @@ export interface ConfigFeatures { invites_length: number; oauth_registration: boolean; + oauth_login_only: boolean; user_registration: boolean; headless: boolean; diff --git a/src/lib/config/readConfig.ts b/src/lib/config/readConfig.ts index 9b72abd..9721276 100644 --- a/src/lib/config/readConfig.ts +++ b/src/lib/config/readConfig.ts @@ -137,6 +137,7 @@ export default function readConfig() { map('FEATURES_INVITES_LENGTH', 'number', 'features.invites_length'), map('FEATURES_OAUTH_REGISTRATION', 'boolean', 'features.oauth_registration'), + map('FEATURES_OAUTH_LOGIN_ONLY', 'boolean', 'features.oauth_login_only'), map('FEATURES_USER_REGISTRATION', 'boolean', 'features.user_registration'), map('FEATURES_HEADLESS', 'boolean', 'features.headless'), diff --git a/src/lib/config/validateConfig.ts b/src/lib/config/validateConfig.ts index fe7b522..d0a8baa 100644 --- a/src/lib/config/validateConfig.ts +++ b/src/lib/config/validateConfig.ts @@ -167,6 +167,7 @@ const validator = s.object({ invites: s.boolean.default(false), invites_length: s.number.default(6), oauth_registration: s.boolean.default(false), + oauth_login_only: s.boolean.default(false), user_registration: s.boolean.default(false), headless: s.boolean.default(false), }) @@ -174,6 +175,7 @@ const validator = s.object({ invites: false, invites_length: 6, oauth_registration: false, + oauth_login_only: false, user_registration: false, headless: false, }), diff --git a/src/lib/middleware/withOAuth.ts b/src/lib/middleware/withOAuth.ts index 8d8d344..76d96c5 100644 --- a/src/lib/middleware/withOAuth.ts +++ b/src/lib/middleware/withOAuth.ts @@ -13,6 +13,7 @@ export interface OAuthQuery { export interface OAuthResponse { username?: string; + user_id?: string; access_token?: string; refresh_token?: string; avatar?: string; @@ -55,23 +56,27 @@ export const withOAuth = const { state } = req.query as { state?: string }; - const existing = await prisma.user.findFirst({ + const existingOauth = await prisma.oAuth.findUnique({ where: { - oauth: { - some: { - provider: provider.toUpperCase() as OauthProviders, - username: oauth_resp.username, - }, + provider_oauthId: { + provider: provider.toUpperCase() as OauthProviders, + oauthId: oauth_resp.user_id, }, }, - include: { - oauth: true, + }); + + const existingUser = await prisma.user.findFirst({ + where: { + username: oauth_resp.username, + }, + select: { + username: true, + id: true, }, }); const user = await req.user(); - const existingOauth = existing?.oauth?.find((o) => o.provider === provider.toUpperCase()); const userOauth = user?.oauth?.find((o) => o.provider === provider.toUpperCase()); if (state === 'link') { @@ -81,22 +86,30 @@ export const withOAuth = return oauthError(`This account was already linked with ${provider}!`); logger.debug(`attempting to link ${provider} account to ${user.username}`); - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - oauth: { - create: { - provider: OauthProviders[provider.toUpperCase()], - token: oauth_resp.access_token, - refresh: oauth_resp.refresh_token || null, - username: oauth_resp.username, - }, + try { + await prisma.user.update({ + where: { + id: user.id, }, - avatar: oauth_resp.avatar, - }, - }); + data: { + oauth: { + create: { + provider: OauthProviders[provider.toUpperCase()], + token: oauth_resp.access_token, + refresh: oauth_resp.refresh_token || null, + username: oauth_resp.username, + oauthId: oauth_resp.user_id, + }, + }, + avatar: oauth_resp.avatar, + }, + }); + } catch (e) { + if (e.code === 'P2002') { + logger.debug(`account already linked with ${provider}`); + return oauthError('This account is already linked with another user.'); + } else throw e; + } res.setUserCookie(user.id); logger.info(`User ${user.username} (${user.id}) linked account via oauth(${provider})`); @@ -112,6 +125,7 @@ export const withOAuth = token: oauth_resp.access_token, refresh: oauth_resp.refresh_token || null, username: oauth_resp.username, + oauthId: oauth_resp.user_id, }, }); @@ -119,7 +133,7 @@ export const withOAuth = logger.info(`User ${user.username} (${user.id}) logged in via oauth(${provider})`); return res.redirect('/dashboard'); - } else if (existing && existingOauth) { + } else if (existingOauth) { await prisma.oAuth.update({ where: { id: existingOauth!.id, @@ -128,16 +142,20 @@ export const withOAuth = token: oauth_resp.access_token, refresh: oauth_resp.refresh_token || null, username: oauth_resp.username, + oauthId: oauth_resp.user_id, }, }); - res.setUserCookie(existing.id); - Logger.get('user').info(`User ${existing.username} (${existing.id}) logged in via oauth(${provider})`); + res.setUserCookie(existingOauth.userId); + Logger.get('user').info( + `User ${existingOauth.username} (${existingOauth.id}) logged in via oauth(${provider})` + ); return res.redirect('/dashboard'); - } else if (existing) { - return oauthError(`Username "${oauth_resp.username}" is already taken, unable to create account.`); - } + } else if (config.features.oauth_login_only) { + return oauthError('Login only mode is enabled, unable to create account.'); + } else if (existingUser) + return oauthError(`Username ${oauth_resp.username} is already taken, unable to create account.`); logger.debug('creating new user via oauth'); const nuser = await prisma.user.create({ @@ -150,6 +168,7 @@ export const withOAuth = token: oauth_resp.access_token, refresh: oauth_resp.refresh_token || null, username: oauth_resp.username, + oauthId: oauth_resp.user_id, }, }, avatar: oauth_resp.avatar, diff --git a/src/pages/api/auth/oauth/discord.ts b/src/pages/api/auth/oauth/discord.ts index a05c51f..8a19b0d 100644 --- a/src/pages/api/auth/oauth/discord.ts +++ b/src/pages/api/auth/oauth/discord.ts @@ -69,6 +69,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi return { username: userJson.username, + user_id: userJson.id, avatar: avatarBase64, access_token: json.access_token, refresh_token: json.refresh_token, diff --git a/src/pages/api/auth/oauth/github.ts b/src/pages/api/auth/oauth/github.ts index 41f06d7..ec172ca 100644 --- a/src/pages/api/auth/oauth/github.ts +++ b/src/pages/api/auth/oauth/github.ts @@ -56,6 +56,7 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise