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:
2026-06-29 14:00:19 -04:00
parent fec0eb1917
commit 9f138deb9d
4 changed files with 300 additions and 74 deletions
+57 -18
View File
@@ -1,7 +1,7 @@
import { create } from 'zustand';
import { api } from '../lib/api.ts';
import { create } from "zustand";
import { api } from "../lib/api.ts";
export type UserStatus = 'online' | 'idle' | 'dnd' | 'offline';
export type UserStatus = "online" | "idle" | "dnd" | "offline";
export interface User {
id: string;
@@ -28,11 +28,20 @@ interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
login: (email: string, password: string) => Promise<void>;
register: (email: string, username: string, password: string, displayName?: string) => Promise<void>;
login: (identifier: string, password: string) => Promise<void>;
register: (
email: string,
username: string,
password: string,
displayName?: string,
) => Promise<void>;
logout: () => Promise<void>;
fetchMe: () => Promise<void>;
updateProfile: (data: UpdateProfilePayload) => Promise<void>;
changePassword: (
currentPassword: string,
newPassword: string,
) => Promise<void>;
clearError: () => void;
}
@@ -42,16 +51,19 @@ export const useAuthStore = create<AuthState>((set) => ({
isLoading: false,
error: null,
login: async (email, password) => {
login: async (identifier, password) => {
set({ isLoading: true, error: null });
try {
await api.post('/auth/login/password', { email, password });
const user = await api.get<User>('/auth/me');
await api.post("/auth/login/password", {
email: identifier,
password,
});
const user = await api.get<User>("/auth/me");
set({ user, isAuthenticated: true, isLoading: false });
} catch (error) {
set({
isLoading: false,
error: error instanceof Error ? error.message : 'Login failed',
error: error instanceof Error ? error.message : "Login failed",
});
throw error;
}
@@ -60,13 +72,19 @@ export const useAuthStore = create<AuthState>((set) => ({
register: async (email, username, password, displayName) => {
set({ isLoading: true, error: null });
try {
await api.post('/auth/register', { email, username, password, display_name: displayName || username });
const user = await api.get<User>('/auth/me');
await api.post("/auth/register", {
email,
username,
password,
display_name: displayName || username,
});
const user = await api.get<User>("/auth/me");
set({ user, isAuthenticated: true, isLoading: false });
} catch (error) {
set({
isLoading: false,
error: error instanceof Error ? error.message : 'Registration failed',
error:
error instanceof Error ? error.message : "Registration failed",
});
throw error;
}
@@ -75,12 +93,12 @@ export const useAuthStore = create<AuthState>((set) => ({
logout: async () => {
set({ isLoading: true, error: null });
try {
await api.post('/auth/logout');
await api.post("/auth/logout");
set({ user: null, isAuthenticated: false, isLoading: false });
} catch (error) {
set({
isLoading: false,
error: error instanceof Error ? error.message : 'Logout failed',
error: error instanceof Error ? error.message : "Logout failed",
});
throw error;
}
@@ -89,14 +107,15 @@ export const useAuthStore = create<AuthState>((set) => ({
fetchMe: async () => {
set({ isLoading: true, error: null });
try {
const user = await api.get<User>('/auth/me');
const user = await api.get<User>("/auth/me");
set({ user, isAuthenticated: true, isLoading: false });
} catch (error) {
set({
user: null,
isAuthenticated: 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) => {
set({ isLoading: true, error: null });
try {
const user = await api.patch<User>('/auth/me', data);
const user = await api.patch<User>("/auth/me", data);
set({ user, isLoading: false });
} catch (error) {
set({
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;
}