mirror of
https://github.com/apricote/Listory.git
synced 2026-01-13 21:21:02 +00:00
feat(api): user authentication
This commit is contained in:
parent
f14eda16ac
commit
f253a66f86
41 changed files with 657 additions and 338 deletions
40
src/auth/auth.controller.ts
Normal file
40
src/auth/auth.controller.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Controller, Get, Res, UseGuards } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { Response } from "express";
|
||||
import { User } from "../users/user.entity";
|
||||
import { ReqUser } from "./decorators/req-user.decorator";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Controller("api/v1/auth")
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly config: ConfigService
|
||||
) {}
|
||||
|
||||
@Get("spotify")
|
||||
@UseGuards(AuthGuard("spotify"))
|
||||
spotifyRedirect() {
|
||||
// User is redirected by AuthGuard
|
||||
}
|
||||
|
||||
@Get("spotify/callback")
|
||||
@UseGuards(AuthGuard("spotify"))
|
||||
async spotifyCallback(@ReqUser() user: User, @Res() res: Response) {
|
||||
const { accessToken } = await this.authService.createToken(user);
|
||||
|
||||
// Transmit accessToken to Frontend
|
||||
res.cookie("listory_access_token", accessToken, {
|
||||
// SPA will directly read cookie, save it to local storage and delete it
|
||||
// 15 Minutes should be enough
|
||||
maxAge: 15 * 60 * 1000,
|
||||
|
||||
// Must be readable by SPA
|
||||
httpOnly: false
|
||||
});
|
||||
|
||||
// Redirect User to SPA
|
||||
res.redirect("/");
|
||||
}
|
||||
}
|
||||
27
src/auth/auth.module.ts
Normal file
27
src/auth/auth.module.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { JwtStrategy } from "./jwt.strategy";
|
||||
import { SpotifyStrategy } from "./spotify.strategy";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule.register({ defaultStrategy: "jwt" }),
|
||||
JwtModule.registerAsync({
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get<string>("JWT_SECRET"),
|
||||
signOptions: { expiresIn: config.get<string>("JWT_EXPIRATION_TIME") }
|
||||
}),
|
||||
inject: [ConfigService]
|
||||
}),
|
||||
UsersModule
|
||||
],
|
||||
providers: [AuthService, SpotifyStrategy, JwtStrategy],
|
||||
exports: [PassportModule],
|
||||
controllers: [AuthController]
|
||||
})
|
||||
export class AuthModule {}
|
||||
18
src/auth/auth.service.spec.ts
Normal file
18
src/auth/auth.service.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { AuthService } from "./auth.service";
|
||||
|
||||
describe("AuthService", () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthService]
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
47
src/auth/auth.service.ts
Normal file
47
src/auth/auth.service.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { User } from "../users/user.entity";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { LoginDto } from "./dto/login.dto";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
private readonly jwtService: JwtService
|
||||
) {}
|
||||
|
||||
async spotifyLogin({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
profile
|
||||
}: LoginDto): Promise<User> {
|
||||
const user = await this.usersService.createOrUpdate({
|
||||
displayName: profile.displayName,
|
||||
photo: profile.photos.length > 0 ? profile.photos[0] : null,
|
||||
spotify: {
|
||||
id: profile.id,
|
||||
accessToken,
|
||||
refreshToken
|
||||
}
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async createToken(user: User): Promise<{ accessToken }> {
|
||||
const payload = {
|
||||
sub: user.id,
|
||||
name: user.displayName,
|
||||
picture: user.photo
|
||||
};
|
||||
|
||||
const token = await this.jwtService.signAsync(payload);
|
||||
|
||||
return { accessToken: token };
|
||||
}
|
||||
|
||||
async findUser(id: string): Promise<User> {
|
||||
return this.usersService.findById(id);
|
||||
}
|
||||
}
|
||||
11
src/auth/decorators/auth.decorator.ts
Normal file
11
src/auth/decorators/auth.decorator.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { applyDecorators, UseGuards } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { ApiBearerAuth, ApiUnauthorizedResponse } from "@nestjs/swagger";
|
||||
|
||||
export function Auth() {
|
||||
return applyDecorators(
|
||||
UseGuards(AuthGuard("jwt")),
|
||||
ApiBearerAuth(),
|
||||
ApiUnauthorizedResponse({ description: 'Unauthorized"' })
|
||||
);
|
||||
}
|
||||
3
src/auth/decorators/req-user.decorator.ts
Normal file
3
src/auth/decorators/req-user.decorator.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { createParamDecorator } from "@nestjs/common";
|
||||
|
||||
export const ReqUser = createParamDecorator((data, req) => req.user);
|
||||
9
src/auth/dto/login.dto.ts
Normal file
9
src/auth/dto/login.dto.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export class LoginDto {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
profile: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
photos: string[];
|
||||
};
|
||||
}
|
||||
23
src/auth/jwt.strategy.ts
Normal file
23
src/auth/jwt.strategy.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Strategy, ExtractJwt } from "passport-jwt";
|
||||
import { AuthService } from "./auth.service";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private readonly config: ConfigService,
|
||||
private readonly authService: AuthService
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: config.get<string>("JWT_SECRET")
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any) {
|
||||
return this.authService.findUser(payload.sub);
|
||||
}
|
||||
}
|
||||
33
src/auth/spotify.strategy.ts
Normal file
33
src/auth/spotify.strategy.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Strategy } from "passport-spotify";
|
||||
import { AuthService } from "./auth.service";
|
||||
|
||||
@Injectable()
|
||||
export class SpotifyStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private readonly config: ConfigService,
|
||||
private readonly authService: AuthService
|
||||
) {
|
||||
super({
|
||||
clientID: config.get<string>("SPOTIFY_CLIENT_ID"),
|
||||
clientSecret: config.get<string>("SPOTIFY_CLIENT_SECRET"),
|
||||
callbackURL: `${config.get<string>("BASE_DOMAIN") ||
|
||||
"http://localhost:3000"}/api/v1/auth/spotify/callback`,
|
||||
scope: [
|
||||
"user-read-private",
|
||||
"user-read-email",
|
||||
"user-read-recently-played"
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async validate(accessToken: string, refreshToken: string, profile: any) {
|
||||
return await this.authService.spotifyLogin({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
profile
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue