Add login page, logout, and dashboard home
parent
248b22dcba
commit
119c55bf67
@ -1,56 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
Ikuti bagian "Setup di laptop baru" di `README.md` (`docker compose up -d`, `pnpm db:migrate`,
|
|
||||||
`pnpm db:seed`).
|
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useActionState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldError,
|
||||||
|
FieldGroup,
|
||||||
|
FieldLabel,
|
||||||
|
} from "@/components/ui/field";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { login, type LoginState } from "../actions";
|
||||||
|
|
||||||
|
const initialState: LoginState = {};
|
||||||
|
|
||||||
|
export function LoginForm() {
|
||||||
|
const [state, formAction, pending] = useActionState(login, initialState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Masuk</CardTitle>
|
||||||
|
<CardDescription>Cleo Minimarket back-office</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form action={formAction}>
|
||||||
|
<FieldGroup>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel htmlFor="username">Username</FieldLabel>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
autoComplete="username"
|
||||||
|
defaultValue={state.username}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
{state.error && <FieldError>{state.error}</FieldError>}
|
||||||
|
<Button type="submit" disabled={pending}>
|
||||||
|
{pending ? "Memproses..." : "Masuk"}
|
||||||
|
</Button>
|
||||||
|
</FieldGroup>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { LoginForm } from "./_components/login-form";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Masuk",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-1 items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-sm">
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
|
||||||
|
export async function logout() {
|
||||||
|
await auth.api.signOut({ headers: await headers() });
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { getCurrentUser } from "@/data/auth";
|
||||||
|
import { logout } from "./actions";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
return (
|
||||||
|
<main className="p-4">
|
||||||
|
<Suspense fallback={<p>Loading...</p>}>
|
||||||
|
<Greeting />
|
||||||
|
</Suspense>
|
||||||
|
<form action={logout}>
|
||||||
|
<Button type="submit" variant="destructive">
|
||||||
|
Keluar
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function Greeting() {
|
||||||
|
const user = await getCurrentUser();
|
||||||
|
|
||||||
|
return <div>Halo, {user.name}!</div>;
|
||||||
|
}
|
||||||
@ -1,69 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
|
||||||
<Image
|
|
||||||
className="dark:invert h-5 w-[100px]"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={100}
|
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the{" "}
|
|
||||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
|
||||||
page.tsx
|
|
||||||
</code>{" "}
|
|
||||||
file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert h-[14px] w-4"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue