Harden login and refresh production deploy guide

This commit is contained in:
2026-05-17 05:52:09 +07:00
parent 9141f99f6a
commit 1274f2b822
5 changed files with 255 additions and 197 deletions

View File

@ -0,0 +1,26 @@
server {
listen 80;
listen [::]:80;
server_name abelbirdnest.id www.abelbirdnest.id;
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:3007;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 120s;
}
location /api/v1/health {
proxy_pass http://127.0.0.1:3007;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto http;
}
}

View File

@ -4,10 +4,10 @@ After=network.target postgresql.service
[Service] [Service]
Type=simple Type=simple
WorkingDirectory=/var/www/abelbirdnest-web WorkingDirectory=/var/www/abelbirdnest-web/AbelBirdNest-Stock
Environment=NODE_ENV=production Environment=NODE_ENV=production
Environment=PORT=3007 Environment=PORT=3007
EnvironmentFile=/var/www/abelbirdnest-web/.env.production EnvironmentFile=/var/www/abelbirdnest-web/AbelBirdNest-Stock/.env.production
ExecStart=/usr/bin/npm run start ExecStart=/usr/bin/npm run start
Restart=always Restart=always
RestartSec=5 RestartSec=5

View File

@ -1,6 +1,6 @@
# Deploy Production # Deploy Production
Dokumen ini menyiapkan deploy production untuk: Panduan ini untuk deploy production dengan asumsi:
- domain `abelbirdnest.id` - domain `abelbirdnest.id`
- reverse proxy `nginx` - reverse proxy `nginx`
@ -8,26 +8,26 @@ Dokumen ini menyiapkan deploy production untuk:
- database `PostgreSQL` - database `PostgreSQL`
- source code dari git `https://git.iptek.co/wirabasalamah/AbelBirdNest-Stock.git` - source code dari git `https://git.iptek.co/wirabasalamah/AbelBirdNest-Stock.git`
- user service khusus `abelbirdnest` - user service khusus `abelbirdnest`
- repo berada di:
`/var/www/abelbirdnest-web/AbelBirdNest-Stock`
## 1. Persiapan Server ## 1. Persiapan Server
Siapkan: Install:
- Node.js LTS - Node.js LTS
- npm - npm
- PostgreSQL - PostgreSQL
- nginx - nginx
- certbot / SSL Lets Encrypt - certbot
Direktori contoh: Direktori aplikasi:
```bash ```bash
/var/www/abelbirdnest-web /var/www/abelbirdnest-web
``` ```
## 2. Buat User Khusus Aplikasi ## 2. Buat User OS Khusus Aplikasi
Jalankan sebagai `root` atau dengan `sudo`:
```bash ```bash
sudo useradd -r -m -d /var/www/abelbirdnest-web -s /bin/bash abelbirdnest sudo useradd -r -m -d /var/www/abelbirdnest-web -s /bin/bash abelbirdnest
@ -35,54 +35,44 @@ sudo mkdir -p /var/www/abelbirdnest-web
sudo chown -R abelbirdnest:abelbirdnest /var/www/abelbirdnest-web sudo chown -R abelbirdnest:abelbirdnest /var/www/abelbirdnest-web
``` ```
Catatan:
- user `abelbirdnest` dipakai khusus untuk menjalankan service aplikasi
- jangan jalankan app production dengan user pribadi atau `root`
## 3. Clone Repo dari Git
Masuk sebagai user aplikasi: Masuk sebagai user aplikasi:
```bash ```bash
sudo -u abelbirdnest -H bash sudo -u abelbirdnest -H bash
cd /var/www/abelbirdnest-web cd /var/www/abelbirdnest-web
git clone https://git.iptek.co/wirabasalamah/AbelBirdNest-Stock.git .
``` ```
Kalau server butuh autentikasi git internal, siapkan credential sesuai kebijakan server Git Anda. ## 3. Clone Repo
Catatan: Clone normal:
- perintah di atas hanya benar jika `/var/www/abelbirdnest-web` masih kosong
- jika Anda sudah menjalankan clone biasa dan hasilnya menjadi:
`/var/www/abelbirdnest-web/AbelBirdNest-Stock`
maka lanjutkan semua perintah deploy dari folder itu:
```bash ```bash
git clone https://git.iptek.co/wirabasalamah/AbelBirdNest-Stock.git
cd /var/www/abelbirdnest-web/AbelBirdNest-Stock cd /var/www/abelbirdnest-web/AbelBirdNest-Stock
``` ```
- alternatifnya, jika ingin struktur tanpa subfolder tambahan, hapus isi folder tujuan lalu clone ulang dengan titik: Catatan:
- panduan ini mengikuti struktur clone normal di atas
- jadi `WorkingDirectory`, `.env.production`, dan semua perintah memakai path:
`/var/www/abelbirdnest-web/AbelBirdNest-Stock`
## 4. Siapkan Environment Production
Salin file contoh:
```bash ```bash
rm -rf /var/www/abelbirdnest-web/AbelBirdNest-Stock cp .env.production.example .env.production
cd /var/www/abelbirdnest-web
git clone https://git.iptek.co/wirabasalamah/AbelBirdNest-Stock.git .
``` ```
## 4. Environment Production Isi minimal:
Salin `.env.production.example` menjadi `.env.production`, lalu isi nilainya.
Yang wajib:
```env ```env
NODE_ENV=production NODE_ENV=production
PORT=3007 PORT=3007
APP_URL=https://abelbirdnest.id APP_URL=https://abelbirdnest.id
DATABASE_URL=postgresql://... DATABASE_URL=postgresql://abelbirdnest_app:password@127.0.0.1:5432/abelbirdnest_prod?schema=public
AUTH_SECRET=... AUTH_SECRET=ganti-dengan-random-string-panjang
AUTH_BOOTSTRAP=false AUTH_BOOTSTRAP=false
SMTP_HOST=... SMTP_HOST=...
SMTP_PORT=465 SMTP_PORT=465
@ -94,20 +84,13 @@ SMTP_FROM=...
Catatan: Catatan:
- `AUTH_SECRET` harus random panjang. - `AUTH_BOOTSTRAP=false` wajib di production
- `AUTH_BOOTSTRAP=false` wajib untuk production. - akun default dev tidak akan aktif
- `APP_URL` harus domain production final. - `AUTH_SECRET` harus random dan panjang
## 5. Inisialisasi Database PostgreSQL ## 5. Inisialisasi PostgreSQL
Contoh di bawah memakai: Masuk sebagai superuser PostgreSQL:
- database: `abelbirdnest_prod`
- database user: `abelbirdnest_app`
- host: `127.0.0.1`
- port: `5432`
Masuk ke PostgreSQL sebagai superuser:
```bash ```bash
sudo -u postgres psql sudo -u postgres psql
@ -119,26 +102,16 @@ Buat user database:
CREATE USER abelbirdnest_app WITH PASSWORD 'ganti-dengan-password-yang-kuat'; CREATE USER abelbirdnest_app WITH PASSWORD 'ganti-dengan-password-yang-kuat';
``` ```
Buat database production: Buat database:
```sql ```sql
CREATE DATABASE abelbirdnest_prod OWNER abelbirdnest_app; CREATE DATABASE abelbirdnest_prod OWNER abelbirdnest_app;
```
Pastikan owner database benar:
```sql
ALTER DATABASE abelbirdnest_prod OWNER TO abelbirdnest_app; ALTER DATABASE abelbirdnest_prod OWNER TO abelbirdnest_app;
```
Opsional tapi disarankan, kunci privilege default:
```sql
REVOKE ALL ON DATABASE abelbirdnest_prod FROM PUBLIC; REVOKE ALL ON DATABASE abelbirdnest_prod FROM PUBLIC;
GRANT ALL PRIVILEGES ON DATABASE abelbirdnest_prod TO abelbirdnest_app; GRANT ALL PRIVILEGES ON DATABASE abelbirdnest_prod TO abelbirdnest_app;
``` ```
Keluar dari `psql`: Keluar:
```sql ```sql
\q \q
@ -150,68 +123,79 @@ Tes koneksi:
psql "postgresql://abelbirdnest_app:ganti-dengan-password-yang-kuat@127.0.0.1:5432/abelbirdnest_prod" psql "postgresql://abelbirdnest_app:ganti-dengan-password-yang-kuat@127.0.0.1:5432/abelbirdnest_prod"
``` ```
Jika koneksi berhasil, pakai URL itu di `.env.production`:
```env
DATABASE_URL="postgresql://abelbirdnest_app:ganti-dengan-password-yang-kuat@127.0.0.1:5432/abelbirdnest_prod?schema=public"
```
Catatan: Catatan:
- untuk `psql`, jangan tambahkan `?schema=public` - untuk `psql`, jangan pakai `?schema=public`
- untuk Prisma `DATABASE_URL`, tetap gunakan `?schema=public` - untuk `DATABASE_URL` Prisma, tetap pakai `?schema=public`
## 6. Install Dependency, Database & Migration ## 6. Install Dependency dan Buat Tabel
Repo ini sudah disiapkan memakai migration Prisma. Masih sebagai user `abelbirdnest`:
Jalankan:
```bash ```bash
cd /var/www/abelbirdnest-web/AbelBirdNest-Stock cd /var/www/abelbirdnest-web/AbelBirdNest-Stock
npm install npm install
npm run prisma:generate npm run prisma:generate
```
Load env production ke shell saat menjalankan command manual:
```bash
set -a
source .env.production
set +a
```
Jalankan migration:
```bash
npm run prisma:migrate:deploy npm run prisma:migrate:deploy
``` ```
Kalau perlu isi master awal: Opsional cek status:
```bash ```bash
npm run seed:master npx prisma migrate status
``` ```
Data seed yang dibawa: ## 7. Seed Data Awal
- grade Untuk fresh database:
- bank
- currency
### Khusus Seed Grade
Seed `grade` membutuhkan file sumber `Grade.xls`.
Sebelum menjalankan:
```bash ```bash
npm run seed:master cd /var/www/abelbirdnest-web/AbelBirdNest-Stock
set -a
source .env.production
set +a
npm run seed:banks
npm run seed:currencies
``` ```
unggah dulu file `Grade.xls` ke server, misalnya ke: ### Khusus Grade
`seed:grades` butuh file `Grade.xls`.
Buat folder data:
```bash
mkdir -p scripts/data
```
Upload file `Grade.xls` ke:
```bash ```bash
/var/www/abelbirdnest-web/AbelBirdNest-Stock/scripts/data/Grade.xls /var/www/abelbirdnest-web/AbelBirdNest-Stock/scripts/data/Grade.xls
``` ```
Contoh dari laptop lokal:
```bash
scp "Grade.xls" user@server:/var/www/abelbirdnest-web/AbelBirdNest-Stock/scripts/data/Grade.xls
```
Lalu jalankan: Lalu jalankan:
```bash ```bash
cd /var/www/abelbirdnest-web/AbelBirdNest-Stock cd /var/www/abelbirdnest-web/AbelBirdNest-Stock
set -a
source .env.production
set +a
npm run seed:grades npm run seed:grades
``` ```
@ -221,132 +205,210 @@ Alternatif jika file ada di lokasi lain:
node scripts/seed-grades-from-xls.mjs /path/ke/Grade.xls node scripts/seed-grades-from-xls.mjs /path/ke/Grade.xls
``` ```
atau: ## 8. Buat User Pertama di PostgreSQL
Karena `AUTH_BOOTSTRAP=false`, Anda harus buat user login pertama sendiri.
### 8.1 Generate Hash Password
Ganti password contoh ini:
```bash ```bash
GRADE_XLS_PATH=/path/ke/Grade.xls npm run seed:grades node -e 'const {randomBytes,scryptSync}=require("crypto"); const p="GantiPasswordKuat123!"; const salt=randomBytes(16).toString("hex"); const derived=scryptSync(p,salt,64).toString("hex"); console.log(`${salt}:${derived}`)'
``` ```
Urutan pertama kali untuk fresh database: Simpan output hash-nya.
### 8.2 Masuk ke PostgreSQL
```bash
psql "postgresql://abelbirdnest_app:password@127.0.0.1:5432/abelbirdnest_prod"
```
### 8.3 Pastikan Role `SYSTEM_ADMIN` Ada
```sql
INSERT INTO roles (code, name, created_at, updated_at)
VALUES ('SYSTEM_ADMIN', 'System Admin', NOW(), NOW())
ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name,
updated_at = NOW();
```
### 8.4 Buat User Pertama
Ganti:
- `Nama Anda`
- `superadmin`
- `superadmin@abelbirdnest.id`
- `HASH_HASIL_LANGKAH_8_1`
```sql
INSERT INTO users (
role_id,
name,
username,
email,
email_verified_at,
phone,
password_hash,
status,
created_at,
updated_at
)
SELECT
r.id,
'Nama Anda',
'superadmin',
'superadmin@abelbirdnest.id',
NOW(),
NULL,
'HASH_HASIL_LANGKAH_8_1',
'ACTIVE',
NOW(),
NOW()
FROM roles r
WHERE r.code = 'SYSTEM_ADMIN'
ON CONFLICT (email) DO UPDATE
SET role_id = EXCLUDED.role_id,
name = EXCLUDED.name,
username = EXCLUDED.username,
email_verified_at = EXCLUDED.email_verified_at,
password_hash = EXCLUDED.password_hash,
status = EXCLUDED.status,
updated_at = NOW();
```
Verifikasi:
```sql
SELECT u.id, u.name, u.username, u.email, u.status, r.code AS role
FROM users u
JOIN roles r ON r.id = u.role_id
WHERE u.email = 'superadmin@abelbirdnest.id';
```
## 9. Build Production
```bash ```bash
cd /var/www/abelbirdnest-web/AbelBirdNest-Stock cd /var/www/abelbirdnest-web/AbelBirdNest-Stock
npm install set -a
npm run prisma:generate source .env.production
npm run prisma:migrate:deploy set +a
npm run seed:banks
npm run seed:currencies
npm run seed:grades
```
Catatan:
- `prisma:migrate:deploy` akan membuat seluruh tabel dari migration yang ada di repo
- `seed:banks` dan `seed:currencies` bisa langsung dijalankan
- `seed:grades` butuh file `Grade.xls` lebih dulu
- user login production tetap harus dibuat terpisah, jangan mengandalkan akun dev/default
## 7. Build Production
```bash
cd /var/www/abelbirdnest-web/AbelBirdNest-Stock
npm run build npm run build
``` ```
## 8. Jalankan App di Port 3007 ## 10. Jalankan App dengan systemd
Manual: File service repo sudah disiapkan untuk struktur subfolder ini:
```bash
PORT=3007 npm run start
```
Atau gunakan `systemd` dari:
```bash ```bash
deploy/systemd/abelbirdnest-web.service deploy/systemd/abelbirdnest-web.service
``` ```
Contoh setup: Pasang:
```bash ```bash
sudo cp deploy/systemd/abelbirdnest-web.service /etc/systemd/system/ sudo cp /var/www/abelbirdnest-web/AbelBirdNest-Stock/deploy/systemd/abelbirdnest-web.service /etc/systemd/system/
sudo chown -R abelbirdnest:abelbirdnest /var/www/abelbirdnest-web/AbelBirdNest-Stock
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable abelbirdnest-web sudo systemctl enable abelbirdnest-web
sudo systemctl start abelbirdnest-web sudo systemctl restart abelbirdnest-web
sudo systemctl status abelbirdnest-web sudo systemctl status abelbirdnest-web
``` ```
Autostart saat server restart terjadi karena service di-`enable`. Verifikasi port:
Untuk verifikasi:
```bash ```bash
sudo systemctl is-enabled abelbirdnest-web ss -ltnp | grep 3007
curl http://127.0.0.1:3007/api/v1/health
``` ```
## 9. Reverse Proxy Nginx Kalau service gagal, cek:
Gunakan file:
```bash ```bash
deploy/nginx/abelbirdnest.id.conf sudo systemctl cat abelbirdnest-web
sudo journalctl -u abelbirdnest-web -n 100 --no-pager
which npm
```
## 11. Pasang Nginx Tahap 1: HTTP Dulu
Jangan langsung pakai config HTTPS sebelum sertifikat ada.
Pakai file ini dulu:
```bash
deploy/nginx/abelbirdnest.id.http.conf
``` ```
Pasang: Pasang:
```bash ```bash
sudo cp deploy/nginx/abelbirdnest.id.conf /etc/nginx/sites-available/abelbirdnest.id.conf sudo cp /var/www/abelbirdnest-web/AbelBirdNest-Stock/deploy/nginx/abelbirdnest.id.http.conf /etc/nginx/sites-available/abelbirdnest.id.conf
sudo ln -s /etc/nginx/sites-available/abelbirdnest.id.conf /etc/nginx/sites-enabled/abelbirdnest.id.conf sudo ln -sf /etc/nginx/sites-available/abelbirdnest.id.conf /etc/nginx/sites-enabled/abelbirdnest.id.conf
sudo nginx -t sudo nginx -t
sudo systemctl reload nginx sudo systemctl reload nginx
``` ```
## 10. Health Check Tes:
Endpoint health:
```bash ```bash
GET /api/v1/health curl http://abelbirdnest.id/api/v1/health
``` ```
Contoh: ## 12. Buat Sertifikat SSL
Setelah HTTP sudah hidup:
```bash
sudo certbot --nginx -d abelbirdnest.id -d www.abelbirdnest.id
```
## 13. Ganti ke Config HTTPS Final
Setelah sertifikat berhasil dibuat, baru pakai config final:
```bash
sudo cp /var/www/abelbirdnest-web/AbelBirdNest-Stock/deploy/nginx/abelbirdnest.id.conf /etc/nginx/sites-available/abelbirdnest.id.conf
sudo nginx -t
sudo systemctl reload nginx
```
Verifikasi:
```bash ```bash
curl https://abelbirdnest.id/api/v1/health curl https://abelbirdnest.id/api/v1/health
``` ```
## 11. Update Deployment Berikutnya ## 14. Update Deployment Berikutnya
Jika aplikasi sudah live dan ada update dari git:
```bash ```bash
cd /var/www/abelbirdnest-web/AbelBirdNest-Stock cd /var/www/abelbirdnest-web/AbelBirdNest-Stock
git pull origin main git pull origin main
set -a
source .env.production
set +a
npm install npm install
npm run prisma:generate
npm run prisma:migrate:deploy npm run prisma:migrate:deploy
npm run build npm run build
sudo systemctl restart abelbirdnest-web sudo systemctl restart abelbirdnest-web
sudo systemctl status abelbirdnest-web
``` ```
Jika branch utama nanti bukan `main`, sesuaikan perintah `git pull`. ## 15. Smoke Test Setelah Live
## 12. Checklist Go-Live Cek minimal:
- `AUTH_BOOTSTRAP=false` - login dengan user `SYSTEM_ADMIN`
- `AUTH_SECRET` sudah production-grade - dashboard terbuka
- `APP_URL=https://abelbirdnest.id` - health check OK
- SSL aktif - buat master `bank`, `currency`, `grade` tampil
- database backup aktif - buat transaksi sederhana
- `npm run build` lulus - logout/login ulang
- `npm run prisma:migrate:deploy` lulus - email reset/verifikasi jika SMTP sudah aktif
- `npm run seed:master` selesai jika dibutuhkan
- login, reset password, dan email verifikasi sudah dites
- create purchase, receipt, lot, sale sudah dites
## 13. Catatan Penting
- Jangan pakai `npm run db:push` untuk production.
- Jangan pakai akun default development.
- Jangan simpan `.env.production` di repo.
- Pastikan ownership file tetap `abelbirdnest:abelbirdnest`.

