diff --git a/AGENTS.md b/AGENTS.md
index 6ba5f00..cef168f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -33,18 +33,31 @@ packages/api-contract/ # Shared Zod schemas, types, route definitions
2. Implement handler in `apps/backend`
3. Consume from `apps/frontend` via workspace import `@repo/api-contract`
+## Authentication (Better Auth)
+
+The project uses **Better Auth** for session management, replacing the legacy Supabase Auth.
+
+- **Backend Context**: `AppContext` (mapped via `AppEnv`) provides `c.get('user')` and `c.get('session')`.
+- **User Roles**: The `User` model includes a `role` field (default: `member`). Use `requireSuperAdmin` middleware for protected admin routes.
+- **Session Persistence**:
+ - Backend: Hono CORS must have `credentials: true`.
+ - Frontend: `authClient` must have `fetchOptions.credentials: 'include'`.
+ - Frontend: Axios instance must have `withCredentials: true`.
+
## Key Quirks
-- **Prisma 7**: Uses `prisma.config.ts`, requires `DATABASE_URL` env var at generate time
-- **Zod v4**: Use `.issues` instead of `.errors` for validation errors
-- **Generated files**: `routeTree.gen.ts` and `tsconfig*.json` are auto-generated (ignored by Biome)
+- **Prisma 7**: Uses `prisma.config.ts`, requires `DATABASE_URL` env var at generate time.
+- **Gravatar Fallback**: Users without an `image` get an automatic Gravatar Identicon.
+ - Backend: Handled in `user.service.ts` for profile API.
+ - Frontend: Handled in `AuthProvider` (`auth.tsx`) for the session state.
+- **Zod v4**: Use `.issues` instead of `.errors` for validation errors.
- **TanStack Router**: Routes are code-generated. Use `--filter frontend build` not raw vite build.
## Docker Deployment (Dokploy)
- **Build Context**: `./` (monorepo root)
- **Dockerfile**: `Dockerfile` (in root, not `apps/backend/`)
-- **Required args**: `DATABASE_URL`, `VITE_API_BASE_URL`, `VITE_SUPABASE_URL`, `VITE_SUPABASE_ANON_KEY`
+- **Required args**: `DATABASE_URL`, `VITE_API_BASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`
- **bun.lock must be tracked**: It's in `.gitignore` by default - remove it for Docker builds
## Linting
@@ -59,8 +72,9 @@ Biome is used (not ESLint). Config in `biome.json`:
**Backend** (`apps/backend/.env`):
- `DATABASE_URL` - PostgreSQL connection (required for Prisma)
-- `SUPABASE_URL`, `SUPABASE_ANON_KEY` - Auth
-- `CORS_ORIGIN` - Frontend URL
+- `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL` - Auth core
+- `APP_BASE_URL` - Frontend URL for trusted origins
+- `CORS_ORIGIN` - Frontend URL for CORS policy
**Frontend** (`apps/frontend/.env`):
- `VITE_*` prefix required (Vite embeds these at build time)
diff --git a/apps/frontend/src/features/profile/profile-page.tsx b/apps/frontend/src/features/profile/profile-page.tsx
index a5ef6d4..de0712d 100644
--- a/apps/frontend/src/features/profile/profile-page.tsx
+++ b/apps/frontend/src/features/profile/profile-page.tsx
@@ -15,6 +15,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import type { UserProfileResponse } from '@repo/api-contract';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Link } from '@tanstack/react-router';
+import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@@ -62,19 +63,27 @@ export function ProfilePage() {
},
});
+ // Sync form with displayName when session data is loaded or updated elsewhere
+ useEffect(() => {
+ if (displayName && !updateProfileForm.formState.isDirty) {
+ updateProfileForm.reset({ fullName: displayName });
+ }
+ }, [displayName, updateProfileForm]);
+
const updateProfileMutation = useMutation({
mutationFn: updateProfile,
- onSuccess: () => {
+ onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['user-profile'] });
- updateProfileForm.reset();
+ updateProfileForm.reset({ fullName: variables.fullName });
},
});
const updatePasswordMutation = useMutation({
mutationFn: async (data: UpdatePasswordForm) => {
- // Note: Supabase doesn't require current password for password update
- // The user needs to be recently authenticated
- await updatePassword({ password: data.newPassword });
+ await updatePassword({
+ currentPassword: data.currentPassword,
+ password: data.newPassword,
+ });
},
onSuccess: () => {
updatePasswordForm.reset();
@@ -118,9 +127,7 @@ export function ProfilePage() {
{profileQuery.data?.email ?? user?.email}
diff --git a/apps/frontend/src/lib/auth.tsx b/apps/frontend/src/lib/auth.tsx index 4679ab3..7386e54 100644 --- a/apps/frontend/src/lib/auth.tsx +++ b/apps/frontend/src/lib/auth.tsx @@ -20,6 +20,7 @@ type UpdateProfileParams = { }; type UpdatePasswordParams = { + currentPassword?: string; password: string; }; @@ -172,11 +173,11 @@ export function AuthProvider({ children }: PropsWithChildren) { } return { - requiresEmailConfirmation: !signUpData?.session, + requiresEmailConfirmation: !signUpData || !('session' in signUpData), }; }, updateProfile: async ({ fullName }) => { - const { error } = await authClient.user.update({ + const { error } = await authClient.updateUser({ name: fullName, }); @@ -184,10 +185,11 @@ export function AuthProvider({ children }: PropsWithChildren) { throw error; } }, - updatePassword: async ({ password }) => { + updatePassword: async ({ currentPassword, password }) => { // Better Auth uses changePassword for authenticated users const { error } = await authClient.changePassword({ newPassword: password, + currentPassword: currentPassword || '', revokeOtherSessions: true, });