diff --git a/.env.gpg b/.env.gpg index 0a054865..65ec618a 100644 Binary files a/.env.gpg and b/.env.gpg differ diff --git a/backend/src/main/java/edu/nus/market/dao/AccountDao.java b/backend/src/main/java/edu/nus/market/dao/AccountDao.java index 6d1c9d64..3bc02e9d 100644 --- a/backend/src/main/java/edu/nus/market/dao/AccountDao.java +++ b/backend/src/main/java/edu/nus/market/dao/AccountDao.java @@ -30,8 +30,8 @@ public interface AccountDao { "RETURNING id") int updatePassword(int id, String passwordHash, String passwordSalt); - @Update("UPDATE account SET nickname = #{nickname}, avatar_url = #{avatar}, phone_code = #{phoneCode}, phone_number = #{phoneNumber}, preferred_currency = #{currency} WHERE id = #{id} AND deleted_at IS NULL" + + @Update("UPDATE account SET nickname = #{nickname}, avatar_url = #{avatar}, phone_code = #{phoneCode}, phone_number = #{phoneNumber}, preferred_currency = #{currency} WHERE id = #{id} AND deleted_at IS NULL " + "RETURNING id") - int updateProfile(String nickname, String avatar, String phoneCode, String phone_number, String currency, int id); + int updateProfile(String nickname, String avatar, String phoneCode, String phoneNumber, String currency, int id); } diff --git a/backend/src/main/java/edu/nus/market/security/CookieManager.java b/backend/src/main/java/edu/nus/market/security/CookieManager.java index 43073bcb..12401db6 100644 --- a/backend/src/main/java/edu/nus/market/security/CookieManager.java +++ b/backend/src/main/java/edu/nus/market/security/CookieManager.java @@ -9,20 +9,18 @@ public class CookieManager { public static ResponseCookie generateCookie(String accessToken){ ResponseCookie cookie = ResponseCookie.from("access_token", accessToken) .httpOnly(true) - .secure(true) .path("/") - .maxAge(7 * 24 * 60 * 60) // 1 week - .sameSite("Strict") + .maxAge(7 * 24 * 60 * 60) + .sameSite("Lax") .build(); return cookie; } public static ResponseCookie deleteCookie(){ ResponseCookie cookie = ResponseCookie.from("access_token", null) .httpOnly(true) - .secure(true) .path("/") - .maxAge(0) // 1 week - .sameSite("Strict") + .maxAge(0) + .sameSite("Lax") .build(); return cookie; } diff --git a/backend/src/main/java/edu/nus/market/service/AccountServiceImpl.java b/backend/src/main/java/edu/nus/market/service/AccountServiceImpl.java index 31af3f6b..15aff904 100644 --- a/backend/src/main/java/edu/nus/market/service/AccountServiceImpl.java +++ b/backend/src/main/java/edu/nus/market/service/AccountServiceImpl.java @@ -57,7 +57,7 @@ public ResponseEntity getAccountService(int id) { @Override public ResponseEntity logoutService(){ ResponseCookie cookie = cookieManager.deleteCookie(); - return ResponseEntity.status(HttpStatus.NO_CONTENT).header("Cookie", cookie.toString()).build(); + return ResponseEntity.status(HttpStatus.NO_CONTENT).header("Set-Cookie", cookie.toString()).build(); } /** @@ -76,7 +76,7 @@ public ResponseEntity loginService(LoginReq loginReq){ String accessToken = jwtTokenManager.generateAccessToken(account.getId()); ResponseCookie cookie = cookieManager.generateCookie(accessToken); // generate the JWTaccesstoken and send it to the frontend - return ResponseEntity.status(HttpStatus.CREATED).header("Cookie", cookie.toString()).body(new ResAccount(account)); + return ResponseEntity.status(HttpStatus.CREATED).header("Set-Cookie", cookie.toString()).body(new ResAccount(account)); } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorMsg(ErrorMsgEnum.WRONG_PASSWORD.ErrorMsg)); } @@ -103,7 +103,7 @@ public ResponseEntity registerService(RegisterReq registerReq){ String accessToken = jwtTokenManager.generateAccessToken((accountId)); ResponseCookie cookie = cookieManager.generateCookie(accessToken); // generate the JWTaccesstoken and send it to the frontend - return ResponseEntity.status(HttpStatus.CREATED).header("Cookie", cookie.toString()).body(new ResAccount(account)); + return ResponseEntity.status(HttpStatus.CREATED).header("Set-Cookie", cookie.toString()).body(new ResAccount(account)); } @Override diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index e6ec0d0e..086f8065 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -1,5 +1,3 @@ -version: '3.8' - services: frontend: build: @@ -7,17 +5,17 @@ services: dockerfile: Dockerfile args: NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} - image: z1yoon/nus-secondhand-market-frontend:latest ports: - "80:3000" restart: always + environment: + - API_BASE_URL=${API_BASE_URL} user: root backend: build: context: backend dockerfile: Dockerfile - image: z1yoon/nus-secondhand-market-backend:latest ports: - "8081:8081" restart: always diff --git a/docker-compose.yaml b/docker-compose.yaml index e0b1db9b..275d66a0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,11 +1,11 @@ -version: '3.8' - services: frontend: image: z1yoon/nus-secondhand-market-frontend:latest ports: - "80:3000" restart: always + environment: + - API_BASE_URL=${API_BASE_URL} user: root backend: diff --git a/frontend/.env.example b/frontend/.env.example index b0f8c538..1fdf2adb 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,4 +1,3 @@ # The base URL of backend API entrypoint. -# For example, "http://localhost:8080/", -# Note: A trailing slash is mandatory. +# For example, "http://localhost:8080", NEXT_PUBLIC_API_BASE_URL= diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 81b57439..4d95d490 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -28,9 +28,7 @@ WORKDIR /app RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -COPY --from=build --chown=nextjs:nodejs /app/public public COPY --from=build --chown=nextjs:nodejs /app/.next/standalone . -COPY --from=build --chown=nextjs:nodejs /app/.next/static .next/static USER nextjs diff --git a/frontend/package.json b/frontend/package.json index 31c1d9b3..8d29e515 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,8 +5,8 @@ "packageManager": "pnpm@9.11.0", "scripts": { "dev": "next dev", - "build": "next build", - "start": "next start", + "build": "next build && cp -r .next/static .next/standalone/.next && cp -r public .next/standalone", + "start": "node .next/standalone/server.js", "lint": "tsc && eslint . --cache --cache-location node_modules/.cache/eslint/.eslint-cache" }, "dependencies": { diff --git a/frontend/src/app/login/form.tsx b/frontend/src/app/login/form.tsx index 6c739293..b95faf37 100644 --- a/frontend/src/app/login/form.tsx +++ b/frontend/src/app/login/form.tsx @@ -36,7 +36,7 @@ export function LoginForm() { string, FormEvent >( - "auth/me", + "/auth/me", async (_, { arg: event }) => { event.preventDefault(); @@ -44,7 +44,7 @@ export function LoginForm() { const { email, password } = v.parse(formSchema, formData); - return await new ClientRequester().post("auth/token", { + return await new ClientRequester().post("/auth/token", { email, password, }); @@ -58,6 +58,7 @@ export function LoginForm() { description: `Welcome back, ${account.nickname ?? account.email}!`, }); router.push("/"); + router.refresh(); }, throwOnError: false, onError: (error) => { diff --git a/frontend/src/app/register/form.tsx b/frontend/src/app/register/form.tsx index a017a477..67b0fddf 100644 --- a/frontend/src/app/register/form.tsx +++ b/frontend/src/app/register/form.tsx @@ -40,7 +40,7 @@ export function RegisterForm() { string, FormEvent >( - "auth/me", + "/auth/me", async (_, { arg: event }) => { event.preventDefault(); @@ -52,7 +52,7 @@ export function RegisterForm() { throw new Error("Passwords do not match. Please double check."); } - return await new ClientRequester().post("auth/me", { + return await new ClientRequester().post("/auth/me", { email, password, }); @@ -66,6 +66,7 @@ export function RegisterForm() { description: `Welcome on board, ${account.email}!`, }); router.push("/"); + router.refresh(); }, throwOnError: false, onError: (error) => { diff --git a/frontend/src/app/settings/cards/delete-account-card.tsx b/frontend/src/app/settings/cards/delete-account-card.tsx index d804e8b1..adc1df4a 100644 --- a/frontend/src/app/settings/cards/delete-account-card.tsx +++ b/frontend/src/app/settings/cards/delete-account-card.tsx @@ -95,9 +95,9 @@ function DeleteAccountButton() { string, MouseEvent >( - "auth/me", + "/auth/me", async () => { - return await new ClientRequester().delete("auth/me"); + return await new ClientRequester().delete("/auth/me"); }, { populateCache: true, @@ -109,6 +109,7 @@ function DeleteAccountButton() { "We are sorry to see you go. 🥲 Remember you can contact our support team to find your account back in the next 30 days!", }); router.push("/"); + router.refresh(); }, throwOnError: false, onError: (error) => { diff --git a/frontend/src/app/settings/cards/update-email-card.tsx b/frontend/src/app/settings/cards/update-email-card.tsx index ba3e7739..29870e08 100644 --- a/frontend/src/app/settings/cards/update-email-card.tsx +++ b/frontend/src/app/settings/cards/update-email-card.tsx @@ -38,7 +38,7 @@ export function UpdateEmailCard({ initialEmail }: Props) { string, FormEvent >( - "auth/me", + "/auth/me", async (_, { arg: event }) => { event.preventDefault(); @@ -46,7 +46,7 @@ export function UpdateEmailCard({ initialEmail }: Props) { const { email } = v.parse(formSchema, formData); - return await new ClientRequester().patch("auth/me", { + return await new ClientRequester().patch("/auth/me", { email, }); }, diff --git a/frontend/src/app/settings/cards/update-nickname-card.tsx b/frontend/src/app/settings/cards/update-nickname-card.tsx index 3031bbc7..a5dd337c 100644 --- a/frontend/src/app/settings/cards/update-nickname-card.tsx +++ b/frontend/src/app/settings/cards/update-nickname-card.tsx @@ -39,7 +39,7 @@ export function UpdateNicknameCard({ initialNickname }: Props) { string, FormEvent >( - "auth/me", + "/auth/me", async (_, { arg: event }) => { event.preventDefault(); @@ -47,7 +47,7 @@ export function UpdateNicknameCard({ initialNickname }: Props) { const { nickname } = v.parse(formSchema, formData); - return await new ClientRequester().patch("auth/me", { + return await new ClientRequester().patch("/auth/me", { nickname, }); }, diff --git a/frontend/src/app/settings/cards/update-whatsapp-card.tsx b/frontend/src/app/settings/cards/update-whatsapp-card.tsx index 7baf5a9d..ec1b7af8 100644 --- a/frontend/src/app/settings/cards/update-whatsapp-card.tsx +++ b/frontend/src/app/settings/cards/update-whatsapp-card.tsx @@ -49,7 +49,7 @@ export function UpdateWhatsappCard({ string, FormEvent >( - "auth/me", + "/auth/me", async (_, { arg: event }) => { event.preventDefault(); @@ -57,7 +57,7 @@ export function UpdateWhatsappCard({ const { phoneCode, phoneNumber } = v.parse(formSchema, formData); - return await new ClientRequester().patch("auth/me", { + return await new ClientRequester().patch("/auth/me", { phone_code: phoneCode, phone_number: phoneNumber, }); diff --git a/frontend/src/app/settings/contacts/page.tsx b/frontend/src/app/settings/contacts/page.tsx index 8a9990cf..e75df22d 100644 --- a/frontend/src/app/settings/contacts/page.tsx +++ b/frontend/src/app/settings/contacts/page.tsx @@ -3,7 +3,7 @@ import { ServerRequester } from "@/utils/requester/server"; import { UpdateWhatsappCard } from "../cards/update-whatsapp-card"; export default async function ContactsSettingsPage() { - const me = await new ServerRequester().get("auth/me"); + const me = await new ServerRequester().get("/auth/me"); return (
diff --git a/frontend/src/app/settings/display/page.tsx b/frontend/src/app/settings/display/page.tsx index 07acdebb..76616b4e 100644 --- a/frontend/src/app/settings/display/page.tsx +++ b/frontend/src/app/settings/display/page.tsx @@ -3,7 +3,7 @@ import { ServerRequester } from "@/utils/requester/server"; import { UpdateNicknameCard } from "../cards/update-nickname-card"; export default async function DisplaySettingsPage() { - const me = await new ServerRequester().get("auth/me"); + const me = await new ServerRequester().get("/auth/me"); return (
diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 4b86cec0..af07e7c5 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -4,7 +4,7 @@ import { DeleteAccountCard } from "./cards/delete-account-card"; import { UpdateEmailCard } from "./cards/update-email-card"; export default async function SettingsPage() { - const me = await new ServerRequester().get("auth/me"); + const me = await new ServerRequester().get("/auth/me"); return (
diff --git a/frontend/src/components/framework/me-card/log-out-button.tsx b/frontend/src/components/framework/me-card/log-out-button.tsx index cec1802c..93f96771 100644 --- a/frontend/src/components/framework/me-card/log-out-button.tsx +++ b/frontend/src/components/framework/me-card/log-out-button.tsx @@ -19,9 +19,9 @@ export function LogOutButton() { string, MouseEvent >( - "auth/me", + "/auth/me", async () => { - return await new ClientRequester().delete("auth/token"); + return await new ClientRequester().delete("/auth/token"); }, { populateCache: true, diff --git a/frontend/src/components/framework/me-card/me-card-client.tsx b/frontend/src/components/framework/me-card/me-card-client.tsx index c7b2a13f..ca8043c0 100644 --- a/frontend/src/components/framework/me-card/me-card-client.tsx +++ b/frontend/src/components/framework/me-card/me-card-client.tsx @@ -24,9 +24,9 @@ type Props = { export function MeCardClient({ initialMe, fallback }: Props) { const { data: me } = useSWR( - "auth/me", + "/auth/me", async () => { - return await new ClientRequester().get("auth/me"); + return await new ClientRequester().get("/auth/me"); }, { fallbackData: initialMe as Account, diff --git a/frontend/src/components/framework/me-card/me-card-server.tsx b/frontend/src/components/framework/me-card/me-card-server.tsx index 605929b4..0dae5846 100644 --- a/frontend/src/components/framework/me-card/me-card-server.tsx +++ b/frontend/src/components/framework/me-card/me-card-server.tsx @@ -4,7 +4,7 @@ import { JoinNowCard } from "./join-now-card"; import { MeCardClient } from "./me-card-client"; export async function MeCardServer() { - const me = await new ServerRequester().get("auth/me"); + const me = await new ServerRequester().get("/auth/me"); return } />; } diff --git a/frontend/src/utils/requester/client-fetcher.ts b/frontend/src/utils/requester/client-fetcher.ts index c2726687..940cf3bb 100644 --- a/frontend/src/utils/requester/client-fetcher.ts +++ b/frontend/src/utils/requester/client-fetcher.ts @@ -2,9 +2,12 @@ import type { Fetcher } from "./fetcher"; export class ClientFetcher implements Fetcher { public async fetch(endpoint: string, init: RequestInit = {}) { - const url = new URL(endpoint, process.env["NEXT_PUBLIC_API_BASE_URL"]); + const url = process.env["NEXT_PUBLIC_API_BASE_URL"] + endpoint; - const response = await fetch(url, init); + const response = await fetch(url, { + ...init, + credentials: "include", + }); if (response.status === 204) { return undefined as never; diff --git a/frontend/src/utils/requester/server-fetcher.ts b/frontend/src/utils/requester/server-fetcher.ts index a00d98fb..5ff9990b 100644 --- a/frontend/src/utils/requester/server-fetcher.ts +++ b/frontend/src/utils/requester/server-fetcher.ts @@ -3,15 +3,16 @@ import type { Fetcher } from "./fetcher"; export class ServerFetcher implements Fetcher { public async fetch(endpoint: string, init: RequestInit = {}) { - const url = new URL(endpoint, process.env["NEXT_PUBLIC_API_BASE_URL"]); + const url = process.env["API_BASE_URL"] + endpoint; const response = await fetch(url, { ...init, headers: { ...init.headers, + credentials: "include", Cookie: cookies().toString(), }, - }); + } as RequestInit); if (response.status === 204) { return undefined as never; diff --git a/init/1-schema.sql b/init/1-schema.sql index addbcac5..81e5f46b 100644 --- a/init/1-schema.sql +++ b/init/1-schema.sql @@ -31,8 +31,11 @@ create table account ( -- Department or school of the user. department_id int default null references department(id) on delete set null, + -- User's WhatsApp phone country code. + phone_code text default null, + -- User's WhatsApp phone number. - phone text default null, + phone_number text default null, -- User's preferred display currency, in the form of ISO 4217, e.g. CNY, SGD. preferred_currency text default null,