feat: frontend auth — login/register modal + user badge
- API service (api.js): fetch wrapper with JWT, auto-refresh on 401 - AuthContext: user state, login/register/logout, loading, roles - AuthModal: tabbed login/register form matching .pen design - User badge in toolbar (Sandbox + WorldMap) with initial avatar - "Entrar" button when not logged in - CSS: auth overlay, card, tabs, inputs, error state, user badge - Auth is opt-in: app works fully without login Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import { CHIPTUNE_PRESET } from './presets/chiptune.js';
|
|||||||
import { useIsMobile } from './hooks/useIsMobile.js';
|
import { useIsMobile } from './hooks/useIsMobile.js';
|
||||||
import { usePinchZoom } from './hooks/usePinchZoom.js';
|
import { usePinchZoom } from './hooks/usePinchZoom.js';
|
||||||
import { getModulesByCategory } from './engine/moduleRegistry.js';
|
import { getModulesByCategory } from './engine/moduleRegistry.js';
|
||||||
|
import { useAuth } from './services/AuthContext.jsx';
|
||||||
|
|
||||||
export default function App({ onSwitchToGame }) {
|
export default function App({ onSwitchToGame }) {
|
||||||
const [, forceUpdate] = useState(0);
|
const [, forceUpdate] = useState(0);
|
||||||
@@ -21,6 +22,7 @@ export default function App({ onSwitchToGame }) {
|
|||||||
const [tempWire, setTempWire] = useState(null);
|
const [tempWire, setTempWire] = useState(null);
|
||||||
const connectingRef = useRef(null);
|
const connectingRef = useRef(null);
|
||||||
const [presetModal, setPresetModal] = useState(null);
|
const [presetModal, setPresetModal] = useState(null);
|
||||||
|
const { user, isLoggedIn, openAuth, logout } = useAuth();
|
||||||
const importRef = useRef(null);
|
const importRef = useRef(null);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
@@ -316,6 +318,14 @@ export default function App({ onSwitchToGame }) {
|
|||||||
{state.modules.length} modules · {state.connections.length} wires
|
{state.modules.length} modules · {state.connections.length} wires
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{isLoggedIn ? (
|
||||||
|
<div className="user-badge" onClick={logout} title="Cerrar sesion">
|
||||||
|
<div className="user-avatar">{user.username?.[0]?.toUpperCase()}</div>
|
||||||
|
<span className="user-name">{user.username}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button className="login-btn" onClick={openAuth}>Entrar</button>
|
||||||
|
)}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<button className="mobile-menu-btn" onClick={() => setMenuOpen(true)}>≡</button>
|
<button className="mobile-menu-btn" onClick={() => setMenuOpen(true)}>≡</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
114
packages/client/src/components/AuthModal.jsx
Normal file
114
packages/client/src/components/AuthModal.jsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useAuth } from '../services/AuthContext.jsx';
|
||||||
|
|
||||||
|
export default function AuthModal() {
|
||||||
|
const { showAuth, closeAuth, login, register } = useAuth();
|
||||||
|
const [tab, setTab] = useState('login'); // 'login' | 'register'
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
if (!showAuth) return null;
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
if (tab === 'login') {
|
||||||
|
await login(email, password);
|
||||||
|
} else {
|
||||||
|
await register(email, username, password);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Error');
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setError('');
|
||||||
|
setEmail('');
|
||||||
|
setUsername('');
|
||||||
|
setPassword('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="auth-overlay" onClick={closeAuth}>
|
||||||
|
<div className="auth-card" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="auth-logo">
|
||||||
|
<div className="auth-logo-box">~</div>
|
||||||
|
<span className="auth-logo-name">Reaktor</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="auth-tabs">
|
||||||
|
<button
|
||||||
|
className={`auth-tab ${tab === 'login' ? 'active' : ''}`}
|
||||||
|
onClick={() => { setTab('login'); reset(); }}
|
||||||
|
>
|
||||||
|
Iniciar Sesion
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`auth-tab ${tab === 'register' ? 'active' : ''}`}
|
||||||
|
onClick={() => { setTab('register'); reset(); }}
|
||||||
|
>
|
||||||
|
Registrarse
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="auth-form">
|
||||||
|
<label className="auth-label">EMAIL</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="auth-input"
|
||||||
|
placeholder="tu@email.com"
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{tab === 'register' && (
|
||||||
|
<>
|
||||||
|
<label className="auth-label">USUARIO</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="auth-input"
|
||||||
|
placeholder="username"
|
||||||
|
value={username}
|
||||||
|
onChange={e => setUsername(e.target.value)}
|
||||||
|
minLength={3}
|
||||||
|
maxLength={50}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label className="auth-label">CONTRASEÑA</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="auth-input"
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
minLength={6}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <div className="auth-error">{error}</div>}
|
||||||
|
|
||||||
|
<button type="submit" className="auth-submit" disabled={loading}>
|
||||||
|
{loading ? '...' : tab === 'login' ? 'Entrar' : 'Crear Cuenta'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button className="auth-skip" onClick={closeAuth}>
|
||||||
|
Continuar sin cuenta
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="auth-close" onClick={closeAuth}>✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import MobileTabBar from '../components/MobileTabBar.jsx';
|
import MobileTabBar from '../components/MobileTabBar.jsx';
|
||||||
import { useIsMobile } from '../hooks/useIsMobile.js';
|
import { useIsMobile } from '../hooks/useIsMobile.js';
|
||||||
|
import { useAuth } from '../services/AuthContext.jsx';
|
||||||
import { WORLD_1 } from './levels/world1.js';
|
import { WORLD_1 } from './levels/world1.js';
|
||||||
import { WORLD_2 } from './levels/world2.js';
|
import { WORLD_2 } from './levels/world2.js';
|
||||||
import { WORLD_3 } from './levels/world3.js';
|
import { WORLD_3 } from './levels/world3.js';
|
||||||
@@ -53,6 +54,7 @@ export default function WorldMap({ onSelectLevel, onSandbox, onAdmin }) {
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const searchRef = useRef(null);
|
const searchRef = useRef(null);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const { user, isLoggedIn, openAuth, logout } = useAuth();
|
||||||
|
|
||||||
const query = search.trim().toLowerCase();
|
const query = search.trim().toLowerCase();
|
||||||
|
|
||||||
@@ -90,6 +92,14 @@ export default function WorldMap({ onSelectLevel, onSandbox, onAdmin }) {
|
|||||||
🛠
|
🛠
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{isLoggedIn ? (
|
||||||
|
<div className="user-badge" onClick={logout} title="Cerrar sesion">
|
||||||
|
<div className="user-avatar">{user.username?.[0]?.toUpperCase()}</div>
|
||||||
|
<span className="user-name">{user.username}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button className="login-btn" onClick={openAuth}>Entrar</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -801,6 +801,103 @@ input, textarea, [contenteditable] { -webkit-user-select: text; user-select: tex
|
|||||||
.admin-star-btn.zero { color: var(--red); }
|
.admin-star-btn.zero { color: var(--red); }
|
||||||
.admin-star-btn.zero:hover { border-color: var(--red); color: var(--red); }
|
.admin-star-btn.zero:hover { border-color: var(--red); color: var(--red); }
|
||||||
|
|
||||||
|
/* ===== Auth Modal ===== */
|
||||||
|
.auth-overlay {
|
||||||
|
position: fixed; inset: 0; background: rgba(0,0,0,0.75);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
z-index: 600; animation: fadeIn 0.2s;
|
||||||
|
}
|
||||||
|
.auth-card {
|
||||||
|
width: 400px; max-width: calc(100% - 32px); background: var(--panel);
|
||||||
|
border: 1px solid var(--border); border-radius: 16px;
|
||||||
|
padding: 32px; display: flex; flex-direction: column; gap: 20px;
|
||||||
|
align-items: center; position: relative;
|
||||||
|
}
|
||||||
|
.auth-logo { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.auth-logo-box {
|
||||||
|
width: 40px; height: 40px; background: var(--surface); border-radius: 8px;
|
||||||
|
border: 1px solid var(--accent); display: flex; align-items: center;
|
||||||
|
justify-content: center; font-size: 22px; font-weight: 700; color: var(--accent);
|
||||||
|
}
|
||||||
|
.auth-logo-name { font-size: 22px; font-weight: 700; color: var(--text); }
|
||||||
|
|
||||||
|
.auth-tabs {
|
||||||
|
display: flex; width: 100%; background: var(--surface); border-radius: 8px;
|
||||||
|
padding: 4px; gap: 4px;
|
||||||
|
}
|
||||||
|
.auth-tab {
|
||||||
|
flex: 1; padding: 10px 0; border: none; border-radius: 6px; cursor: pointer;
|
||||||
|
font-size: 13px; font-weight: 600; font-family: inherit; text-align: center;
|
||||||
|
background: transparent; color: var(--text2); transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.auth-tab.active { background: var(--accent); color: #000; }
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
display: flex; flex-direction: column; gap: 12px; width: 100%;
|
||||||
|
}
|
||||||
|
.auth-label {
|
||||||
|
font-size: 10px; font-weight: 700; color: var(--text2);
|
||||||
|
letter-spacing: 1px; text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.auth-input {
|
||||||
|
width: 100%; padding: 12px 14px; background: var(--bg);
|
||||||
|
border: 1px solid var(--border); border-radius: 8px;
|
||||||
|
color: var(--text); font-size: 14px; font-family: inherit;
|
||||||
|
-webkit-user-select: text; user-select: text;
|
||||||
|
}
|
||||||
|
.auth-input:focus { outline: none; border-color: var(--accent); }
|
||||||
|
|
||||||
|
.auth-submit {
|
||||||
|
width: 100%; padding: 14px 0; background: var(--accent);
|
||||||
|
border: none; border-radius: 8px; color: #000;
|
||||||
|
font-size: 14px; font-weight: 700; cursor: pointer;
|
||||||
|
font-family: inherit; transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.auth-submit:hover { opacity: 0.9; }
|
||||||
|
.auth-submit:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.auth-error {
|
||||||
|
padding: 8px 12px; background: rgba(255,68,102,0.1);
|
||||||
|
border: 1px solid var(--red); border-radius: 6px;
|
||||||
|
color: var(--red); font-size: 12px; text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-skip {
|
||||||
|
background: none; border: none; color: var(--text2);
|
||||||
|
font-size: 13px; cursor: pointer; font-family: inherit;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.auth-skip:hover { color: var(--text); }
|
||||||
|
|
||||||
|
.auth-close {
|
||||||
|
position: absolute; top: 12px; right: 12px;
|
||||||
|
width: 32px; height: 32px; border-radius: 16px;
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
color: var(--text); font-size: 14px; cursor: pointer;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User badge in toolbar */
|
||||||
|
.user-badge {
|
||||||
|
display: flex; align-items: center; gap: 6px; cursor: pointer;
|
||||||
|
padding: 4px 10px; border-radius: 6px; background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.user-badge:hover { border-color: var(--accent); }
|
||||||
|
.user-avatar {
|
||||||
|
width: 22px; height: 22px; border-radius: 11px;
|
||||||
|
background: var(--accent); display: flex; align-items: center;
|
||||||
|
justify-content: center; font-size: 10px; font-weight: 700; color: #000;
|
||||||
|
}
|
||||||
|
.user-name { font-size: 11px; font-weight: 600; color: var(--text); }
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
padding: 4px 12px; border: 1px solid var(--accent); border-radius: 6px;
|
||||||
|
background: transparent; color: var(--accent); cursor: pointer;
|
||||||
|
font-size: 11px; font-weight: 600; font-family: inherit;
|
||||||
|
}
|
||||||
|
.login-btn:hover { background: var(--accent); color: #000; }
|
||||||
|
|
||||||
/* ===== Fullscreen Keyboard ===== */
|
/* ===== Fullscreen Keyboard ===== */
|
||||||
.keyboard-fullscreen {
|
.keyboard-fullscreen {
|
||||||
position: fixed; inset: 0; z-index: 500;
|
position: fixed; inset: 0; z-index: 500;
|
||||||
|
|||||||
@@ -2,16 +2,22 @@ import React, { useState } from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import GameApp from './game/GameApp.jsx';
|
import GameApp from './game/GameApp.jsx';
|
||||||
|
import { AuthProvider } from './services/AuthContext.jsx';
|
||||||
|
import AuthModal from './components/AuthModal.jsx';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
function Root() {
|
function Root() {
|
||||||
const [mode, setMode] = useState('game'); // 'game' | 'sandbox'
|
const [mode, setMode] = useState('game'); // 'game' | 'sandbox'
|
||||||
|
|
||||||
if (mode === 'sandbox') {
|
return (
|
||||||
return <App onSwitchToGame={() => setMode('game')} />;
|
<AuthProvider>
|
||||||
}
|
{mode === 'sandbox'
|
||||||
|
? <App onSwitchToGame={() => setMode('game')} />
|
||||||
return <GameApp onSwitchToSandbox={() => setMode('sandbox')} />;
|
: <GameApp onSwitchToSandbox={() => setMode('sandbox')} />
|
||||||
|
}
|
||||||
|
<AuthModal />
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(<Root />);
|
createRoot(document.getElementById('root')).render(<Root />);
|
||||||
|
|||||||
74
packages/client/src/services/AuthContext.jsx
Normal file
74
packages/client/src/services/AuthContext.jsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||||
|
import { auth as authApi, users as usersApi, setAccessToken, setOnUnauthorized } from './api.js';
|
||||||
|
|
||||||
|
const AuthContext = createContext(null);
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
return useContext(AuthContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthProvider({ children }) {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [showAuth, setShowAuth] = useState(false);
|
||||||
|
|
||||||
|
const logout = useCallback(async () => {
|
||||||
|
try { await authApi.logout(); } catch {}
|
||||||
|
setAccessToken(null);
|
||||||
|
setUser(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// On mount, try to refresh session
|
||||||
|
useEffect(() => {
|
||||||
|
setOnUnauthorized(() => {
|
||||||
|
setUser(null);
|
||||||
|
setAccessToken(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
authApi.refresh().then(async (ok) => {
|
||||||
|
if (ok) {
|
||||||
|
try {
|
||||||
|
const me = await usersApi.me();
|
||||||
|
setUser(me);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const login = useCallback(async (email, password) => {
|
||||||
|
const data = await authApi.login(email, password);
|
||||||
|
setAccessToken(data.accessToken);
|
||||||
|
setUser(data.user);
|
||||||
|
setShowAuth(false);
|
||||||
|
return data.user;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const register = useCallback(async (email, username, password) => {
|
||||||
|
const data = await authApi.register(email, username, password);
|
||||||
|
setAccessToken(data.accessToken);
|
||||||
|
setUser(data.user);
|
||||||
|
setShowAuth(false);
|
||||||
|
return data.user;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
user,
|
||||||
|
loading,
|
||||||
|
isLoggedIn: !!user,
|
||||||
|
isAdmin: user?.role === 'admin',
|
||||||
|
isPremium: user?.role === 'premium' || user?.role === 'admin',
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
showAuth,
|
||||||
|
openAuth: () => setShowAuth(true),
|
||||||
|
closeAuth: () => setShowAuth(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
packages/client/src/services/api.js
Normal file
83
packages/client/src/services/api.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
const API_BASE = '/api/v1';
|
||||||
|
|
||||||
|
let _accessToken = null;
|
||||||
|
let _onUnauthorized = null;
|
||||||
|
|
||||||
|
export function setAccessToken(token) { _accessToken = token; }
|
||||||
|
export function getAccessToken() { return _accessToken; }
|
||||||
|
export function setOnUnauthorized(fn) { _onUnauthorized = fn; }
|
||||||
|
|
||||||
|
async function request(method, path, body = null, opts = {}) {
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
if (_accessToken) headers['Authorization'] = `Bearer ${_accessToken}`;
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
credentials: 'include', // send cookies (refresh token)
|
||||||
|
body: body ? JSON.stringify(body) : null,
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 401 && !path.includes('/auth/')) {
|
||||||
|
// Try to refresh
|
||||||
|
const refreshed = await refreshToken();
|
||||||
|
if (refreshed) {
|
||||||
|
headers['Authorization'] = `Bearer ${_accessToken}`;
|
||||||
|
const retry = await fetch(`${API_BASE}${path}`, {
|
||||||
|
method, headers, credentials: 'include',
|
||||||
|
body: body ? JSON.stringify(body) : null,
|
||||||
|
});
|
||||||
|
if (retry.ok) return retry.json();
|
||||||
|
}
|
||||||
|
_onUnauthorized?.();
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data.error || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshToken() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/auth/refresh`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!res.ok) return false;
|
||||||
|
const data = await res.json();
|
||||||
|
_accessToken = data.accessToken;
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
export const auth = {
|
||||||
|
register: (email, username, password) =>
|
||||||
|
request('POST', '/auth/register', { email, username, password }),
|
||||||
|
login: (email, password) =>
|
||||||
|
request('POST', '/auth/login', { email, password }),
|
||||||
|
logout: () => request('POST', '/auth/logout'),
|
||||||
|
refresh: refreshToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Users
|
||||||
|
export const users = {
|
||||||
|
me: () => request('GET', '/users/me'),
|
||||||
|
update: (data) => request('PATCH', '/users/me', data),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Admin
|
||||||
|
export const admin = {
|
||||||
|
stats: () => request('GET', '/admin/stats'),
|
||||||
|
users: (params = '') => request('GET', `/admin/users${params ? '?' + params : ''}`),
|
||||||
|
updateUser: (id, data) => request('PATCH', `/admin/users/${id}`, data),
|
||||||
|
patches: (params = '') => request('GET', `/admin/patches${params ? '?' + params : ''}`),
|
||||||
|
updatePatch: (id, data) => request('PATCH', `/admin/patches/${id}`, data),
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user