211 lines
5.5 KiB
TypeScript
211 lines
5.5 KiB
TypeScript
import { forwardRef, Inject, Injectable, Logger } 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";
|
|
import { OAuthProvider } from "./provider/oauthProvider.interface";
|
|
|
|
@Injectable()
|
|
export class OAuthService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private config: ConfigService,
|
|
@Inject(forwardRef(() => AuthService)) private auth: AuthService,
|
|
@Inject("OAUTH_PLATFORMS") private platforms: string[],
|
|
@Inject("OAUTH_PROVIDERS")
|
|
private oAuthProviders: Record<string, OAuthProvider<unknown>>,
|
|
) {}
|
|
private readonly logger = new Logger(OAuthService.name);
|
|
|
|
available(): string[] {
|
|
return this.platforms
|
|
.map((platform) => [
|
|
platform,
|
|
this.config.get(`oauth.${platform}-enabled`),
|
|
])
|
|
.filter(([_, enabled]) => enabled)
|
|
.map(([platform, _]) => platform);
|
|
}
|
|
|
|
availableProviders(): Record<string, OAuthProvider<unknown>> {
|
|
return Object.fromEntries(
|
|
Object.entries(this.oAuthProviders)
|
|
.map(([providerName, provider]) => [
|
|
[providerName, provider],
|
|
this.config.get(`oauth.${providerName}-enabled`),
|
|
])
|
|
.filter(([_, enabled]) => enabled)
|
|
.map(([provider, _]) => provider),
|
|
);
|
|
}
|
|
|
|
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, ip: string) {
|
|
const oauthUser = await this.prisma.oAuthUser.findFirst({
|
|
where: {
|
|
provider: user.provider,
|
|
providerUserId: user.providerId,
|
|
},
|
|
});
|
|
if (oauthUser) {
|
|
await this.updateIsAdmin(user);
|
|
const updatedUser = await this.prisma.user.findFirst({
|
|
where: {
|
|
id: oauthUser.userId,
|
|
},
|
|
});
|
|
this.logger.log(`Successful login for user ${user.email} from IP ${ip}`);
|
|
return this.auth.generateToken(updatedUser, { idToken: user.idToken });
|
|
}
|
|
|
|
return this.signUp(user, ip);
|
|
}
|
|
|
|
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(preferredUsername: string) {
|
|
// Only keep letters, numbers, dots, and underscores. Truncate to 20 characters.
|
|
let username = preferredUsername
|
|
.replace(/[^a-zA-Z0-9._]/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, ip: string) {
|
|
// 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,
|
|
},
|
|
});
|
|
await this.updateIsAdmin(user);
|
|
return this.auth.generateToken(existingUser, { idToken: user.idToken });
|
|
}
|
|
|
|
const result = await this.auth.signUp(
|
|
{
|
|
email: user.email,
|
|
username: await this.getAvailableUsername(user.providerUsername),
|
|
password: null,
|
|
},
|
|
ip,
|
|
user.isAdmin,
|
|
);
|
|
|
|
await this.prisma.oAuthUser.create({
|
|
data: {
|
|
provider: user.provider,
|
|
providerUserId: user.providerId.toString(),
|
|
providerUsername: user.providerUsername,
|
|
userId: result.user.id,
|
|
},
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
private async updateIsAdmin(user: OAuthSignInDto) {
|
|
if ("isAdmin" in user)
|
|
await this.prisma.user.update({
|
|
where: {
|
|
email: user.email,
|
|
},
|
|
data: {
|
|
isAdmin: user.isAdmin,
|
|
},
|
|
});
|
|
}
|
|
}
|