* feat(auth): add OAuth2 login with GitHub and Google * chore(translations): add files for Japanese * fix(auth): fix link function for GitHub * feat(oauth): basic oidc implementation * feat(oauth): oauth guard * fix: disable image optimizations for logo to prevent caching issues with custom logos * fix: memory leak while downloading large files * chore(translations): update translations via Crowdin (#278) * New translations en-us.ts (Japanese) * New translations en-us.ts (Japanese) * New translations en-us.ts (Japanese) * release: 0.18.2 * doc(translations): Add Japanese README (#279) * Added Japanese README. * Added JAPANESE README link to README.md. * Updated Japanese README. * Updated Environment Variable Table. * updated zh-cn README. * feat(oauth): unlink account * refactor(oauth): make providers extensible * fix(oauth): fix discoveryUri error when toggle google-enabled * feat(oauth): add microsoft and discord as oauth provider * docs(oauth): update README.md * docs(oauth): update oauth2-guide.md * set password to null for new oauth users * New translations en-us.ts (Japanese) (#281) * chore(translations): add Polish files * fix(oauth): fix random username and password * feat(oauth): add totp * fix(oauth): fix totp throttle * fix(oauth): fix qrcode and remove comment * feat(oauth): add error page * fix(oauth): i18n of error page * feat(auth): add OAuth2 login * fix(auth): fix link function for GitHub * feat(oauth): basic oidc implementation * feat(oauth): oauth guard * feat(oauth): unlink account * refactor(oauth): make providers extensible * fix(oauth): fix discoveryUri error when toggle google-enabled * feat(oauth): add microsoft and discord as oauth provider * docs(oauth): update README.md * docs(oauth): update oauth2-guide.md * set password to null for new oauth users * fix(oauth): fix random username and password * feat(oauth): add totp * fix(oauth): fix totp throttle * fix(oauth): fix qrcode and remove comment * feat(oauth): add error page * fix(oauth): i18n of error page * refactor: return null instead of `false` in `getIdOfCurrentUser` functiom * feat: show original oauth error if available * refactor: run formatter * refactor(oauth): error message i18n * refactor(oauth): make OAuth token available someone may use it (to revoke token or get other info etc.) also improved the i18n message * chore(oauth): remove unused import * chore: add database migration * fix: missing python installation for nanoid --------- Co-authored-by: Elias Schneider <login@eliasschneider.com> Co-authored-by: ふうせん <10260662+fusengum@users.noreply.github.com>
172 lines
4.2 KiB
TypeScript
172 lines
4.2 KiB
TypeScript
import { Inject, Injectable } from "@nestjs/common";
|
|
import { User } from "@prisma/client";
|
|
import { nanoid } from "nanoid";
|
|
import { AuthService } from "../auth/auth.service";
|
|
import { ConfigService } from "../config/config.service";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import { OAuthSignInDto } from "./dto/oauthSignIn.dto";
|
|
import { ErrorPageException } from "./exceptions/errorPage.exception";
|
|
|
|
@Injectable()
|
|
export class OAuthService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private config: ConfigService,
|
|
private auth: AuthService,
|
|
@Inject("OAUTH_PLATFORMS") private platforms: string[],
|
|
) {}
|
|
|
|
available(): string[] {
|
|
return this.platforms
|
|
.map((platform) => [
|
|
platform,
|
|
this.config.get(`oauth.${platform}-enabled`),
|
|
])
|
|
.filter(([_, enabled]) => enabled)
|
|
.map(([platform, _]) => platform);
|
|
}
|
|
|
|
async status(user: User) {
|
|
const oauthUsers = await this.prisma.oAuthUser.findMany({
|
|
select: {
|
|
provider: true,
|
|
providerUsername: true,
|
|
},
|
|
where: {
|
|
userId: user.id,
|
|
},
|
|
});
|
|
return Object.fromEntries(oauthUsers.map((u) => [u.provider, u]));
|
|
}
|
|
|
|
async signIn(user: OAuthSignInDto) {
|
|
const oauthUser = await this.prisma.oAuthUser.findFirst({
|
|
where: {
|
|
provider: user.provider,
|
|
providerUserId: user.providerId,
|
|
},
|
|
include: {
|
|
user: true,
|
|
},
|
|
});
|
|
if (oauthUser) {
|
|
return this.auth.generateToken(oauthUser.user, true);
|
|
}
|
|
|
|
return this.signUp(user);
|
|
}
|
|
|
|
async link(
|
|
userId: string,
|
|
provider: string,
|
|
providerUserId: string,
|
|
providerUsername: string,
|
|
) {
|
|
const oauthUser = await this.prisma.oAuthUser.findFirst({
|
|
where: {
|
|
provider,
|
|
providerUserId,
|
|
},
|
|
});
|
|
if (oauthUser) {
|
|
throw new ErrorPageException("already_linked", "/account", [
|
|
`provider_${provider}`,
|
|
]);
|
|
}
|
|
|
|
await this.prisma.oAuthUser.create({
|
|
data: {
|
|
userId,
|
|
provider,
|
|
providerUsername,
|
|
providerUserId,
|
|
},
|
|
});
|
|
}
|
|
|
|
async unlink(user: User, provider: string) {
|
|
const oauthUser = await this.prisma.oAuthUser.findFirst({
|
|
where: {
|
|
userId: user.id,
|
|
provider,
|
|
},
|
|
});
|
|
if (oauthUser) {
|
|
await this.prisma.oAuthUser.delete({
|
|
where: {
|
|
id: oauthUser.id,
|
|
},
|
|
});
|
|
} else {
|
|
throw new ErrorPageException("not_linked", "/account", [provider]);
|
|
}
|
|
}
|
|
|
|
private async getAvailableUsername(email: string) {
|
|
// only remove + and - from email for now (maybe not enough)
|
|
let username = email.split("@")[0].replace(/[+-]/g, "").substring(0, 20);
|
|
while (true) {
|
|
const user = await this.prisma.user.findFirst({
|
|
where: {
|
|
username: username,
|
|
},
|
|
});
|
|
if (user) {
|
|
username = username + "_" + nanoid(10).replaceAll("-", "");
|
|
} else {
|
|
return username;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async signUp(user: OAuthSignInDto) {
|
|
// register
|
|
if (!this.config.get("oauth.allowRegistration")) {
|
|
throw new ErrorPageException("no_user", "/auth/signIn", [
|
|
`provider_${user.provider}`,
|
|
]);
|
|
}
|
|
|
|
if (!user.email) {
|
|
throw new ErrorPageException("no_email", "/auth/signIn", [
|
|
`provider_${user.provider}`,
|
|
]);
|
|
}
|
|
|
|
const existingUser: User = await this.prisma.user.findFirst({
|
|
where: {
|
|
email: user.email,
|
|
},
|
|
});
|
|
|
|
if (existingUser) {
|
|
await this.prisma.oAuthUser.create({
|
|
data: {
|
|
provider: user.provider,
|
|
providerUserId: user.providerId.toString(),
|
|
providerUsername: user.providerUsername,
|
|
userId: existingUser.id,
|
|
},
|
|
});
|
|
return this.auth.generateToken(existingUser, true);
|
|
}
|
|
|
|
const result = await this.auth.signUp({
|
|
email: user.email,
|
|
username: await this.getAvailableUsername(user.email),
|
|
password: null,
|
|
});
|
|
|
|
await this.prisma.oAuthUser.create({
|
|
data: {
|
|
provider: user.provider,
|
|
providerUserId: user.providerId.toString(),
|
|
providerUsername: user.providerUsername,
|
|
userId: result.user.id,
|
|
},
|
|
});
|
|
|
|
return result;
|
|
}
|
|
}
|