feat: merge conflict resolution, site management, UI fixes

This commit is contained in:
ShukeBta
2026-05-16 17:57:34 +08:00
parent 0be8c7952e
commit cbb4b806be
84 changed files with 13826 additions and 202 deletions
+69 -6
View File
@@ -2,26 +2,89 @@ import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { User } from '../types'
import { refreshToken } from '../api/refresh'
// Single source of truth for the authenticated user + JWT.
// Persisted to localStorage so a page reload does not drop the session.
interface AuthState {
token: string | null
refreshToken: string | null
user: User | null
setSession: (token: string, user: User) => void
tier: string
setSession: (token: string, refreshToken: string, user: User) => void
setUser: (user: User) => void
setToken: (token: string) => void
setRefreshToken: (refreshToken: string) => void
logout: () => void
tokenRefresh: () => Promise<boolean>
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
(set, get) => ({
token: null,
refreshToken: null,
user: null,
setSession: (token, user) => set({ token, user }),
setUser: (user) => set({ user }),
logout: () => set({ token: null, user: null }),
tier: 'free',
setSession: (token, refreshToken, user) => set({
token,
refreshToken,
user,
tier: user.tier || 'free'
}),
setUser: (user) => set({ user, tier: user.tier || 'free' }),
setToken: (token) => set({ token }),
setRefreshToken: (refreshToken) => set({ refreshToken }),
logout: () => set({ token: null, refreshToken: null, user: null, tier: 'free' }),
tokenRefresh: async () => {
const rt = get().refreshToken
if (!rt) {
return false
}
try {
const resp = await refreshToken(rt)
set({
token: resp.token,
refreshToken: resp.refresh_token
})
return true
} catch {
// Refresh failed, need to logout
set({ token: null, refreshToken: null, user: null, tier: 'free' })
return false
}
},
}),
{ name: 'mediastationgo-auth' },
{
name: 'mediastationgo-auth',
partialize: (state) => ({
token: state.token,
refreshToken: state.refreshToken,
user: state.user,
tier: state.tier
}),
},
),
)
// Helper function to check if user is authenticated
export function isAuthenticated(): boolean {
return useAuthStore.getState().token !== null
}
// Helper function to check if user is admin
export function isAdmin(): boolean {
const user = useAuthStore.getState().user
return user?.role === 'admin'
}
// Helper function to check if user is plus
export function isPlus(): boolean {
const state = useAuthStore.getState()
return state.tier === 'plus' || state.user?.role === 'admin'
}
// Helper function to check if user is super user (admin or plus)
export function isSuperUser(): boolean {
return isAdmin() || isPlus()
}
+139
View File
@@ -0,0 +1,139 @@
import { create } from 'zustand'
import { getMyPermissions } from '../api/permission'
interface PermissionState {
permissions: Record<string, boolean>
role: string
tier: string
isSuper: boolean
isLoading: boolean
error: string | null
fetchPermissions: () => Promise<void>
hasPermission: (key: string) => boolean
clearPermissions: () => void
}
export const usePermissionStore = create<PermissionState>((set, get) => ({
permissions: {},
role: '',
tier: 'free',
isSuper: false,
isLoading: false,
error: null,
fetchPermissions: async () => {
set({ isLoading: true, error: null })
try {
const result = await getMyPermissions()
set({
permissions: result.permissions,
role: result.role,
tier: result.tier,
isSuper: result.is_super,
isLoading: false,
})
} catch (err) {
set({
isLoading: false,
error: err instanceof Error ? err.message : 'Failed to fetch permissions',
})
}
},
hasPermission: (key: string) => {
const state = get()
// Super user (admin or plus) has all permissions
if (state.isSuper) {
return true
}
return state.permissions[key] === true
},
clearPermissions: () => {
set({
permissions: {},
role: '',
tier: 'free',
isSuper: false,
error: null,
})
},
}))
// Default permissions for new users (without fetching from server)
export const defaultPermissions: Record<string, boolean> = {
can_view_dashboard: true,
can_play_media: true,
can_cast: true,
can_external_player: true,
can_favorite: true,
can_view_history: true,
can_edit_media: false,
can_rescrape: false,
can_use_ai: false,
can_capture_frames: false,
can_manage_downloads: false,
can_view_discover: false,
can_manage_subscriptions: false,
can_manage_sites: false,
can_use_ai_assistant: false,
can_manage_users: false,
can_manage_files: false,
can_manage_strm: false,
can_access_settings: false,
}
// Permission display names for UI
export const permissionDisplayNames: Record<string, string> = {
can_view_dashboard: '查看仪表盘',
can_play_media: '播放媒体',
can_cast: '投屏',
can_external_player: '外部播放器',
can_favorite: '收藏',
can_view_history: '观看历史',
can_edit_media: '编辑媒体',
can_rescrape: '重新刮削',
can_use_ai: '使用 AI 搜索',
can_capture_frames: '截图',
can_manage_downloads: '管理下载',
can_view_discover: '发现',
can_manage_subscriptions: '管理订阅',
can_manage_sites: '管理站点',
can_use_ai_assistant: 'AI 助手',
can_manage_users: '管理用户',
can_manage_files: '管理文件',
can_manage_strm: '管理 STRM',
can_access_settings: '访问设置',
}
// Permission categories for grouping
export const permissionCategories = {
basic: [
'can_view_dashboard',
'can_play_media',
'can_cast',
'can_external_player',
'can_favorite',
'can_view_history',
],
media: [
'can_edit_media',
'can_rescrape',
'can_capture_frames',
'can_manage_files',
'can_manage_strm',
],
advanced: [
'can_use_ai',
'can_use_ai_assistant',
'can_view_discover',
'can_manage_downloads',
'can_manage_subscriptions',
'can_manage_sites',
],
admin: [
'can_manage_users',
'can_access_settings',
],
}