Skip to content

Commit

Permalink
Implement login logic
Browse files Browse the repository at this point in the history
  • Loading branch information
devleejb committed Jan 17, 2024
1 parent 85ac23f commit dc7d30b
Show file tree
Hide file tree
Showing 21 changed files with 517 additions and 39 deletions.
265 changes: 256 additions & 9 deletions backend/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.1.17",
"@prisma/client": "^5.8.1",
"passport-github": "^1.1.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
},
Expand Down
20 changes: 15 additions & 5 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { AuthController } from "./user/auth.controller";
import { AuthService } from "./user/auth.service";
import { PrismaService } from "./db/prisma.service";
import { UsersModule } from "./users/users.module";
import { AuthModule } from "./auth/auth.module";
import { ConfigModule } from "@nestjs/config";
import { APP_GUARD } from "@nestjs/core/constants";
import { JwtAuthGuard } from "./auth/jwt.guard";

@Module({
imports: [],
controllers: [AppController, AuthController],
providers: [AppService, PrismaService, AuthService],
imports: [ConfigModule.forRoot({ isGlobal: true }), UsersModule, AuthModule],
controllers: [AppController],
providers: [
AppService,
PrismaService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
18 changes: 18 additions & 0 deletions backend/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AuthController } from "./auth.controller";

describe("AuthController", () => {
let controller: AuthController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();

controller = module.get<AuthController>(AuthController);
});

it("should be defined", () => {
expect(controller).toBeDefined();
});
});
31 changes: 31 additions & 0 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Controller, Get, Req, UseGuards } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { LoginRequest } from "./interfaces/LoginRequest";
import { JwtService } from "@nestjs/jwt";
import { LoginResponse } from "./interfaces/LoginResponse";
import { UsersService } from "src/users/users.service";
import { Public } from "src/utils/decorators/auth.decorator";

@Controller("auth")
export class AuthController {
constructor(
private jwtService: JwtService,
private usersService: UsersService
) {}

@Public()
@Get("login/github")
@Get("callback/github")
@UseGuards(AuthGuard("github"))
async login(@Req() req: LoginRequest): Promise<LoginResponse> {
const user = await this.usersService.findOrCreate(
req.user.socialProvider,
req.user.socialUid,
req.user.nickname
);

const accessToken = this.jwtService.sign({ sub: user.id, nickname: user.nickname });

return { accessToken };
}
}
26 changes: 26 additions & 0 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { UsersModule } from "src/users/users.module";
import { AuthController } from "./auth.controller";
import { GithubStrategy } from "./github.strategy";
import { ConfigService } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
import { JwtStrategy } from "./jwt.strategy";

@Module({
imports: [
UsersModule,
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => {
return {
signOptions: { expiresIn: "24h" },
secret: configService.get<string>("JWT_SECRET"),
};
},
inject: [ConfigService],
}),
],
providers: [AuthService, GithubStrategy, JwtStrategy],
controllers: [AuthController],
})
export class AuthModule {}
18 changes: 18 additions & 0 deletions backend/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
20 changes: 20 additions & 0 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable } from "@nestjs/common";
import { UsersService } from "src/users/users.service";

@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}

async issueJwtToken(socialProvider: string, socialUid: string, nickname: string) {
const user = await this.usersService.findOrCreate(socialProvider, socialUid, nickname);

if (user) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { socialProvider: _socaialProvider, socialUid: _social, ...result } = user;

return result;
}

return null;
}
}
29 changes: 29 additions & 0 deletions backend/src/auth/github.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { Profile, Strategy } from "passport-github";
import { LoginUserInfo } from "./interfaces/LoginRequest";

@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, "github") {
constructor(configService: ConfigService) {
super({
clientID: configService.get<string>("GITHUB_CLIENT_ID"),
clientSecret: configService.get<string>("GITHUB_CLIENT_SECRET"),
callbackURL: configService.get<string>("GITHUB_CALLBACK_URL"),
scope: ["public_profile"],
});
}

async validate(
_accessToken: string,
_refreshToken: string,
profile: Profile
): Promise<LoginUserInfo> {
return {
socialProvider: "github",
socialUid: profile.id,
nickname: profile.username,
};
}
}
9 changes: 9 additions & 0 deletions backend/src/auth/interfaces/LoginRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SocialProvider } from "src/utils/types/auth.type";

export interface LoginUserInfo {
socialProvider: SocialProvider;
socialUid: string;
nickname: string;
}

export type LoginRequest = Request & { user: LoginUserInfo };
3 changes: 3 additions & 0 deletions backend/src/auth/interfaces/LoginResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface LoginResponse {
accessToken: string;
}
22 changes: 22 additions & 0 deletions backend/src/auth/jwt.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { AuthGuard } from "@nestjs/passport";
import { IS_PUBLIC_PATH } from "src/utils/decorators/auth.decorator";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
constructor(private reflector: Reflector) {
super();
}

canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_PATH, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
20 changes: 20 additions & 0 deletions backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ExtractJwt, Strategy as PassportJwtStrategy } from "passport-jwt";
import { ConfigService } from "@nestjs/config";
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { JwtPayload } from "src/utils/types/jwt.type";

@Injectable()
export class JwtStrategy extends PassportStrategy(PassportJwtStrategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>("JWT_SECRET"),
});
}

async validate(payload: JwtPayload) {
return { id: payload.sub, nickname: payload.nickname };
}
}
13 changes: 0 additions & 13 deletions backend/src/user/auth.controller.ts

This file was deleted.

8 changes: 0 additions & 8 deletions backend/src/user/dto/login.dto.ts

This file was deleted.

9 changes: 9 additions & 0 deletions backend/src/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { UsersService } from "./users.service";
import { PrismaService } from "src/db/prisma.service";

@Module({
providers: [UsersService, PrismaService],
exports: [UsersService],
})
export class UsersModule {}
18 changes: 18 additions & 0 deletions backend/src/users/users.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { Injectable } from "@nestjs/common";
import { PrismaService } from "../db/prisma.service";
import { User } from "@prisma/client";
import { PrismaService } from "src/db/prisma.service";

@Injectable()
export class AuthService {
export class UsersService {
constructor(private prismaService: PrismaService) {}

async findOrCreateUser(idToken: string, nickname: string): Promise<User | null> {
const socialUid = idToken;
async findOrCreate(
socialProvider: string,
socialUid: string,
nickname: string
): Promise<User | null> {
return this.prismaService.user.upsert({
where: {
socialProvider,
socialUid,
},
update: {},
create: {
socialProvider,
socialUid,
nickname,
},
Expand Down
4 changes: 4 additions & 0 deletions backend/src/utils/decorators/auth.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SetMetadata } from "@nestjs/common";

export const IS_PUBLIC_PATH = "isPublicPath";
export const Public = () => SetMetadata(IS_PUBLIC_PATH, true);
1 change: 1 addition & 0 deletions backend/src/utils/types/auth.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type SocialProvider = "github";
4 changes: 4 additions & 0 deletions backend/src/utils/types/jwt.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface JwtPayload {
sub: string;
nickname: string;
}

0 comments on commit dc7d30b

Please sign in to comment.