2020-09-05 23:35:53 +02:00
|
|
|
import { ForbiddenException, Injectable } from "@nestjs/common";
|
2020-05-03 20:18:57 +02:00
|
|
|
import { ConfigService } from "@nestjs/config";
|
|
|
|
|
import { JwtService } from "@nestjs/jwt";
|
2023-02-20 23:50:57 +01:00
|
|
|
import { randomBytes } from "crypto";
|
2020-02-01 16:11:48 +01:00
|
|
|
import { User } from "../users/user.entity";
|
|
|
|
|
import { UsersService } from "../users/users.service";
|
2023-02-19 16:16:34 +01:00
|
|
|
import { ApiToken } from "./api-token.entity";
|
|
|
|
|
import { ApiTokenRepository } from "./api-token.repository";
|
2020-09-05 23:35:53 +02:00
|
|
|
import { AuthSession } from "./auth-session.entity";
|
|
|
|
|
import { AuthSessionRepository } from "./auth-session.repository";
|
2020-02-01 16:11:48 +01:00
|
|
|
import { LoginDto } from "./dto/login.dto";
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class AuthService {
|
2020-05-03 20:18:57 +02:00
|
|
|
private readonly userFilter: null | string;
|
2020-09-05 23:35:53 +02:00
|
|
|
private readonly sessionExpirationTime: string;
|
|
|
|
|
|
2020-02-01 16:11:48 +01:00
|
|
|
constructor(
|
2020-05-03 20:18:57 +02:00
|
|
|
private readonly config: ConfigService,
|
2020-02-01 16:11:48 +01:00
|
|
|
private readonly usersService: UsersService,
|
2020-09-05 23:35:53 +02:00
|
|
|
private readonly jwtService: JwtService,
|
2023-02-19 16:16:34 +01:00
|
|
|
private readonly authSessionRepository: AuthSessionRepository,
|
|
|
|
|
private readonly apiTokenRepository: ApiTokenRepository
|
2020-05-03 20:18:57 +02:00
|
|
|
) {
|
|
|
|
|
this.userFilter = this.config.get<string>("SPOTIFY_USER_FILTER");
|
2020-09-05 23:35:53 +02:00
|
|
|
this.sessionExpirationTime = this.config.get<string>(
|
|
|
|
|
"SESSION_EXPIRATION_TIME"
|
|
|
|
|
);
|
2020-05-03 20:18:57 +02:00
|
|
|
}
|
2020-02-01 16:11:48 +01:00
|
|
|
|
|
|
|
|
async spotifyLogin({
|
|
|
|
|
accessToken,
|
|
|
|
|
refreshToken,
|
2020-05-02 17:17:20 +02:00
|
|
|
profile,
|
2020-02-01 16:11:48 +01:00
|
|
|
}: LoginDto): Promise<User> {
|
2020-05-03 20:18:57 +02:00
|
|
|
if (!this.allowedByUserFilter(profile.id)) {
|
2021-06-22 20:34:55 +02:00
|
|
|
throw new ForbiddenException("UserNotInUserFilter");
|
2020-05-03 20:18:57 +02:00
|
|
|
}
|
|
|
|
|
|
2020-02-01 16:11:48 +01:00
|
|
|
const user = await this.usersService.createOrUpdate({
|
|
|
|
|
displayName: profile.displayName,
|
2021-08-14 17:30:38 +00:00
|
|
|
photo: profile.photos.length > 0 ? profile.photos[0].value : null,
|
2020-02-01 16:11:48 +01:00
|
|
|
spotify: {
|
|
|
|
|
id: profile.id,
|
|
|
|
|
accessToken,
|
2020-05-02 17:17:20 +02:00
|
|
|
refreshToken,
|
|
|
|
|
},
|
2020-02-01 16:11:48 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return user;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-25 16:01:55 +02:00
|
|
|
async createSession(user: User): Promise<{
|
2020-09-05 23:35:53 +02:00
|
|
|
session: AuthSession;
|
|
|
|
|
refreshToken: string;
|
|
|
|
|
}> {
|
|
|
|
|
const session = this.authSessionRepository.create();
|
|
|
|
|
|
|
|
|
|
session.user = user;
|
|
|
|
|
await this.authSessionRepository.save(session);
|
|
|
|
|
|
2021-06-22 20:34:55 +02:00
|
|
|
const { refreshToken } = await this.createRefreshToken(session);
|
2020-09-05 23:35:53 +02:00
|
|
|
|
|
|
|
|
return { session, refreshToken };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* createRefreshToken should only be used while creating a new session.
|
|
|
|
|
* @param session
|
|
|
|
|
*/
|
|
|
|
|
private async createRefreshToken(
|
|
|
|
|
session: AuthSession
|
2023-02-19 16:16:34 +01:00
|
|
|
): Promise<{ refreshToken: string }> {
|
2020-09-05 23:35:53 +02:00
|
|
|
const payload = {
|
|
|
|
|
sub: session.user.id,
|
|
|
|
|
name: session.user.displayName,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const token = await this.jwtService.signAsync(payload, {
|
|
|
|
|
jwtid: session.id,
|
|
|
|
|
// jwtService uses the shorter access token time as a default
|
|
|
|
|
expiresIn: this.sessionExpirationTime,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { refreshToken: token };
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-19 16:16:34 +01:00
|
|
|
async createAccessToken(
|
|
|
|
|
session: AuthSession
|
|
|
|
|
): Promise<{ accessToken: string }> {
|
2020-09-05 23:35:53 +02:00
|
|
|
if (session.revokedAt) {
|
|
|
|
|
throw new ForbiddenException("SessionIsRevoked");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
|
sub: session.user.id,
|
|
|
|
|
name: session.user.displayName,
|
|
|
|
|
picture: session.user.photo,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const token = await this.jwtService.signAsync(payload);
|
|
|
|
|
|
|
|
|
|
return { accessToken: token };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async findSession(id: string): Promise<AuthSession> {
|
2022-06-25 13:48:25 +00:00
|
|
|
return this.authSessionRepository.findOneBy({ id });
|
2020-09-05 23:35:53 +02:00
|
|
|
}
|
|
|
|
|
|
2023-02-19 16:16:34 +01:00
|
|
|
async createApiToken(user: User, description: string): Promise<ApiToken> {
|
|
|
|
|
console.log("createApiToken");
|
|
|
|
|
const apiToken = this.apiTokenRepository.create();
|
|
|
|
|
|
|
|
|
|
apiToken.user = user;
|
|
|
|
|
apiToken.description = description;
|
|
|
|
|
|
|
|
|
|
// TODO demagic 20
|
|
|
|
|
const tokenBuffer = await new Promise<Buffer>((resolve, reject) =>
|
|
|
|
|
randomBytes(20, (err, buf) => (err ? reject(err) : resolve(buf)))
|
|
|
|
|
);
|
|
|
|
|
apiToken.token = `lis${tokenBuffer.toString("hex")}`;
|
|
|
|
|
|
|
|
|
|
await this.apiTokenRepository.save(apiToken);
|
|
|
|
|
|
|
|
|
|
return apiToken;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listApiTokens(user: User): Promise<ApiToken[]> {
|
|
|
|
|
return this.apiTokenRepository.scoped.byUser(user).getMany();
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-20 23:50:57 +01:00
|
|
|
async revokeApiToken(user: User, id: string): Promise<void> {
|
|
|
|
|
const apiToken = await this.apiTokenRepository.findOneBy({ user, id });
|
2023-02-19 16:16:34 +01:00
|
|
|
|
2023-02-20 23:50:57 +01:00
|
|
|
if (apiToken && apiToken.revokedAt == null) {
|
|
|
|
|
apiToken.revokedAt = new Date();
|
|
|
|
|
await this.apiTokenRepository.save(apiToken);
|
|
|
|
|
}
|
2023-02-19 16:16:34 +01:00
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async findApiToken(token: string): Promise<ApiToken> {
|
|
|
|
|
return this.apiTokenRepository.findOneBy({ token });
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-01 16:11:48 +01:00
|
|
|
async findUser(id: string): Promise<User> {
|
|
|
|
|
return this.usersService.findById(id);
|
|
|
|
|
}
|
2020-05-03 20:18:57 +02:00
|
|
|
|
|
|
|
|
allowedByUserFilter(spotifyID: string) {
|
|
|
|
|
if (!this.userFilter) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const whitelistedIDs = this.userFilter.split(",");
|
|
|
|
|
|
|
|
|
|
return whitelistedIDs.includes(spotifyID);
|
|
|
|
|
}
|
2020-02-01 16:11:48 +01:00
|
|
|
}
|