Connect backend
This commit is contained in:
@@ -13,13 +13,27 @@ const DEMO_OTP = "123456";
|
|||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { completeRegistration } = useAuth();
|
const { completeRegistration, lastError } = useAuth();
|
||||||
|
|
||||||
const [step, setStep] = useState<"phone" | "otp">("phone");
|
const [step, setStep] = useState<"phone" | "otp">("phone");
|
||||||
const [phone, setPhone] = useState("");
|
const [phone, setPhone] = useState("");
|
||||||
const [otp, setOtp] = useState("");
|
const [otp, setOtp] = useState("");
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [errors, setErrors] = useState({ phone: "", otp: "" });
|
const [errors, setErrors] = useState({ phone: "", otp: "" });
|
||||||
|
|
||||||
|
const mapApiError = (
|
||||||
|
err: { status: number; body: string } | null,
|
||||||
|
): string => {
|
||||||
|
if (!err) return "Đăng ký thất bại. Vui lòng thử lại.";
|
||||||
|
if (err.status === 0)
|
||||||
|
return "Không kết nối được đến máy chủ. Kiểm tra backend có đang chạy không.";
|
||||||
|
if (err.status >= 500)
|
||||||
|
return `Máy chủ lỗi (${err.status}). Backend có thể chưa sẵn sàng.`;
|
||||||
|
if (err.body === "ExistedUser") return "Số điện thoại đã được đăng ký";
|
||||||
|
if (err.body === "InvalidPhoneNumber") return "Số điện thoại không hợp lệ";
|
||||||
|
return err.body || "Đăng ký thất bại. Vui lòng thử lại.";
|
||||||
|
};
|
||||||
|
|
||||||
// Validate Vietnamese phone number
|
// Validate Vietnamese phone number
|
||||||
const validatePhone = (phoneNumber: string): boolean => {
|
const validatePhone = (phoneNumber: string): boolean => {
|
||||||
// Vietnamese phone format: 10 digits starting with 0
|
// Vietnamese phone format: 10 digits starting with 0
|
||||||
@@ -49,7 +63,7 @@ export default function RegisterPage() {
|
|||||||
setErrors({ phone: "", otp: "" });
|
setErrors({ phone: "", otp: "" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOtpSubmit = (e: FormEvent) => {
|
const handleOtpSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!otp.trim()) {
|
if (!otp.trim()) {
|
||||||
@@ -62,9 +76,17 @@ export default function RegisterPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete registration
|
setSubmitting(true);
|
||||||
completeRegistration(phone);
|
try {
|
||||||
router.push("/");
|
const success = await completeRegistration(phone);
|
||||||
|
if (success) {
|
||||||
|
router.push("/");
|
||||||
|
} else {
|
||||||
|
setErrors({ ...errors, otp: mapApiError(lastError) });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackToPhone = () => {
|
const handleBackToPhone = () => {
|
||||||
@@ -237,8 +259,9 @@ export default function RegisterPage() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
style="login"
|
style="login"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
Hoàn tất đăng ký
|
{submitting ? "Đang đăng ký..." : "Hoàn tất đăng ký"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
@@ -247,6 +270,7 @@ export default function RegisterPage() {
|
|||||||
onClick={handleBackToPhone}
|
onClick={handleBackToPhone}
|
||||||
size="lg"
|
size="lg"
|
||||||
style="login"
|
style="login"
|
||||||
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
Thay đổi số điện thoại
|
Thay đổi số điện thoại
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import { FormEvent, useState } from "react";
|
|||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { login } = useAuth();
|
const { login, lastError } = useAuth();
|
||||||
|
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [errors, setErrors] = useState({
|
const [errors, setErrors] = useState({
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -39,21 +40,37 @@ export default function LoginForm() {
|
|||||||
return isValid;
|
return isValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: FormEvent) => {
|
const mapApiError = (
|
||||||
|
err: { status: number; body: string } | null,
|
||||||
|
): string => {
|
||||||
|
if (!err) return "Đăng nhập thất bại. Vui lòng thử lại.";
|
||||||
|
if (err.status === 0)
|
||||||
|
return "Không kết nối được đến máy chủ. Kiểm tra backend có đang chạy không.";
|
||||||
|
if (err.status >= 500)
|
||||||
|
return `Máy chủ lỗi (${err.status}). Backend có thể chưa sẵn sàng hoặc account-service chưa chạy.`;
|
||||||
|
if (err.body === "InvalidPhoneNumber") return "Số điện thoại không tồn tại";
|
||||||
|
if (err.body === "InvalidPassword") return "Mật khẩu không đúng";
|
||||||
|
return err.body || "Tên đăng nhập hoặc mật khẩu không đúng";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!validate()) return;
|
if (!validate()) return;
|
||||||
|
|
||||||
const success = login(username, password);
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
if (success) {
|
const success = await login(username, password);
|
||||||
router.push("/");
|
if (success) {
|
||||||
} else {
|
router.push("/");
|
||||||
setErrors({
|
} else {
|
||||||
username: "",
|
setErrors((prev) => ({
|
||||||
password: "",
|
...prev,
|
||||||
general: "Tên đăng nhập hoặc mật khẩu không đúng",
|
general: mapApiError(lastError),
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,8 +124,9 @@ export default function LoginForm() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
style="login"
|
style="login"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
Đăng nhập
|
{submitting ? "Đang đăng nhập..." : "Đăng nhập"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Register Button */}
|
{/* Register Button */}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# Điều hướng API tới Backend
|
||||||
|
location /api {
|
||||||
|
# 'backend' là tên service được định nghĩa trong docker-compose.yaml
|
||||||
|
proxy_pass http://host.docker.internal:32080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Điều hướng phần còn lại tới Frontend
|
||||||
|
location / {
|
||||||
|
# 'frontend' là tên service được định nghĩa trong docker-compose.yaml
|
||||||
|
proxy_pass http://host.docker.internal:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:stable-alpine
|
||||||
|
container_name: nginx-gateway
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { apiRequest } from "./client";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiCustomer,
|
||||||
|
LoginPayload,
|
||||||
|
QuickLoginPayload,
|
||||||
|
QuickSignupPayload,
|
||||||
|
SignupPayload,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
const CUSTOMER_BASE = "/api/customer";
|
||||||
|
|
||||||
|
export function signup(payload: SignupPayload) {
|
||||||
|
return apiRequest<ApiCustomer>(`${CUSTOMER_BASE}/signup`, {
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function quickSignup(payload: QuickSignupPayload) {
|
||||||
|
return apiRequest<ApiCustomer>(`${CUSTOMER_BASE}/quick_signup`, {
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login(payload: LoginPayload) {
|
||||||
|
return apiRequest<ApiCustomer>(`${CUSTOMER_BASE}/login`, {
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function quickLogin(payload: QuickLoginPayload) {
|
||||||
|
return apiRequest<ApiCustomer>(`${CUSTOMER_BASE}/quick_login`, {
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function me() {
|
||||||
|
return apiRequest<ApiCustomer>(CUSTOMER_BASE, { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
// Backend has no logout endpoint; clearing the httpOnly `auth` cookie
|
||||||
|
// requires a server-side Set-Cookie with Max-Age=0. Until the gateway
|
||||||
|
// exposes /logout, we trigger an invalid-auth response by calling /me
|
||||||
|
// with a fresh client after clearing browser-accessible state.
|
||||||
|
// The auth cookie is httpOnly so we cannot remove it from JS; the
|
||||||
|
// server must do it. For now this is a no-op network call.
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
status: number;
|
||||||
|
body: string;
|
||||||
|
|
||||||
|
constructor(status: number, body: string, message?: string) {
|
||||||
|
super(message ?? body ?? `Request failed with status ${status}`);
|
||||||
|
this.name = "ApiError";
|
||||||
|
this.status = status;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
|
|
||||||
|
interface RequestOptions {
|
||||||
|
method?: HttpMethod;
|
||||||
|
body?: unknown;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiRequest<T = unknown>(
|
||||||
|
path: string,
|
||||||
|
options: RequestOptions = {},
|
||||||
|
): Promise<T> {
|
||||||
|
const { method = "GET", body, headers = {}, signal } = options;
|
||||||
|
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rawText = await res.text();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new ApiError(res.status, rawText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawText) return undefined as T;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(rawText) as T;
|
||||||
|
} catch {
|
||||||
|
return rawText as unknown as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
+123
-118
@@ -3,163 +3,168 @@
|
|||||||
import {
|
import {
|
||||||
ReactNode,
|
ReactNode,
|
||||||
createContext,
|
createContext,
|
||||||
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import type { User } from "./types";
|
import * as authApi from "./api/auth";
|
||||||
|
import { ApiError } from "./api/client";
|
||||||
|
|
||||||
|
import type { ApiCustomer, User } from "./types";
|
||||||
|
|
||||||
|
export interface AuthErrorInfo {
|
||||||
|
// HTTP status (0 = network / fetch failure)
|
||||||
|
status: number;
|
||||||
|
// Raw body or error message from the backend (e.g. "InvalidPassword")
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
login: (username: string, password: string) => boolean;
|
loading: boolean;
|
||||||
logout: () => void;
|
login: (phone: string, password: string) => Promise<boolean>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
registerPhone: string | null;
|
registerPhone: string | null;
|
||||||
setRegisterPhone: (phone: string | null) => void;
|
setRegisterPhone: (phone: string | null) => void;
|
||||||
completeRegistration: (phone: string) => void;
|
completeRegistration: (phone: string) => Promise<boolean>;
|
||||||
|
lastError: AuthErrorInfo | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
// Mock user database
|
const STORAGE_KEY = "coffee-shop-user";
|
||||||
const MOCK_AUTH_DB = {
|
|
||||||
// Admin
|
|
||||||
admin: {
|
|
||||||
username: "admin",
|
|
||||||
password: "admin",
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
name: "Quản lý",
|
|
||||||
role: "manager" as const,
|
|
||||||
avatar: null,
|
|
||||||
phone: "0912345678",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Staff (username and password are their names)
|
// Stable numeric id derived from UUID string — used because local
|
||||||
"Nguyễn Văn An": {
|
// User.id is typed as number (staff module uses numeric ids).
|
||||||
username: "Nguyễn Văn An",
|
// Backend never reads this back; identity on the server side is cookie-based.
|
||||||
password: "Nguyễn Văn An",
|
function hashUuidToNumber(uuid: string): number {
|
||||||
user: {
|
let h = 0;
|
||||||
id: 2,
|
for (let i = 0; i < uuid.length; i++) {
|
||||||
name: "Nguyễn Văn An",
|
h = (h * 31 + uuid.charCodeAt(i)) | 0;
|
||||||
role: "staff" as const,
|
}
|
||||||
avatar: null,
|
return Math.abs(h);
|
||||||
phone: "0901234567",
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
"Trần Thị Bình": {
|
|
||||||
username: "Trần Thị Bình",
|
|
||||||
password: "Trần Thị Bình",
|
|
||||||
user: {
|
|
||||||
id: 3,
|
|
||||||
name: "Trần Thị Bình",
|
|
||||||
role: "staff" as const,
|
|
||||||
avatar: null,
|
|
||||||
phone: "0902345678",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"Lê Văn Cường": {
|
|
||||||
username: "Lê Văn Cường",
|
|
||||||
password: "Lê Văn Cường",
|
|
||||||
user: {
|
|
||||||
id: 4,
|
|
||||||
name: "Lê Văn Cường",
|
|
||||||
role: "staff" as const,
|
|
||||||
avatar: null,
|
|
||||||
phone: "0903456789",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Customers (username is phone number, password is custom)
|
function toUser(c: ApiCustomer): User {
|
||||||
"0987654321": {
|
return {
|
||||||
username: "0987654321",
|
id: hashUuidToNumber(c.id),
|
||||||
password: "user1",
|
apiId: c.id,
|
||||||
user: {
|
name: c.name,
|
||||||
id: 5,
|
role: "customer",
|
||||||
name: "Khách hàng",
|
avatar: null,
|
||||||
role: "customer" as const,
|
phone: c.phone,
|
||||||
avatar: null,
|
};
|
||||||
phone: "0987654321",
|
}
|
||||||
},
|
|
||||||
},
|
function toErrorInfo(e: unknown): AuthErrorInfo {
|
||||||
"0976543210": {
|
if (e instanceof ApiError) return { status: e.status, body: e.body ?? "" };
|
||||||
username: "0976543210",
|
if (e instanceof Error) return { status: 0, body: e.message };
|
||||||
password: "user1",
|
return { status: 0, body: String(e) };
|
||||||
user: {
|
}
|
||||||
id: 6,
|
|
||||||
name: "Khách hàng",
|
|
||||||
role: "customer" as const,
|
|
||||||
avatar: null,
|
|
||||||
phone: "0976543210",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const [registerPhone, setRegisterPhone] = useState<string | null>(null);
|
const [registerPhone, setRegisterPhone] = useState<string | null>(null);
|
||||||
|
const [lastError, setLastError] = useState<AuthErrorInfo | null>(null);
|
||||||
|
|
||||||
// Load user from localStorage on mount
|
const persist = useCallback((u: User | null) => {
|
||||||
useEffect(() => {
|
if (u) localStorage.setItem(STORAGE_KEY, JSON.stringify(u));
|
||||||
const savedUser = localStorage.getItem("coffee-shop-user");
|
else localStorage.removeItem(STORAGE_KEY);
|
||||||
if (savedUser) {
|
|
||||||
try {
|
|
||||||
setUser(JSON.parse(savedUser));
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to parse saved user", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = (username: string, password: string): boolean => {
|
// Hydrate from cached user immediately, then verify against server.
|
||||||
const authEntry = MOCK_AUTH_DB[username as keyof typeof MOCK_AUTH_DB];
|
useEffect(() => {
|
||||||
|
const cached = localStorage.getItem(STORAGE_KEY);
|
||||||
if (authEntry && authEntry.password === password) {
|
if (cached) {
|
||||||
setUser(authEntry.user);
|
try {
|
||||||
localStorage.setItem("coffee-shop-user", JSON.stringify(authEntry.user));
|
setUser(JSON.parse(cached));
|
||||||
return true;
|
} catch {
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
let cancelled = false;
|
||||||
};
|
(async () => {
|
||||||
|
try {
|
||||||
|
const current = await authApi.me();
|
||||||
|
if (!cancelled) {
|
||||||
|
const u = toUser(current);
|
||||||
|
setUser(u);
|
||||||
|
persist(u);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) {
|
||||||
|
setUser(null);
|
||||||
|
persist(null);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const logout = () => {
|
return () => {
|
||||||
setUser(null);
|
cancelled = true;
|
||||||
localStorage.removeItem("coffee-shop-user");
|
|
||||||
};
|
|
||||||
|
|
||||||
const completeRegistration = (phone: string) => {
|
|
||||||
// Create new customer account
|
|
||||||
const newUser: User = {
|
|
||||||
id: Date.now(),
|
|
||||||
name: "Khách hàng",
|
|
||||||
role: "customer",
|
|
||||||
avatar: null,
|
|
||||||
phone,
|
|
||||||
};
|
};
|
||||||
|
}, [persist]);
|
||||||
|
|
||||||
// Add to mock database (in real app, this would be API call)
|
const login = useCallback(
|
||||||
(MOCK_AUTH_DB as any)[phone] = {
|
async (phone: string, password: string): Promise<boolean> => {
|
||||||
username: phone,
|
setLastError(null);
|
||||||
password: "user1", // Default password for new customers
|
try {
|
||||||
user: newUser,
|
const customer = await authApi.login({ phone, password });
|
||||||
};
|
const u = toUser(customer);
|
||||||
|
setUser(u);
|
||||||
|
persist(u);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setLastError(toErrorInfo(e));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[persist],
|
||||||
|
);
|
||||||
|
|
||||||
setUser(newUser);
|
const logout = useCallback(async () => {
|
||||||
localStorage.setItem("coffee-shop-user", JSON.stringify(newUser));
|
try {
|
||||||
setRegisterPhone(null);
|
await authApi.logout();
|
||||||
};
|
} finally {
|
||||||
|
setUser(null);
|
||||||
|
persist(null);
|
||||||
|
}
|
||||||
|
}, [persist]);
|
||||||
|
|
||||||
|
const completeRegistration = useCallback(
|
||||||
|
async (phone: string): Promise<boolean> => {
|
||||||
|
setLastError(null);
|
||||||
|
try {
|
||||||
|
const customer = await authApi.quickSignup({ phone });
|
||||||
|
const u = toUser(customer);
|
||||||
|
setUser(u);
|
||||||
|
persist(u);
|
||||||
|
setRegisterPhone(null);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
setLastError(toErrorInfo(e));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[persist],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
|
loading,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
registerPhone,
|
registerPhone,
|
||||||
setRegisterPhone,
|
setRegisterPhone,
|
||||||
completeRegistration,
|
completeRegistration,
|
||||||
|
lastError,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -7,6 +7,34 @@ export interface User {
|
|||||||
role: UserRole;
|
role: UserRole;
|
||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
apiId?: string; // backend UUID — set when user was authenticated via API
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== AUTH API PAYLOADS (aligned with backend DTOs) =====
|
||||||
|
export interface SignupPayload {
|
||||||
|
name: string;
|
||||||
|
phone: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuickSignupPayload {
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginPayload {
|
||||||
|
phone: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuickLoginPayload {
|
||||||
|
phone: string;
|
||||||
|
otp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiCustomer {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
phone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== MENU TYPES =====
|
// ===== MENU TYPES =====
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
async rewrites() {
|
||||||
|
const gateway =
|
||||||
|
process.env.GATEWAY_INTERNAL_URL ?? "http://localhost:3000";
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/api/:path*",
|
||||||
|
destination: `${gateway}/api/:path*`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user