fix: new oauth stuff (#240)

* - fix: use oauth's user id instead of username
 - feat: add login only config for oauth

* Addresses tomato's concerns

* fix: catch same account on different user
This commit is contained in:
TacticalCoderJay 2022-12-06 21:40:13 -08:00 committed by GitHub
parent 9f797613d2
commit ea1a0b7fc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 71 additions and 30 deletions

View file

@ -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");

View file

@ -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 {

View file

@ -102,6 +102,7 @@ export interface ConfigFeatures {
invites_length: number;
oauth_registration: boolean;
oauth_login_only: boolean;
user_registration: boolean;
headless: boolean;

View file

@ -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'),

View file

@ -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,
}),

View file

@ -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,

View file

@ -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,

View file

@ -56,6 +56,7 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAu
return {
username: userJson.login,
user_id: userJson.id.toString(),
avatar: avatarBase64,
access_token: json.access_token,
};

View file

@ -61,6 +61,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
return {
username: userJson.names[0].displayName,
user_id: userJson.resourceName.split('/')[1],
avatar: avatarBase64,
access_token: json.access_token,
refresh_token: json.refresh_token,