Skip to content

Commit a96d6a5

Browse files
authored
Merge pull request #18 from asepindrak/dev
Dev
2 parents 9ea0b47 + 8b9177c commit a96d6a5

File tree

14 files changed

+565
-164
lines changed

14 files changed

+565
-164
lines changed

backend/package-lock.json

Lines changed: 48 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "commitflow-api",
3-
"version": "1.1.6",
3+
"version": "1.1.7",
44
"description": "Backend CommitFlow",
55
"author": "asepindrak",
66
"private": false,
@@ -35,6 +35,7 @@
3535
"@nestjs/websockets": "^11.1.6",
3636
"@prisma/client": "^6.18.0",
3737
"axios": "^1.12.2",
38+
"bcrypt": "^6.0.0",
3839
"class-transformer": "^0.5.1",
3940
"class-validator": "^0.14.2",
4041
"dayjs": "^1.11.19",
@@ -62,6 +63,7 @@
6263
"@nestjs/cli": "^11.0.0",
6364
"@nestjs/schematics": "^11.0.0",
6465
"@nestjs/testing": "^11.0.1",
66+
"@types/bcrypt": "^6.0.0",
6567
"@types/express": "^5.0.5",
6668
"@types/jest": "^30.0.0",
6769
"@types/multer": "^2.0.0",

backend/prisma/schema.prisma

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,19 @@ model chatMessage {
4848
}
4949

5050
model User {
51-
id String @id @default(cuid())
52-
email String? @unique
53-
phone String?
54-
name String?
55-
photo String?
56-
role Role? @default(USER)
57-
password String? // kalau pake password
58-
session_token String? @unique
59-
createdAt DateTime @default(now())
60-
updatedAt DateTime @updatedAt
61-
members TeamMember[] @relation("UserMembers")
51+
id String @id @default(cuid())
52+
email String? @unique
53+
phone String?
54+
name String?
55+
photo String?
56+
role Role? @default(USER)
57+
password String? // kalau pake password
58+
session_token String? @unique
59+
refreshTokenHash String? // simpan hash dari refresh token
60+
refreshTokenExpiresAt DateTime?
61+
createdAt DateTime @default(now())
62+
updatedAt DateTime @updatedAt
63+
members TeamMember[] @relation("UserMembers")
6264
6365
}
6466

backend/src/app.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import { Injectable } from "@nestjs/common";
33
@Injectable()
44
export class AppService {
55
getHello(): string {
6-
return `CommitFlow API (1.1.6) is running!`;
6+
return `CommitFlow API (1.1.7) is running!`;
77
}
88
}
Lines changed: 126 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,77 @@
1-
// src/auth/auth.controller.ts
2-
import { Body, Controller, Post, UnauthorizedException } from "@nestjs/common";
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import {
3+
Body,
4+
Controller,
5+
Post,
6+
UnauthorizedException,
7+
Req,
8+
Res,
9+
HttpCode,
10+
} from "@nestjs/common";
311
import { AuthService } from "./auth.service";
412
import { RegisterDto } from "./dto/register.dto";
513
import { LoginDto } from "./dto/login.dto";
14+
import type { Request, Response } from "express";
15+
16+
const REFRESH_COOKIE_NAME = "refresh_token";
17+
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
18+
19+
// Environment-aware cookie options
20+
const isProd = process.env.NODE_ENV === "production";
21+
const FE_URL =
22+
process.env.FE_URL || process.env.FRONTEND_ORIGIN || "http://localhost:3000";
23+
const BACKEND_ORIGIN =
24+
process.env.BACKEND_ORIGIN || `http://localhost:${process.env.PORT || 8000}`;
25+
const isCrossOrigin = FE_URL !== BACKEND_ORIGIN;
26+
27+
// For cross-origin cookies in production you MUST use sameSite: 'none' AND secure: true.
28+
// For local dev over HTTP, browsers disallow SameSite=None without Secure, so we fall back to 'lax'.
29+
const COOKIE_OPTIONS = {
30+
httpOnly: true,
31+
secure: isProd, // true in production (HTTPS)
32+
sameSite: isProd && isCrossOrigin ? ("none" as const) : ("lax" as const),
33+
path: "/",
34+
};
635

