Add login server action, getCurrentUser, and progress notes

main
Deardo Satria 2 weeks ago
parent 31c211f6ab
commit 6e512a14fc

@ -35,7 +35,7 @@ Tanpa folder `src/`. Kode aplikasi di root, sejajar config.
```
app/
(auth)/login/page.tsx halaman publik (belum login)
(dashboard)/layout.tsx cek session; semua halaman di dalamnya wajib login
(dashboard)/layout.tsx kerangka UI halaman yang wajib login (bukan tempat cek session)
(dashboard)/<fitur>/
page.tsx Server Component: ambil data lewat data/, render
actions.ts Server Actions fitur ini ('use server')
@ -81,8 +81,13 @@ proxy.ts pengganti middleware di Next 16; hanya cek rin
component. `db/` dan `lib/auth.ts` sengaja tanpa itu karena dibaca CLI `auth` di luar Next.js
(package `server-only` tidak di-install; Next.js menanganinya sendiri). Hanya `data/`, `db/`,
dan `lib/auth.ts` yang membaca `process.env` untuk secret.
- `proxy.ts` hanya untuk redirect cepat berdasarkan ada/tidaknya cookie session. Pemeriksaan
session yang sebenarnya tetap di layout/page dan di setiap action.
- Session dicek lewat `getCurrentUser()` di `data/auth.ts` (redirect ke `/login` kalau tidak ada),
dipanggil oleh page dan action yang butuh user. Layout tidak dipakai untuk cek session: layout
tidak di-render ulang saat navigasi antar halaman di dalamnya.
- `cacheComponents` aktif: komponen yang membaca session (`headers()`/`cookies()`) wajib berada
di dalam `<Suspense>`, kalau tidak build gagal. Jangan `await` session di level atas layout.
- `proxy.ts` hanya untuk redirect cepat berdasarkan ada/tidaknya cookie session, bukan pengganti
`getCurrentUser()`.
- Query selalu lewat Drizzle query builder atau `sql` template tag (parameterized). Jangan
menyusun SQL dengan string concatenation.

@ -0,0 +1,73 @@
# Progress
Catatan sementara untuk melanjutkan pekerjaan di komputer lain. Hapus file ini kalau halaman
login sudah selesai.
Terakhir diperbarui: 2026-10-20.
## Sudah selesai
- Drizzle ORM + `pg` ke Postgres (`db/`, `drizzle.config.ts`, script `db:*`).
- Better Auth 1.7: login pakai username, tabel auth sudah di-migrate, sign-up publik ditutup.
Endpoint di `app/api/auth/[...all]/route.ts`.
- shadcn/ui preset `base-nova`, komponen: button, input, label, card, field, separator.
- `old-source/` di-commit tanpa `db_login.inc.php` (kredensial).
- Halaman login, langkah 1 sampai 3 dari 7:
1. `zod` terpasang.
2. `data/auth.ts`: `getCurrentUser()` cek session, redirect ke `/login` kalau belum login.
3. `app/(auth)/login/actions.ts`: server action `login` (validasi zod, lalu
`auth.api.signInUsername`, lalu `redirect("/")`).
## Berikutnya: halaman login, langkah 4 sampai 7
4. **Form login (client component)**
`app/(auth)/login/_components/login-form.tsx`, diawali `"use client"`.
Pakai `useActionState(login, {})`. Isinya `Card` berisi `FieldGroup` dengan dua `Field`
(username dan password, `name="username"` / `name="password"`), `FieldError` untuk
`state.error`, dan `Button type="submit"` yang `disabled` saat `pending`.
Input username diberi `defaultValue={state.username}` supaya tidak kosong setelah gagal login.
5. **Halaman login**
`app/(auth)/login/page.tsx`: Server Component yang menampilkan `<LoginForm />` di tengah layar.
6. **Pindahkan halaman utama ke grup dashboard**
Hapus `app/page.tsx` (bawaan create-next-app), lalu buat `app/(dashboard)/page.tsx`.
Komponen yang memanggil `getCurrentUser()` harus dibungkus `<Suspense>` karena
`cacheComponents` aktif (tanpa itu build gagal). Tampilkan nama user yang login.
7. **Logout**
`app/(dashboard)/actions.ts` dengan `"use server"`: action `logout` memanggil
`auth.api.signOut({ headers: await headers() })` lalu `redirect("/login")`.
Pasang lewat `<form action={logout}><Button type="submit">Keluar</Button></form>` di
halaman dashboard.
Cek akhir: buka `/` saat belum login, harus diarahkan ke `/login`. Login sebagai `admin`,
nama tampil di `/`. Klik Keluar, kembali ke `/login`. Lalu `pnpm exec tsc --noEmit` dan `pnpm lint`.
Setelah itu: commit, lalu rencana berikutnya (belum dimulai):
- User read-only (hanya `SELECT`) di MariaDB untuk `AMIGO_DATABASE_URL`, menggantikan `root`.
- Koneksi Drizzle ke MariaDB di `db/amigo/` dengan `drizzle-kit pull` (jangan pernah migrate/push).
- `proxy.ts` untuk redirect cepat ke `/login` kalau tidak ada cookie session.
## Menyiapkan komputer lain
Yang tidak ada di git dan harus disiapkan ulang:
1. `.env`: isi `DATABASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, `AMIGO_DATABASE_URL`.
Buat `BETTER_AUTH_SECRET` baru:
`node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`
2. Postgres lokal:
`docker run -d --name cleo-postgres -e POSTGRES_PASSWORD=<password> -e POSTGRES_DB=cleo_auth -p 5432:5432 -v cleo-pgdata:/var/lib/postgresql/data postgres:17`
3. `pnpm install`, lalu `pnpm db:migrate` untuk membuat tabel auth.
4. Akun admin dibuat ulang. Sign-up sedang ditutup, jadi sementara hapus
`disableSignUp: true` di `lib/auth.ts`, jalankan `pnpm dev`, kirim request sign-up:
```powershell
$body = @{ name = "Administrator"; email = "admin@cleo.local"; username = "admin"; password = "<password>" } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "http://localhost:3000/api/auth/sign-up/email" -ContentType "application/json" -Headers @{ Origin = "http://localhost:3000" } -Body $body
```
lalu kembalikan `disableSignUp: true`.
5. MariaDB `cleo` di `192.168.3.3` hanya bisa diakses dari jaringan kantor. Halaman login tidak
membutuhkannya.

