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
|
|
@ -1,17 +1,17 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { AuthenticationModule } from "./authentication/authentication.module";
|
||||
import { AuthModule } from "./auth/auth.module";
|
||||
import { DatabaseModule } from "./database/database.module";
|
||||
import { ConnectionsModule } from "./connections/connections.module";
|
||||
import { FrontendModule } from './frontend/frontend.module';
|
||||
import { SourcesModule } from "./sources/sources.module";
|
||||
import { UsersModule } from "./users/users.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
DatabaseModule,
|
||||
AuthenticationModule,
|
||||
ConnectionsModule,
|
||||
FrontendModule
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
SourcesModule
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
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 {}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { ConnectionsService } from "./connections.service";
|
||||
import { AuthService } from "./auth.service";
|
||||
|
||||
describe("ConnectionsService", () => {
|
||||
let service: ConnectionsService;
|
||||
describe("AuthService", () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ConnectionsService]
|
||||
providers: [AuthService]
|
||||
}).compile();
|
||||
|
||||
service = module.get<ConnectionsService>(ConnectionsService);
|
||||
service = module.get<AuthService>(AuthService);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
import { JwtStrategy } from "./jwt.strategy";
|
||||
|
||||
@Module({
|
||||
imports: [PassportModule.register({ defaultStrategy: "jwt" })],
|
||||
providers: [JwtStrategy],
|
||||
exports: [PassportModule]
|
||||
})
|
||||
export class AuthenticationModule {}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { passportJwtSecret } from "jwks-rsa";
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private readonly config: ConfigService) {
|
||||
super({
|
||||
secretOrKeyProvider: passportJwtSecret({
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 5,
|
||||
jwksUri: `${config.get<string>("AUTH0_DOMAIN")}.well-known/jwks.json`
|
||||
}),
|
||||
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
audience: config.get<string>("AUTH0_AUDIENCE"),
|
||||
issuer: config.get<string>("AUTH0_DOMAIN"),
|
||||
algorithms: ["RS256"]
|
||||
});
|
||||
}
|
||||
|
||||
validate(payload: any) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export enum ConnectionType {
|
||||
SPOTIFY = "spotify"
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { ConnectionType } from "./connection-type.enum";
|
||||
|
||||
@Entity()
|
||||
export class Connection {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
userID: string;
|
||||
|
||||
@Column()
|
||||
type: ConnectionType;
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { ConnectionsController } from "./connections.controller";
|
||||
|
||||
describe("Connections Controller", () => {
|
||||
let controller: ConnectionsController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [ConnectionsController]
|
||||
}).compile();
|
||||
|
||||
controller = module.get<ConnectionsController>(ConnectionsController);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { Controller, Get, UseGuards } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Controller("api/v1/connections")
|
||||
export class ConnectionsController {
|
||||
@Get()
|
||||
@UseGuards(AuthGuard("jwt"))
|
||||
get() {
|
||||
return { msg: "Success!" };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { ConnectionsController } from "./connections.controller";
|
||||
import { ConnectionsRepository } from "./connections.repository";
|
||||
import { ConnectionsService } from "./connections.service";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ConnectionsRepository])],
|
||||
controllers: [ConnectionsController],
|
||||
providers: [ConnectionsService]
|
||||
})
|
||||
export class ConnectionsModule {}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { EntityRepository, Repository } from "typeorm";
|
||||
import { Connection } from "./connection.entity";
|
||||
|
||||
@EntityRepository(Connection)
|
||||
export class ConnectionsRepository extends Repository<Connection> {}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { ConnectionsRepository } from "./connections.repository";
|
||||
|
||||
@Injectable()
|
||||
export class ConnectionsService {
|
||||
constructor(private readonly connectionRepository: ConnectionsRepository) {}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { join } from "path";
|
||||
|
||||
|
|
@ -17,9 +17,9 @@ export const DatabaseModule = TypeOrmModule.forRootAsync({
|
|||
entities: [join(__dirname, "..", "**/*.entity.{ts,js}")],
|
||||
|
||||
// Migrations
|
||||
migrationsRun: true,
|
||||
migrations: [join(__dirname, "migrations", "*.{ts,js}")],
|
||||
synchronize: false
|
||||
// migrationsRun: true,
|
||||
// migrations: [join(__dirname, "migrations", "*.{ts,js}")],
|
||||
synchronize: true
|
||||
}),
|
||||
inject: [ConfigService]
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import { Controller, Get } from "@nestjs/common";
|
||||
|
||||
@Controller("")
|
||||
export class FrontendController {
|
||||
@Get()
|
||||
index() {}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { FrontendController } from './frontend.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [FrontendController]
|
||||
})
|
||||
export class FrontendModule {}
|
||||
17
src/main.ts
17
src/main.ts
|
|
@ -1,14 +1,23 @@
|
|||
import { NestFactory } from "@nestjs/core";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { join } from "path";
|
||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
|
||||
app.useStaticAssets(join(__dirname, "frontend", "public"));
|
||||
app.setBaseViewsDir(join(__dirname, "frontend", "views"));
|
||||
app.setViewEngine("mustache");
|
||||
// Setup API Docs
|
||||
const options = new DocumentBuilder()
|
||||
.setTitle("Listory")
|
||||
.setDescription("Track and analyze your Spotify Listens")
|
||||
.setVersion("1.0")
|
||||
.addBearerAuth()
|
||||
.addTag("user")
|
||||
.addTag("listens")
|
||||
.addTag("auth")
|
||||
.build();
|
||||
const document = SwaggerModule.createDocument(app, options);
|
||||
SwaggerModule.setup("api/docs", app, document);
|
||||
|
||||
await app.listen(3000);
|
||||
}
|
||||
|
|
|
|||
7
src/sources/sources.module.ts
Normal file
7
src/sources/sources.module.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SpotifyModule } from './spotify/spotify.module';
|
||||
|
||||
@Module({
|
||||
imports: [SpotifyModule]
|
||||
})
|
||||
export class SourcesModule {}
|
||||
12
src/sources/spotify/spotify-connection.entity.ts
Normal file
12
src/sources/spotify/spotify-connection.entity.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Column } from "typeorm";
|
||||
|
||||
export class SpotifyConnection {
|
||||
@Column()
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
accessToken: string;
|
||||
|
||||
@Column()
|
||||
refreshToken: string;
|
||||
}
|
||||
9
src/sources/spotify/spotify.module.ts
Normal file
9
src/sources/spotify/spotify.module.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SpotifyService } from './spotify.service';
|
||||
import { SpotifyApiModule } from './spotify-api/spotify-api.module';
|
||||
|
||||
@Module({
|
||||
providers: [SpotifyService],
|
||||
imports: [SpotifyApiModule]
|
||||
})
|
||||
export class SpotifyModule {}
|
||||
18
src/sources/spotify/spotify.service.spec.ts
Normal file
18
src/sources/spotify/spotify.service.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SpotifyService } from './spotify.service';
|
||||
|
||||
describe('SpotifyService', () => {
|
||||
let service: SpotifyService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [SpotifyService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SpotifyService>(SpotifyService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
8
src/sources/spotify/spotify.service.ts
Normal file
8
src/sources/spotify/spotify.service.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {Interval} from "@nestjs/cron"
|
||||
|
||||
@Injectable()
|
||||
export class SpotifyService {
|
||||
@Interval
|
||||
|
||||
}
|
||||
10
src/users/dto/create-or-update.dto.ts
Normal file
10
src/users/dto/create-or-update.dto.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export class CreateOrUpdateDto {
|
||||
displayName: string;
|
||||
photo?: string;
|
||||
|
||||
spotify: {
|
||||
id: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
}
|
||||
17
src/users/user.entity.ts
Normal file
17
src/users/user.entity.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from "typeorm";
|
||||
import { SpotifyConnection } from "../sources/spotify/spotify-connection.entity";
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
displayName: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
photo?: string;
|
||||
|
||||
@Column(type => SpotifyConnection)
|
||||
spotify: SpotifyConnection;
|
||||
}
|
||||
5
src/users/user.repository.ts
Normal file
5
src/users/user.repository.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { EntityRepository, Repository } from "typeorm";
|
||||
import { User } from "./user.entity";
|
||||
|
||||
@EntityRepository(User)
|
||||
export class UserRepository extends Repository<User> {}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FrontendController } from './frontend.controller';
|
||||
import { UsersController } from './users.controller';
|
||||
|
||||
describe('Frontend Controller', () => {
|
||||
let controller: FrontendController;
|
||||
describe('Users Controller', () => {
|
||||
let controller: UsersController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [FrontendController],
|
||||
controllers: [UsersController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<FrontendController>(FrontendController);
|
||||
controller = module.get<UsersController>(UsersController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
17
src/users/users.controller.ts
Normal file
17
src/users/users.controller.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Controller, Get } from "@nestjs/common";
|
||||
import { Auth } from "../auth/decorators/auth.decorator";
|
||||
import { ReqUser } from "../auth/decorators/req-user.decorator";
|
||||
import { User } from "./user.entity";
|
||||
|
||||
@Controller("api/v1/users")
|
||||
export class UsersController {
|
||||
@Get("me")
|
||||
@Auth()
|
||||
getMe(@ReqUser() user: User): Omit<User, "spotify"> {
|
||||
return {
|
||||
id: user.id,
|
||||
displayName: user.displayName,
|
||||
photo: user.photo
|
||||
};
|
||||
}
|
||||
}
|
||||
13
src/users/users.module.ts
Normal file
13
src/users/users.module.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { UserRepository } from "./user.repository";
|
||||
import { UsersService } from "./users.service";
|
||||
import { UsersController } from './users.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([UserRepository])],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
controllers: [UsersController]
|
||||
})
|
||||
export class UsersModule {}
|
||||
18
src/users/users.service.spec.ts
Normal file
18
src/users/users.service.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { UsersService } from "./users.service";
|
||||
|
||||
describe("UsersService", () => {
|
||||
let service: UsersService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UsersService]
|
||||
}).compile();
|
||||
|
||||
service = module.get<UsersService>(UsersService);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
42
src/users/users.service.ts
Normal file
42
src/users/users.service.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { CreateOrUpdateDto } from "./dto/create-or-update.dto";
|
||||
import { User } from "./user.entity";
|
||||
import { UserRepository } from "./user.repository";
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly userRepository: UserRepository) {}
|
||||
|
||||
async findById(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne(id);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException("UserNotFound");
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async createOrUpdate(data: CreateOrUpdateDto): Promise<User> {
|
||||
let user = await this.userRepository.findOne({
|
||||
where: { spotify: { id: data.spotify.id } }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = this.userRepository.create({
|
||||
spotify: {
|
||||
id: data.spotify.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
user.spotify.accessToken = data.spotify.accessToken;
|
||||
user.spotify.refreshToken = data.spotify.refreshToken;
|
||||
user.displayName = data.displayName;
|
||||
user.photo = data.photo;
|
||||
|
||||
await this.userRepository.save(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue