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:
parent
9f797613d2
commit
ea1a0b7fc8
9 changed files with 71 additions and 30 deletions
|
@ -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");
|
|
@ -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 {
|
||||
|
|
|
@ -102,6 +102,7 @@ export interface ConfigFeatures {
|
|||
invites_length: number;
|
||||
|
||||
oauth_registration: boolean;
|
||||
oauth_login_only: boolean;
|
||||
user_registration: boolean;
|
||||
|
||||
headless: boolean;
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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_oauthId: {
|
||||
provider: provider.toUpperCase() as OauthProviders,
|
||||
oauthId: oauth_resp.user_id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: oauth_resp.username,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
oauth: true,
|
||||
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,6 +86,7 @@ export const withOAuth =
|
|||
return oauthError(`This account was already linked with ${provider}!`);
|
||||
|
||||
logger.debug(`attempting to link ${provider} account to ${user.username}`);
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
|
@ -92,11 +98,18 @@ 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,
|
||||
},
|
||||
});
|
||||
} 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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue