Compare commits
2
Commits
e7f465f74f
...
4e59f6e25c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e59f6e25c | ||
|
|
593c149478 |
@@ -81,7 +81,7 @@ v1.3/
|
|||||||
## 🔐 Login
|
## 🔐 Login
|
||||||
|
|
||||||
The whole system is behind a login page (`/login`). Login is **in-page**:
|
The whole system is behind a login page (`/login`). Login is **in-page**:
|
||||||
the form (username / password / optional organization) is sent to
|
the form (username / password) is sent to
|
||||||
`POST /api/v1/auth/login`; the backend exchanges it with Casdoor
|
`POST /api/v1/auth/login`; the backend exchanges it with Casdoor
|
||||||
(password grant), verifies the returned JWT, and starts the session —
|
(password grant), verifies the returned JWT, and starts the session —
|
||||||
no browser redirect to Casdoor.
|
no browser redirect to Casdoor.
|
||||||
@@ -113,6 +113,71 @@ Casdoor organization, application and users (idempotent). Full details —
|
|||||||
addresses, credentials, role model, failure scenarios and the checklist —
|
addresses, credentials, role model, failure scenarios and the checklist —
|
||||||
are in [`docs/casdoor-auth.md`](docs/casdoor-auth.md).
|
are in [`docs/casdoor-auth.md`](docs/casdoor-auth.md).
|
||||||
|
|
||||||
|
> The **currently committed seed** (`backups/علی محقق/`) instead ships a
|
||||||
|
> single admin — `ali.mohaghegh` («علی محقق») — see
|
||||||
|
> [Restoring the committed seed data](#restoring-the-committed-seed-data).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Working with the deployed system (server)
|
||||||
|
|
||||||
|
The production deployment of this stack runs on the server and is reachable
|
||||||
|
at these public addresses (HTTPS):
|
||||||
|
|
||||||
|
| What | Address |
|
||||||
|
|---|---|
|
||||||
|
| **App (ERP)** | https://aramesh.bennu-api-bahrestan.ir/ |
|
||||||
|
| **Casdoor admin panel** | https://casdoor.bennu-api-bahrestan.ir/ |
|
||||||
|
|
||||||
|
Log in to the app with your Casdoor account (org `inventory`); the role is
|
||||||
|
assigned inside the app (`users.role` — see «Login» above and
|
||||||
|
[`docs/casdoor-auth.md`](docs/casdoor-auth.md)).
|
||||||
|
|
||||||
|
### MinIO console on the server (SSH tunnel)
|
||||||
|
|
||||||
|
The MinIO console is deliberately **loopback-only** on the server
|
||||||
|
(`127.0.0.1:9001`) — verified unreachable from the internet — so the way in
|
||||||
|
is an SSH tunnel from your machine:
|
||||||
|
|
||||||
|
1. Open PowerShell and start the tunnel (**keep the window open** while you
|
||||||
|
use the panel):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh -L 9001:127.0.0.1:9001 root@171.22.25.75
|
||||||
|
```
|
||||||
|
|
||||||
|
You will also land in a normal SSH session — that is fine; the tunnel
|
||||||
|
lives as long as the window stays open.
|
||||||
|
|
||||||
|
2. In your browser, open: **http://localhost:9001**
|
||||||
|
|
||||||
|
3. Log in with:
|
||||||
|
|
||||||
|
| Username | Password |
|
||||||
|
|---|---|
|
||||||
|
| `aramesh-minio` | `feb10fadb5441019dfc33bf12a9b0f74e7b6c478` |
|
||||||
|
|
||||||
|
You will see the **`procurement-files`** bucket — the invoices/proformas the
|
||||||
|
ERP stores, browsable and downloadable from there.
|
||||||
|
|
||||||
|
⚠️ These credentials are **full admin access to the file storage**. Treat
|
||||||
|
them as a production secret (this README is inside the repo, so anyone with
|
||||||
|
repo access can read them — rotate them on the server if they ever leak).
|
||||||
|
|
||||||
|
Close the PowerShell window (or `Ctrl+C`) when you are done to drop the
|
||||||
|
tunnel.
|
||||||
|
|
||||||
|
Notes from the server setup:
|
||||||
|
|
||||||
|
- The panel is published only on the server's loopback, so the internet
|
||||||
|
cannot reach it at all — the tunnel is what carries it to your browser.
|
||||||
|
- Host port `9000` stays reserved for the deploy-webhook receiver, which is
|
||||||
|
why the console uses `9001`.
|
||||||
|
- If you ever need the raw S3 API from your machine, or a clickable URL for
|
||||||
|
the console (e.g. `storage.aramesh.bennu-api-bahrestan.ir` behind Traefik
|
||||||
|
with a password prompt on top), that can be arranged on request — the
|
||||||
|
tunnel remains the safer default.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prerequisites (target machine)
|
## Prerequisites (target machine)
|
||||||
@@ -197,26 +262,35 @@ docker exec -it procurement-postgres psql -U postgres -d procurement
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Restoring the demo data (for the team)
|
## Restoring the committed seed data
|
||||||
|
|
||||||
The databases are **not** in git — Docker volumes hold the data. To let
|
The databases are **not** in git — Docker volumes hold the data. The repo
|
||||||
everyone see the same system state, the repo ships exact database dumps in
|
ships recovery dumps in `backups/علی محقق/`:
|
||||||
`backups/`. After a fresh boot:
|
|
||||||
|
| File | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `casdoor.dump` | Casdoor schema + console admin (`admin` / `123`), org `inventory`, app `inventory-app`, the JWT signing cert, and the single org user `ali.mohaghegh` |
|
||||||
|
| `procurement.dump` | Procurement schema + the single `users` row «علی محقق» with role `admin`; all other tables empty (categories/units re-seed at API startup) |
|
||||||
|
|
||||||
|
⚠️ This **replaces all data** in the `procurement` and `casdoor` databases.
|
||||||
|
On a running stack:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell -ExecutionPolicy Bypass -File .\backups\restore.ps1 # Windows
|
docker compose stop api casdoor
|
||||||
bash backups/restore.sh # Linux / Git Bash
|
docker cp "backups/علی محقق/procurement.dump" procurement-postgres:/tmp/
|
||||||
|
docker cp "backups/علی محقق/casdoor.dump" procurement-postgres:/tmp/
|
||||||
|
docker exec procurement-postgres pg_restore -U postgres -d procurement --clean --if-exists /tmp/procurement.dump
|
||||||
|
docker exec procurement-postgres pg_restore -U postgres -d casdoor --clean --if-exists /tmp/casdoor.dump
|
||||||
|
docker compose start casdoor api
|
||||||
```
|
```
|
||||||
|
|
||||||
This **replaces all data** in the `procurement` and `casdoor` databases with
|
Login afterwards: `ali.mohaghegh` / `hulkfuck002` (Casdoor rejects spaces
|
||||||
the committed state — a clean, realistic demo: 3 projects × 2 warehouses,
|
in usernames, so «ali mohaghegh» is not a valid login name).
|
||||||
4 suppliers, 12 materials across all 8 categories, and 4 quotes covering
|
|
||||||
every lifecycle state (offered / in-transit / partially received /
|
|
||||||
completed) with matching stock.
|
|
||||||
|
|
||||||
Two alternative ways to get a clean state:
|
Want demo data instead? Two seeders are still available (they create
|
||||||
|
different users — see the Login table above):
|
||||||
|
|
||||||
1. **App seeder** (realistic demo data, keeps the Casdoor org as-is):
|
1. **App seeder** (realistic demo dataset; keeps the Casdoor org as-is):
|
||||||
```powershell
|
```powershell
|
||||||
docker exec procurement-api sh -c "cd /srv/backend && python -m tools.seed"
|
docker exec procurement-api sh -c "cd /srv/backend && python -m tools.seed"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ router = APIRouter(prefix="/auth", tags=["auth"])
|
|||||||
class LoginRequest(BaseModel):
|
class LoginRequest(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
organization: str | None = None # optional override; defaults to CASDOOR_ORGANIZATION
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login")
|
@router.post("/login")
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ to re-run).
|
|||||||
## 2. Login flow (in-page, no redirect)
|
## 2. Login flow (in-page, no redirect)
|
||||||
|
|
||||||
```
|
```
|
||||||
Browser ──► POST /api/v1/auth/login {username, password, organization?}
|
Browser ──► POST /api/v1/auth/login {username, password}
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Backend ──► POST http://casdoor:8000/api/login/oauth/access_token
|
Backend ──► POST http://casdoor:8000/api/login/oauth/access_token
|
||||||
|
|||||||
@@ -1460,7 +1460,6 @@ export const apiV26 = {
|
|||||||
login: async (body: {
|
login: async (body: {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
organization?: string;
|
|
||||||
}): Promise<{ access_token: string; expires_in: number | null; user: import("../store/auth").AuthUser }> => {
|
}): Promise<{ access_token: string; expires_in: number | null; user: import("../store/auth").AuthUser }> => {
|
||||||
const res = await request<{
|
const res = await request<{
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ClipboardList, Loader2, Lock, ShieldCheck, Wrench, X } from "lucide-react";
|
import { ClipboardList, Eye, EyeOff, Loader2, Lock, ShieldCheck, Wrench, X } from "lucide-react";
|
||||||
import { api, apiV26 } from "../../lib/api";
|
import { api, apiV26 } from "../../lib/api";
|
||||||
import { useAuthStore } from "../../store/auth";
|
import { useAuthStore } from "../../store/auth";
|
||||||
import type { Project } from "../../lib/types";
|
import type { Project } from "../../lib/types";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ورود به سامانه — Casdoor درونصفحهای (بدون ریدایرکت)
|
// ورود به سامانه — Casdoor درونصفحهای (بدون ریدایرکت)
|
||||||
// فرم اصلی: Username / Password / Organization (اختیاری) →
|
// فرم اصلی: Username / Password →
|
||||||
// POST /api/v1/auth/login (بکاند با grant_type=password از
|
// POST /api/v1/auth/login (بکاند با grant_type=password از
|
||||||
// Casdoor توکن میگیرد و نقش را از جدول users خودمان برمیگرداند)
|
// Casdoor توکن میگیرد و نقش را از جدول users خودمان برمیگرداند)
|
||||||
// حالت توسعه (فقط ادمین — سوپر یوزر) — چرخهی ۳ مرحلهای:
|
// حالت توسعه (فقط ادمین — سوپر یوزر) — چرخهی ۳ مرحلهای:
|
||||||
@@ -36,7 +36,7 @@ export function LoginPage() {
|
|||||||
// ---- فرم اصلی (Casdoor) ----
|
// ---- فرم اصلی (Casdoor) ----
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [organization, setOrganization] = useState("");
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// ---- پنل حالت توسعه (۳ مرحله) ----
|
// ---- پنل حالت توسعه (۳ مرحله) ----
|
||||||
@@ -71,7 +71,6 @@ export function LoginPage() {
|
|||||||
const { access_token, user } = await apiV26.auth.login({
|
const { access_token, user } = await apiV26.auth.login({
|
||||||
username: username.trim(),
|
username: username.trim(),
|
||||||
password,
|
password,
|
||||||
...(organization.trim() ? { organization: organization.trim() } : {}),
|
|
||||||
});
|
});
|
||||||
if (!user.role) {
|
if (!user.role) {
|
||||||
toast.error("نقش شما هنوز در سیستم تعیین نشده است — با مدیر سیستم تماس بگیرید");
|
toast.error("نقش شما هنوز در سیستم تعیین نشده است — با مدیر سیستم تماس بگیرید");
|
||||||
@@ -101,7 +100,6 @@ export function LoginPage() {
|
|||||||
const { user: adminUser } = await apiV26.auth.login({
|
const { user: adminUser } = await apiV26.auth.login({
|
||||||
username: devAdminUser.trim(),
|
username: devAdminUser.trim(),
|
||||||
password: devAdminPass,
|
password: devAdminPass,
|
||||||
...(organization.trim() ? { organization: organization.trim() } : {}),
|
|
||||||
});
|
});
|
||||||
if (adminUser.role !== "admin") {
|
if (adminUser.role !== "admin") {
|
||||||
toast.error("حالت توسعه فقط برای ادمین (سوپر یوزر) در دسترس است");
|
toast.error("حالت توسعه فقط برای ادمین (سوپر یوزر) در دسترس است");
|
||||||
@@ -298,30 +296,33 @@ export function LoginPage() {
|
|||||||
<label className="mb-1 block text-[12px] font-semibold text-muted" htmlFor="login-password">
|
<label className="mb-1 block text-[12px] font-semibold text-muted" htmlFor="login-password">
|
||||||
رمز عبور
|
رمز عبور
|
||||||
</label>
|
</label>
|
||||||
<input
|
<div className="relative">
|
||||||
id="login-password"
|
<input
|
||||||
type="password"
|
id="login-password"
|
||||||
autoComplete="current-password"
|
type={showPassword ? "text" : "password"}
|
||||||
dir="ltr"
|
autoComplete="current-password"
|
||||||
className="h-10 w-full rounded-lg border border-line bg-panel px-3 text-left text-sm shadow-sm placeholder:text-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
dir="ltr"
|
||||||
placeholder="••••••"
|
className="h-10 w-full rounded-lg border border-line bg-panel px-3 pr-10 text-left text-sm shadow-sm placeholder:text-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||||
value={password}
|
placeholder="••••••"
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
value={password}
|
||||||
/>
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
</div>
|
/>
|
||||||
<div className="field">
|
<button
|
||||||
<label className="mb-1 block text-[12px] font-semibold text-muted" htmlFor="login-org">
|
type="button"
|
||||||
سازمان (اختیاری)
|
onClick={() => setShowPassword((v) => !v)}
|
||||||
</label>
|
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||||
<input
|
aria-pressed={showPassword}
|
||||||
id="login-org"
|
aria-controls="login-password"
|
||||||
type="text"
|
title={showPassword ? "پنهان کردن رمز" : "نمایش رمز"}
|
||||||
dir="ltr"
|
className="absolute inset-y-0 right-0 flex cursor-pointer items-center rounded-lg px-3 text-muted transition-colors hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||||
className="h-10 w-full rounded-lg border border-line bg-panel px-3 text-left text-sm shadow-sm placeholder:text-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
>
|
||||||
placeholder="inventory"
|
{showPassword ? (
|
||||||
value={organization}
|
<EyeOff className="h-4 w-4" aria-hidden="true" />
|
||||||
onChange={(e) => setOrganization(e.target.value)}
|
) : (
|
||||||
/>
|
<Eye className="h-4 w-4" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
Reference in New Issue
Block a user