feat(api): user authentication

This commit is contained in:
Julian Tölle 2020-02-01 16:11:48 +01:00
parent f14eda16ac
commit f253a66f86
41 changed files with 657 additions and 338 deletions

View 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
View 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 {}

View 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
View 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);
}
}

View 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"' })
);
}

View file

@ -0,0 +1,3 @@
import { createParamDecorator } from "@nestjs/common";
export const ReqUser = createParamDecorator((data, req) => req.user);

View 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
View 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);
}
}

View 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
});
}
}