@ -0,0 +1,50 @@
"use server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { isAPIError } from "better-auth/api";
import { z } from "zod";
import { auth } from "@/lib/auth";
const loginSchema = z.object({
username: z.string().trim().min(1, "Username wajib diisi"),
password: z.string().min(1, "Password wajib diisi"),
});
export type LoginState = {
error?: string;
username?: string;
};
export async function login(
_prevState: LoginState,
formData: FormData,
): Promise<LoginState> {
const parsed = loginSchema.safeParse({
username: formData.get("username"),
password: formData.get("password"),
});
if (!parsed.success) {
return {
error: parsed.error.issues[0].message,
username: String(formData.get("username") ?? ""),
};
}
try {
await auth.api.signInUsername({
body: parsed.data,
headers: await headers(),
});
} catch (error) {
if (isAPIError(error)) {
return {
error: "Username atau password salah",
username: parsed.data.username,
};
}
throw error;
}
redirect("/");
}

@ -0,0 +1,21 @@
import "server-only";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
export type CurrentUser = {
id: string;
name: string;
username: string | null;
};
export async function getCurrentUser(): Promise<CurrentUser> {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) redirect("/login");
return {
id: session.user.id,
name: session.user.name,
username: session.user.username ?? null,
};
}

@ -23,7 +23,8 @@
"react": "19.3.0",
"react-dom": "19.3.0",
"shadcn": "^4.22.1",
"tw-animate-css": "^1.4.0"
"tw-animate-css": "^1.4.0",
"zod": "^4.6.7"
},
"devDependencies": {
"@tailwindcss/turbopack": "^4",

@ -44,6 +44,9 @@ importers:
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
zod:
specifier: ^4.6.7
version: 4.6.7
devDependencies:
'@tailwindcss/turbopack':
specifier: ^4

Loading…
Cancel
Save