View File

@ -31,6 +31,9 @@ export async function AppShell({
<MobileNav pathname={pathname} userRole={user.role} /> <MobileNav pathname={pathname} userRole={user.role} />
<Topbar title={copy.title} description={copy.description} user={user} /> <Topbar title={copy.title} description={copy.description} user={user} />
<div className="flex-1 p-6">{children}</div> <div className="flex-1 p-6">{children}</div>
<footer className="border-t border-line/70 px-6 py-4 text-center text-sm text-slate-500">
© 2026 AbelBirdnest
</footer>
</div> </div>
</main> </main>
</div> </div>

View File

@ -6,28 +6,20 @@ import { FormEvent, useState } from "react";
import { AppLogo } from "@/components/branding/app-logo"; import { AppLogo } from "@/components/branding/app-logo";
import { useLocale } from "@/components/providers/locale-provider"; import { useLocale } from "@/components/providers/locale-provider";
import { defaultAuthAccounts } from "@/features/auth/lib/default-accounts";
export function LoginClient() { export function LoginClient() {
const { dict } = useLocale(); const { dict } = useLocale();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [identity, setIdentity] = useState("admin@abelbirdnest.local"); const [identity, setIdentity] = useState("");
const [password, setPassword] = useState("admin123"); const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [helperEmail, setHelperEmail] = useState("admin@abelbirdnest.local"); const [helperEmail, setHelperEmail] = useState("");
const [helperSubmitting, setHelperSubmitting] = useState<"reset" | "verify" | null>(null); const [helperSubmitting, setHelperSubmitting] = useState<"reset" | "verify" | null>(null);
const [helperMessage, setHelperMessage] = useState<string | null>(null); const [helperMessage, setHelperMessage] = useState<string | null>(null);
function applyAccount(identityValue: string, passwordValue: string) {
setIdentity(identityValue);
setPassword(passwordValue);
setHelperEmail(identityValue);
setError(null);
}
async function submitHelper(type: "reset" | "verify") { async function submitHelper(type: "reset" | "verify") {
setHelperSubmitting(type); setHelperSubmitting(type);
setHelperMessage(null); setHelperMessage(null);
@ -246,31 +238,6 @@ export function LoginClient() {
<span>{dict.login.privacy}</span> <span>{dict.login.privacy}</span>
<span>{dict.login.terms}</span> <span>{dict.login.terms}</span>
</div> </div>
<div className="mt-8 rounded-lg border border-blue-200 bg-blue-50 px-4 py-4 text-left text-[13px] leading-6 text-slate-600">
<p className="font-semibold text-slate-700">{dict.login.devAccounts}</p>
<div className="mt-3 space-y-3">
{defaultAuthAccounts.map((account) => (
<button
key={account.roleCode}
type="button"
onClick={() => applyAccount(account.email, account.password)}
className="block w-full rounded-lg border border-blue-200 bg-white px-3 py-3 text-left transition hover:border-moss/40 hover:bg-moss/5"
>
<div className="flex items-center justify-between gap-3">
<span className="font-semibold text-ink">{account.roleLabel}</span>
<span className="text-[11px] font-bold uppercase tracking-[0.08em] text-slate-500">
{account.username}
</span>
</div>
<div className="mt-2 text-[12px] text-slate-600">{account.email}</div>
<div className="text-[12px] text-slate-600">Password: <span className="font-semibold text-ink">{account.password}</span></div>
</button>
))}
</div>
<p className="mt-3 text-[12px] text-slate-500">
{dict.login.autoFillHint}
</p>
</div>
</footer> </footer>
</div> </div>
</section> </section>
@ -278,7 +245,7 @@ export function LoginClient() {
<div className="pointer-events-none fixed bottom-0 left-0 flex w-full justify-between px-4 py-3 text-[11px] font-bold uppercase tracking-[0.08em] text-slate-400"> <div className="pointer-events-none fixed bottom-0 left-0 flex w-full justify-between px-4 py-3 text-[11px] font-bold uppercase tracking-[0.08em] text-slate-400">
<span>v2.4.0-pro</span> <span>v2.4.0-pro</span>
<span>© 2024 AbelBirdnest Stock</span> <span>© 2026 AbelBirdnest</span>
</div> </div>
</main> </main>
); );