Initial open-source release (MIT): image2api AI gateway
Full Go backend + Vue 3 frontend, OpenAI-compatible API, multi-provider account pools, billing/admin, Docker one-command deploy with auto HTTPS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// Client-side auth state. The session token lives in localStorage and is sent
|
||||
// as `Authorization: Bearer <token>` on every admin API call (see api.js).
|
||||
// The server slides the 24h session whenever it's used with <22h left, so an
|
||||
// active admin never gets logged out; we also re-validate via /me on a timer.
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const TOKEN_KEY = 'gw_token'
|
||||
const BASE = import.meta.env.VITE_API_BASE || ''
|
||||
|
||||
export const auth = reactive({
|
||||
token: localStorage.getItem(TOKEN_KEY) || '',
|
||||
user: null, // { id, email, name, role, status, credits, invite_code, ... }
|
||||
ready: false, // true once an initial /me check has resolved
|
||||
loginOpen: false, // is the login modal showing?
|
||||
loginIntent: '', // where to go after a successful login
|
||||
startMode: 'login',// which tab the modal opens on: 'login' | 'register'
|
||||
pendingInvite: '', // invite code from a /?ref=CODE link, sent with register
|
||||
})
|
||||
|
||||
export function isAuthed() { return !!auth.token && !!auth.user }
|
||||
export function isAdmin() { return isAuthed() && auth.user.role === 'admin' }
|
||||
export function isAgent() { return isAuthed() && auth.user.role === 'agent' }
|
||||
export function getToken() { return auth.token }
|
||||
|
||||
/** Open the login modal, remembering where the user wanted to go. */
|
||||
export function openLogin(intent = '') { auth.loginIntent = intent || ''; auth.startMode = 'login'; auth.loginOpen = true }
|
||||
/** Open straight to the register tab, carrying an optional invite code.
|
||||
* Default landing is the home page — an invitee clicking a /?ref=CODE
|
||||
* link should NOT be punted into the画图 flow before they've explored. */
|
||||
export function openRegister(inviteCode = '', intent = '/') {
|
||||
auth.pendingInvite = inviteCode || ''
|
||||
auth.loginIntent = intent || ''
|
||||
auth.startMode = 'register'
|
||||
auth.loginOpen = true
|
||||
}
|
||||
export function closeLogin() { auth.loginOpen = false }
|
||||
|
||||
export function setSession(token, user) {
|
||||
auth.token = token || ''
|
||||
auth.user = user || null
|
||||
if (token) localStorage.setItem(TOKEN_KEY, token)
|
||||
else localStorage.removeItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export function clearSession() {
|
||||
auth.token = ''
|
||||
auth.user = null
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
/** Validate the stored token against /me. Refreshes auth.user; clears on 401.
|
||||
* Returns the user (or null). Hitting /me also slides the server session. */
|
||||
export async function refreshMe() {
|
||||
if (!auth.token) { auth.user = null; auth.ready = true; return null }
|
||||
try {
|
||||
const r = await fetch(`${BASE}/admin/api/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${auth.token}` },
|
||||
})
|
||||
if (r.ok) {
|
||||
const d = await r.json()
|
||||
auth.user = d.user
|
||||
} else {
|
||||
clearSession()
|
||||
}
|
||||
} catch {
|
||||
// network error — keep the token, don't force a logout
|
||||
}
|
||||
auth.ready = true
|
||||
return auth.user
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
if (auth.token) {
|
||||
try {
|
||||
await fetch(`${BASE}/admin/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${auth.token}` },
|
||||
})
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
clearSession()
|
||||
}
|
||||
|
||||
// Keep the session warm: re-validate every 10 minutes while a tab is open.
|
||||
setInterval(() => { if (auth.token) refreshMe() }, 10 * 60 * 1000)
|
||||
Reference in New Issue
Block a user