736
@Controller("auth")
837
export class AuthController {
938
constructor(private authService: AuthService) {}
1039

1140
@Post("anon")
12-
async anonLogin(@Body("userId") userId?: string) {
13-
const { token, user } = await this.authService.createOrGetAnonymousUser(
14-
userId
15-
);
16-
return { token, userId: user.id }; // kirim userId juga
41+
async anonLogin(
42+
@Body("userId") userId?: string,
43+
@Res({ passthrough: true }) res?: Response
44+
) {
45+
const result = await this.authService.createOrGetAnonymousUser(userId);
46+
47+
// if service returned a refreshToken, set it as httpOnly cookie
48+
if (result?.refreshToken && res) {
49+
res.cookie(REFRESH_COOKIE_NAME, result.refreshToken, {
50+
...COOKIE_OPTIONS,
51+
maxAge: COOKIE_MAX_AGE,
52+
});
53+
}
54+
55+
// keep the same response shape you had before
56+
return { token: result.token, userId: result.user.id };
1757
}
1858

1959
@Post("register")
20-
async register(@Body() dto: RegisterDto) {
60+
async register(
61+
@Body() dto: RegisterDto,
62+
@Res({ passthrough: true }) res?: Response
63+
) {
2164
const result = await this.authService.register(dto);
22-
// return mapping so FE bisa replace optimistic client id with real teamMember.id
65+
66+
// if service returned a refreshToken, set cookie
67+
if (result?.refreshToken && res) {
68+
res.cookie(REFRESH_COOKIE_NAME, result.refreshToken, {
69+
...COOKIE_OPTIONS,
70+
maxAge: COOKIE_MAX_AGE,
71+
});
72+
}
73+
74+
// keep your existing response mapping
2375
return {
2476
token: result.token,
2577
userId: result.user.id,
@@ -31,15 +83,78 @@ export class AuthController {
3183
}
3284

3385
@Post("login")
34-
async login(@Body() dto: LoginDto) {
86+
async login(
87+
@Body() dto: LoginDto,
88+
@Res({ passthrough: true }) res?: Response
89+
) {
3590
const result = await this.authService.login(dto);
3691
if (!result) throw new UnauthorizedException("Invalid credentials");
37-
// return similar shape as register (token + user + optional member)
92+
93+
// set refresh cookie if present (service should ideally return refreshToken)
94+
if (result.refreshToken && res) {
95+
res.cookie(REFRESH_COOKIE_NAME, result.refreshToken, {
96+
...COOKIE_OPTIONS,
97+
maxAge: COOKIE_MAX_AGE,
98+
// set domain if backend is serving under a hostname different than localhost (optional)
99+
});
100+
}
101+
102+
// return similar shape as before (token + user + optional member)
38103
return {
39104
token: result.token,
40105
userId: result?.user?.id ?? "",
41106
user: result.user,
42107
teamMemberId: result?.teamMemberId,
43108
};
44109
}
110+
111+
// refresh endpoint: reads refresh_token cookie, verifies, rotates
112+
@HttpCode(200)
113+
@Post("refresh")
114+
async refresh(
115+
@Req() req: Request,
116+
@Res({ passthrough: true }) res: Response
117+
) {
118+
const token = req.cookies?.[REFRESH_COOKIE_NAME];
119+
if (!token) throw new UnauthorizedException("No refresh token");
120+
121+
// attempt to verify and refresh via AuthService
122+
let payload: any;
123+
try {
124+
payload = (this.authService as any).jwtService.verify(token);
125+
} catch (e: any) {
126+
throw new UnauthorizedException("Invalid refresh token");
127+
}
128+
129+
const userId = payload.sub;
130+
const newTokens = await this.authService.refreshTokens(userId, token);
131+
if (!newTokens) throw new UnauthorizedException("Refresh failed");
132+
133+
// rotate cookie
134+
res.cookie(REFRESH_COOKIE_NAME, newTokens.refreshToken, {
135+
...COOKIE_OPTIONS,
136+
maxAge: COOKIE_MAX_AGE,
137+
});
138+
139+
// return access token
140+
return { token: newTokens.accessToken };
141+
}
142+
143+
@Post("logout")
144+
async logout(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
145+
const token = req.cookies?.[REFRESH_COOKIE_NAME];
146+
if (token) {
147+
try {
148+
const payload: any = (this.authService as any).jwtService.verify(token);
149+
await this.authService.revokeRefreshToken(payload.sub);
150+
} catch (e: any) {
151+
// ignore verification errors during logout
152+
console.log(e);
153+
}
154+
}
155+
156+
// clear cookie with same attributes so browser accepts deletion
157+
res.clearCookie(REFRESH_COOKIE_NAME, { ...COOKIE_OPTIONS, path: "/" });
158+
return { ok: true };
159+
}
45160
}

0 commit comments

Comments
 (0)