feat: login with username or email + change password
Backend: - Login handler accepts username OR email via the same field - New PUT /auth/me/password endpoint (verifies current password, requires min 8 chars for new password) Frontend: - Login form shows EMAIL OR USERNAME field (single field for both) - Auth store updated with changePassword function - UserSettings has new CHANGE PASSWORD section
This commit is contained in:
@@ -38,6 +38,7 @@ func (h *Handler) RegisterPublicRoutes(r chi.Router) {
|
|||||||
func (h *Handler) RegisterProtectedRoutes(r chi.Router) {
|
func (h *Handler) RegisterProtectedRoutes(r chi.Router) {
|
||||||
r.Get("/auth/me", h.Me)
|
r.Get("/auth/me", h.Me)
|
||||||
r.Patch("/auth/me", h.UpdateProfile)
|
r.Patch("/auth/me", h.UpdateProfile)
|
||||||
|
r.Put("/auth/me/password", h.ChangePassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
type registerRequest struct {
|
type registerRequest struct {
|
||||||
@@ -49,6 +50,7 @@ type registerRequest struct {
|
|||||||
|
|
||||||
type loginRequest struct {
|
type loginRequest struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +61,11 @@ type updateProfileRequest struct {
|
|||||||
StatusText string `json:"status_text"`
|
StatusText string `json:"status_text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type changePasswordRequest struct {
|
||||||
|
CurrentPassword string `json:"current_password"`
|
||||||
|
NewPassword string `json:"new_password"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserProfile struct {
|
type UserProfile struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
@@ -150,10 +157,19 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
login := strings.TrimSpace(req.Email)
|
||||||
|
if login == "" {
|
||||||
|
login = strings.TrimSpace(req.Username)
|
||||||
|
}
|
||||||
|
if login == "" || req.Password == "" {
|
||||||
|
http.Error(w, `{"error":"email/username and password required"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var userID, hash string
|
var userID, hash string
|
||||||
err := h.db.QueryRowContext(r.Context(), `
|
err := h.db.QueryRowContext(r.Context(), `
|
||||||
SELECT id, password_hash FROM users WHERE email = $1
|
SELECT id, password_hash FROM users WHERE email = $1 OR username = $1
|
||||||
`, req.Email).Scan(&userID, &hash)
|
`, login).Scan(&userID, &hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
|
http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
@@ -295,3 +311,58 @@ func (h *Handler) getProfile(ctx context.Context, userID string) (UserProfile, e
|
|||||||
func (h *Handler) setSessionCookie(w http.ResponseWriter, token string) {
|
func (h *Handler) setSessionCookie(w http.ResponseWriter, token string) {
|
||||||
SetSessionCookie(w, h.cfg.Session.CookieName, token, h.cfg.Session.Duration)
|
SetSessionCookie(w, h.cfg.Session.CookieName, token, h.cfg.Session.Duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChangePassword handles PUT /auth/me/password.
|
||||||
|
func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, ok := middleware.UserIDFromContext(r.Context())
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req changePasswordRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.CurrentPassword == "" || req.NewPassword == "" {
|
||||||
|
http.Error(w, `{"error":"current and new password required"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.NewPassword) < 8 {
|
||||||
|
http.Error(w, `{"error":"password must be at least 8 characters"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify current password
|
||||||
|
var currentHash string
|
||||||
|
err := h.db.QueryRowContext(r.Context(), "SELECT password_hash FROM users WHERE id = $1", userID).Scan(¤tHash)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, `{"error":"user not found"}`, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = VerifyPassword(req.CurrentPassword, currentHash)
|
||||||
|
if err != nil || !ok {
|
||||||
|
http.Error(w, `{"error":"current password is incorrect"}`, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash new password
|
||||||
|
newHash, err := HashPassword(req.NewPassword)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = h.db.ExecContext(r.Context(), "UPDATE users SET password_hash = $1 WHERE id = $2", newHash, userID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, `{"error":"failed to update password"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"message": "password updated"})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useAuthStore } from '../stores/auth.ts';
|
import { useAuthStore } from "../stores/auth.ts";
|
||||||
|
|
||||||
export function LoginForm() {
|
export function LoginForm() {
|
||||||
const [isRegister, setIsRegister] = useState(false);
|
const [isRegister, setIsRegister] = useState(false);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState("");
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState("");
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
const { login, register, isLoading, error, clearError } = useAuthStore();
|
const { login, register, isLoading, error, clearError } = useAuthStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -16,44 +16,63 @@ export function LoginForm() {
|
|||||||
clearError();
|
clearError();
|
||||||
|
|
||||||
if (isRegister) {
|
if (isRegister) {
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
await register(email, username, password);
|
await register(email, username, password);
|
||||||
} else {
|
} else {
|
||||||
await login(email, password);
|
await login(email, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate('/');
|
navigate("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex items-center justify-center bg-gb-bg p-4">
|
<div className="h-full w-full flex items-center justify-center bg-gb-bg p-4">
|
||||||
<div className="terminal-border bg-gb-bg-h p-6 w-full max-w-md">
|
<div className="terminal-border bg-gb-bg-h p-6 w-full max-w-md">
|
||||||
<pre className="text-gb-orange font-mono text-lg mb-6">{'┌─ DUMPSTER ─┐'}</pre>
|
<pre className="text-gb-orange font-mono text-lg mb-6">
|
||||||
<h2 className="text-gb-fg-s mb-4">{isRegister ? '[REGISTER]' : '[LOGIN]'}</h2>
|
{"┌─ DUMPSTER ─┐"}
|
||||||
|
</pre>
|
||||||
|
<h2 className="text-gb-fg-s mb-4">
|
||||||
|
{isRegister ? "[REGISTER]" : "[LOGIN]"}
|
||||||
|
</h2>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
{isRegister ? (
|
||||||
<label className="block text-gb-fg-t mb-1">EMAIL:</label>
|
<>
|
||||||
<input
|
<div>
|
||||||
type="email"
|
<label className="block text-gb-fg-t mb-1">EMAIL:</label>
|
||||||
value={email}
|
<input
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
type="email"
|
||||||
className="terminal-input"
|
value={email}
|
||||||
required
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
autoComplete="email"
|
className="terminal-input"
|
||||||
/>
|
required
|
||||||
</div>
|
autoComplete="email"
|
||||||
{isRegister && (
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gb-fg-t mb-1">USERNAME:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="terminal-input"
|
||||||
|
required
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gb-fg-t mb-1">USERNAME:</label>
|
<label className="block text-gb-fg-t mb-1">
|
||||||
|
EMAIL OR USERNAME:
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={email}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="terminal-input"
|
className="terminal-input"
|
||||||
required
|
required
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
|
placeholder="email or username"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -65,12 +84,16 @@ export function LoginForm() {
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="terminal-input"
|
className="terminal-input"
|
||||||
required
|
required
|
||||||
autoComplete={isRegister ? 'new-password' : 'current-password'}
|
autoComplete={
|
||||||
|
isRegister ? "new-password" : "current-password"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isRegister && (
|
{isRegister && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gb-fg-t mb-1">CONFIRM PASSWORD:</label>
|
<label className="block text-gb-fg-t mb-1">
|
||||||
|
CONFIRM PASSWORD:
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
@@ -81,8 +104,16 @@ export function LoginForm() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && <p className="text-gb-red text-sm">ERR: {error}</p>}
|
{error && <p className="text-gb-red text-sm">ERR: {error}</p>}
|
||||||
<button type="submit" className="terminal-button w-full" disabled={isLoading}>
|
<button
|
||||||
{isLoading ? '[PROCESSING...]' : isRegister ? '[REGISTER]' : '[LOGIN]'}
|
type="submit"
|
||||||
|
className="terminal-button w-full"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading
|
||||||
|
? "[PROCESSING...]"
|
||||||
|
: isRegister
|
||||||
|
? "[REGISTER]"
|
||||||
|
: "[LOGIN]"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{!isRegister && (
|
{!isRegister && (
|
||||||
@@ -91,28 +122,29 @@ export function LoginForm() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
// Get challenge from server
|
const resp = await fetch(
|
||||||
const resp = await fetch('/api/v1/auth/webauthn/login/begin', { method: 'POST', credentials: 'include' });
|
"/api/v1/auth/webauthn/login/begin",
|
||||||
if (!resp.ok) throw new Error('Passkey login not available');
|
{ method: "POST", credentials: "include" },
|
||||||
|
);
|
||||||
|
if (!resp.ok)
|
||||||
|
throw new Error("Passkey login not available");
|
||||||
const options = await resp.json();
|
const options = await resp.json();
|
||||||
|
const credential = await navigator.credentials.get({
|
||||||
// Use WebAuthn API
|
publicKey: options,
|
||||||
const credential = await navigator.credentials.get({ publicKey: options });
|
|
||||||
if (!credential) return;
|
|
||||||
|
|
||||||
// Send to server
|
|
||||||
const finishResp = await fetch('/api/v1/auth/webauthn/login/finish', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(credential),
|
|
||||||
});
|
});
|
||||||
|
if (!credential) return;
|
||||||
if (finishResp.ok) {
|
const finishResp = await fetch(
|
||||||
navigate('/');
|
"/api/v1/auth/webauthn/login/finish",
|
||||||
}
|
{
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(credential),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (finishResp.ok) navigate("/");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Passkey login failed:', err);
|
console.error("Passkey login failed:", err);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="terminal-button w-full border-gb-aqua text-gb-aqua"
|
className="terminal-button w-full border-gb-aqua text-gb-aqua"
|
||||||
@@ -130,12 +162,15 @@ export function LoginForm() {
|
|||||||
}}
|
}}
|
||||||
className="text-gb-blue hover:text-gb-aqua text-sm"
|
className="text-gb-blue hover:text-gb-aqua text-sm"
|
||||||
>
|
>
|
||||||
{isRegister ? '[BACK TO LOGIN]' : '[CREATE ACCOUNT]'}
|
{isRegister ? "[BACK TO LOGIN]" : "[CREATE ACCOUNT]"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{!isRegister && (
|
{!isRegister && (
|
||||||
<p className="text-gb-fg-f text-xs mt-4 text-center">
|
<p className="text-gb-fg-f text-xs mt-4 text-center">
|
||||||
Use Link component: <Link to="/" className="text-gb-blue hover:text-gb-aqua">[HOME]</Link>
|
Use Link component:{" "}
|
||||||
|
<Link to="/" className="text-gb-blue hover:text-gb-aqua">
|
||||||
|
[HOME]
|
||||||
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useAuthStore } from '../stores/auth.ts';
|
|||||||
import { usePushStore } from '../stores/push.ts';
|
import { usePushStore } from '../stores/push.ts';
|
||||||
|
|
||||||
export function UserSettings() {
|
export function UserSettings() {
|
||||||
const { user, updateProfile, isLoading, error, clearError, fetchMe } = useAuthStore();
|
const { user, updateProfile, changePassword, isLoading, error, clearError, fetchMe } = useAuthStore();
|
||||||
const { isSupported, isSubscribed, subscribe, unsubscribe, checkSubscription } = usePushStore();
|
const { isSupported, isSubscribed, subscribe, unsubscribe, checkSubscription } = usePushStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -17,6 +17,14 @@ export function UserSettings() {
|
|||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Password change state
|
||||||
|
const [currentPassword, setCurrentPassword] = useState('');
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
||||||
|
const [passwordLoading, setPasswordLoading] = useState(false);
|
||||||
|
const [passwordError, setPasswordError] = useState<string | null>(null);
|
||||||
|
const [passwordSuccess, setPasswordSuccess] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
setDisplayName(user.display_name || '');
|
setDisplayName(user.display_name || '');
|
||||||
@@ -86,6 +94,32 @@ export function UserSettings() {
|
|||||||
|
|
||||||
const hexValid = /^#[0-9a-fA-F]{6}$/.test(accentColor);
|
const hexValid = /^#[0-9a-fA-F]{6}$/.test(accentColor);
|
||||||
|
|
||||||
|
const handleChangePassword = async () => {
|
||||||
|
setPasswordError(null);
|
||||||
|
setPasswordSuccess(false);
|
||||||
|
if (newPassword !== confirmNewPassword) {
|
||||||
|
setPasswordError("new passwords do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newPassword.length < 8) {
|
||||||
|
setPasswordError("password must be at least 8 characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPasswordLoading(true);
|
||||||
|
try {
|
||||||
|
await changePassword(currentPassword, newPassword);
|
||||||
|
setPasswordSuccess(true);
|
||||||
|
setCurrentPassword('');
|
||||||
|
setNewPassword('');
|
||||||
|
setConfirmNewPassword('');
|
||||||
|
setTimeout(() => setPasswordSuccess(false), 3000);
|
||||||
|
} catch {
|
||||||
|
// error is set in store
|
||||||
|
} finally {
|
||||||
|
setPasswordLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full bg-gb-bg p-4 md:p-8 overflow-y-auto">
|
<div className="h-full w-full bg-gb-bg p-4 md:p-8 overflow-y-auto">
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-2xl mx-auto">
|
||||||
@@ -234,6 +268,53 @@ export function UserSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Change Password */}
|
||||||
|
<div className="border border-gb-bg-t p-4">
|
||||||
|
<label className="block text-gb-fg-f mb-2 font-mono text-sm">
|
||||||
|
CHANGE PASSWORD:
|
||||||
|
</label>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
className="terminal-input w-full"
|
||||||
|
placeholder="current password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
className="terminal-input w-full"
|
||||||
|
placeholder="new password (min 8 chars)"
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmNewPassword}
|
||||||
|
onChange={(e) => setConfirmNewPassword(e.target.value)}
|
||||||
|
className="terminal-input w-full"
|
||||||
|
placeholder="confirm new password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
{passwordError && (
|
||||||
|
<p className="text-gb-red text-xs font-mono">ERR: {passwordError}</p>
|
||||||
|
)}
|
||||||
|
{passwordSuccess && (
|
||||||
|
<p className="text-gb-green text-xs font-mono">[password changed!]</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleChangePassword}
|
||||||
|
disabled={passwordLoading || !currentPassword || newPassword.length < 8 || newPassword !== confirmNewPassword}
|
||||||
|
className="terminal-button text-xs"
|
||||||
|
>
|
||||||
|
{passwordLoading ? '[CHANGING...]' : '[CHANGE PASSWORD]'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Push Notifications */}
|
{/* Push Notifications */}
|
||||||
{isSupported && (
|
{isSupported && (
|
||||||
<div className="border border-gb-bg-t p-4">
|
<div className="border border-gb-bg-t p-4">
|
||||||
|
|||||||
+57
-18
@@ -1,7 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from "zustand";
|
||||||
import { api } from '../lib/api.ts';
|
import { api } from "../lib/api.ts";
|
||||||
|
|
||||||
export type UserStatus = 'online' | 'idle' | 'dnd' | 'offline';
|
export type UserStatus = "online" | "idle" | "dnd" | "offline";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -28,11 +28,20 @@ interface AuthState {
|
|||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
login: (email: string, password: string) => Promise<void>;
|
login: (identifier: string, password: string) => Promise<void>;
|
||||||
register: (email: string, username: string, password: string, displayName?: string) => Promise<void>;
|
register: (
|
||||||
|
email: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName?: string,
|
||||||
|
) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
fetchMe: () => Promise<void>;
|
fetchMe: () => Promise<void>;
|
||||||
updateProfile: (data: UpdateProfilePayload) => Promise<void>;
|
updateProfile: (data: UpdateProfilePayload) => Promise<void>;
|
||||||
|
changePassword: (
|
||||||
|
currentPassword: string,
|
||||||
|
newPassword: string,
|
||||||
|
) => Promise<void>;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,16 +51,19 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
||||||
login: async (email, password) => {
|
login: async (identifier, password) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
try {
|
try {
|
||||||
await api.post('/auth/login/password', { email, password });
|
await api.post("/auth/login/password", {
|
||||||
const user = await api.get<User>('/auth/me');
|
email: identifier,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
const user = await api.get<User>("/auth/me");
|
||||||
set({ user, isAuthenticated: true, isLoading: false });
|
set({ user, isAuthenticated: true, isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: error instanceof Error ? error.message : 'Login failed',
|
error: error instanceof Error ? error.message : "Login failed",
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -60,13 +72,19 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
register: async (email, username, password, displayName) => {
|
register: async (email, username, password, displayName) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
try {
|
try {
|
||||||
await api.post('/auth/register', { email, username, password, display_name: displayName || username });
|
await api.post("/auth/register", {
|
||||||
const user = await api.get<User>('/auth/me');
|
email,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
display_name: displayName || username,
|
||||||
|
});
|
||||||
|
const user = await api.get<User>("/auth/me");
|
||||||
set({ user, isAuthenticated: true, isLoading: false });
|
set({ user, isAuthenticated: true, isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: error instanceof Error ? error.message : 'Registration failed',
|
error:
|
||||||
|
error instanceof Error ? error.message : "Registration failed",
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -75,12 +93,12 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
logout: async () => {
|
logout: async () => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
try {
|
try {
|
||||||
await api.post('/auth/logout');
|
await api.post("/auth/logout");
|
||||||
set({ user: null, isAuthenticated: false, isLoading: false });
|
set({ user: null, isAuthenticated: false, isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: error instanceof Error ? error.message : 'Logout failed',
|
error: error instanceof Error ? error.message : "Logout failed",
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -89,14 +107,15 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
fetchMe: async () => {
|
fetchMe: async () => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const user = await api.get<User>('/auth/me');
|
const user = await api.get<User>("/auth/me");
|
||||||
set({ user, isAuthenticated: true, isLoading: false });
|
set({ user, isAuthenticated: true, isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
user: null,
|
user: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: error instanceof Error ? error.message : 'Failed to fetch user',
|
error:
|
||||||
|
error instanceof Error ? error.message : "Failed to fetch user",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -104,12 +123,32 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
updateProfile: async (data) => {
|
updateProfile: async (data) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const user = await api.patch<User>('/auth/me', data);
|
const user = await api.patch<User>("/auth/me", data);
|
||||||
set({ user, isLoading: false });
|
set({ user, isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: error instanceof Error ? error.message : 'Update failed',
|
error: error instanceof Error ? error.message : "Update failed",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
changePassword: async (currentPassword, newPassword) => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
await api.put("/auth/me/password", {
|
||||||
|
current_password: currentPassword,
|
||||||
|
new_password: newPassword,
|
||||||
|
});
|
||||||
|
set({ isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Password change failed",
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user