Add configurable agent max level and default sub-agent credit ratio, per-agent block direct player login on suspend, admin/agent wallet transaction views, and match detail my-bets section with refreshed player card styling. Co-authored-by: Cursor <cursoragent@cursor.com>
171 lines
4.8 KiB
TypeScript
171 lines
4.8 KiB
TypeScript
import { ref, computed } from 'vue';
|
|
|
|
export type StaffUserType = 'ADMIN' | 'AGENT';
|
|
|
|
export interface StaffUser {
|
|
id: string;
|
|
username: string;
|
|
userType: StaffUserType;
|
|
locale?: string;
|
|
role?: string;
|
|
agentLevel?: number | null;
|
|
maxAgentLevel?: number | null;
|
|
canManageSubAgents?: boolean;
|
|
}
|
|
|
|
const TOKEN_KEY = 'manage_token';
|
|
const USER_KEY = 'manage_user';
|
|
|
|
function decodeJwtStaffClaims(rawToken: string): Partial<StaffUser> | null {
|
|
try {
|
|
const segment = rawToken.split('.')[1];
|
|
if (!segment) return null;
|
|
const padded = segment.replace(/-/g, '+').replace(/_/g, '/');
|
|
const payload = JSON.parse(atob(padded)) as {
|
|
sub?: string;
|
|
username?: string;
|
|
userType?: string;
|
|
role?: string;
|
|
};
|
|
if (payload.userType !== 'ADMIN' && payload.userType !== 'AGENT') return null;
|
|
if (!payload.sub || !payload.username) return null;
|
|
return {
|
|
id: payload.sub,
|
|
username: payload.username,
|
|
userType: payload.userType as StaffUserType,
|
|
role: payload.role,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function loadUser(): StaffUser | null {
|
|
try {
|
|
const raw = localStorage.getItem(USER_KEY);
|
|
if (!raw) return null;
|
|
const parsed = JSON.parse(raw) as Partial<StaffUser>;
|
|
if (!parsed.id || !parsed.username || !parsed.userType) return null;
|
|
return parsed as StaffUser;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function migrateLegacyTokens() {
|
|
if (localStorage.getItem(TOKEN_KEY)) return;
|
|
const legacyAdmin = localStorage.getItem('admin_token');
|
|
const legacyAgent = localStorage.getItem('agent_token');
|
|
if (legacyAdmin) {
|
|
localStorage.setItem(TOKEN_KEY, legacyAdmin);
|
|
localStorage.removeItem('admin_token');
|
|
localStorage.removeItem(USER_KEY);
|
|
return;
|
|
}
|
|
if (legacyAgent) {
|
|
localStorage.setItem(TOKEN_KEY, legacyAgent);
|
|
localStorage.removeItem('agent_token');
|
|
localStorage.removeItem(USER_KEY);
|
|
}
|
|
}
|
|
|
|
migrateLegacyTokens();
|
|
|
|
const token = ref(localStorage.getItem(TOKEN_KEY) || '');
|
|
const user = ref<StaffUser | null>(loadUser());
|
|
|
|
/** Align manage_user.userType with JWT when localStorage is stale (common after account switch). */
|
|
export function reconcileStaffSessionFromToken(): boolean {
|
|
if (!token.value) return false;
|
|
const claims = decodeJwtStaffClaims(token.value);
|
|
if (!claims?.userType || !claims.id || !claims.username) return false;
|
|
|
|
if (
|
|
user.value?.id === claims.id &&
|
|
user.value.username === claims.username &&
|
|
user.value.userType === claims.userType
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const next: StaffUser = {
|
|
id: claims.id,
|
|
username: claims.username,
|
|
userType: claims.userType,
|
|
locale: user.value?.locale,
|
|
role: claims.role ?? user.value?.role,
|
|
};
|
|
user.value = next;
|
|
localStorage.setItem(USER_KEY, JSON.stringify(next));
|
|
return true;
|
|
}
|
|
|
|
reconcileStaffSessionFromToken();
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('storage', () => {
|
|
token.value = localStorage.getItem(TOKEN_KEY) || '';
|
|
user.value = loadUser();
|
|
reconcileStaffSessionFromToken();
|
|
});
|
|
}
|
|
|
|
export function clearStaffSession() {
|
|
token.value = '';
|
|
user.value = null;
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
localStorage.removeItem(USER_KEY);
|
|
localStorage.removeItem('admin_token');
|
|
localStorage.removeItem('agent_token');
|
|
void import('../utils/session-hydrate').then((m) => m.resetStaffSessionHydration());
|
|
}
|
|
|
|
function resolveUserType(): StaffUserType | null {
|
|
return user.value?.userType ?? decodeJwtStaffClaims(token.value)?.userType ?? null;
|
|
}
|
|
|
|
export function useAuthStore() {
|
|
const isAdmin = computed(() => resolveUserType() === 'ADMIN');
|
|
const isAgent = computed(() => resolveUserType() === 'AGENT');
|
|
const isTier1Agent = computed(() => isAgent.value && user.value?.agentLevel === 1);
|
|
const isTier2Agent = computed(() => isAgent.value && user.value?.agentLevel === 2);
|
|
const canManageSubAgents = computed(() => {
|
|
if (!isAgent.value) return false;
|
|
if (user.value?.canManageSubAgents != null) return user.value.canManageSubAgents;
|
|
const level = user.value?.agentLevel;
|
|
const max = user.value?.maxAgentLevel;
|
|
if (level == null || level < 1) return false;
|
|
if (max == null || max === 0) return true;
|
|
return level < max;
|
|
});
|
|
const portalLabel = computed(() => (isAdmin.value ? '平台后台' : '代理后台'));
|
|
|
|
function setSession(newToken: string, newUser: StaffUser) {
|
|
token.value = newToken;
|
|
user.value = newUser;
|
|
localStorage.setItem(TOKEN_KEY, newToken);
|
|
localStorage.setItem(USER_KEY, JSON.stringify(newUser));
|
|
localStorage.removeItem('admin_token');
|
|
localStorage.removeItem('agent_token');
|
|
}
|
|
|
|
function logout() {
|
|
clearStaffSession();
|
|
}
|
|
|
|
return {
|
|
token,
|
|
user,
|
|
isAdmin,
|
|
isAgent,
|
|
isTier1Agent,
|
|
isTier2Agent,
|
|
canManageSubAgents,
|
|
portalLabel,
|
|
setSession,
|
|
logout,
|
|
clearStaffSession,
|
|
reconcileStaffSessionFromToken,
|
|
};
|
|
}
|