mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
feat: Implement granular role-based access control, enhance IPTV stream proxy with header forwarding, and improve IPTV store with per-source channel caching.
This commit is contained in:
+32
-2
@@ -3,10 +3,27 @@
|
||||
* NOT Zustand — needs to be synchronous at import time for store key generation
|
||||
*/
|
||||
|
||||
export type Role = 'super_admin' | 'admin' | 'viewer';
|
||||
|
||||
export type Permission =
|
||||
| 'source_management'
|
||||
| 'account_management'
|
||||
| 'danmaku_api'
|
||||
| 'data_management'
|
||||
| 'player_settings'
|
||||
| 'danmaku_appearance'
|
||||
| 'view_settings';
|
||||
|
||||
const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
|
||||
super_admin: ['source_management', 'account_management', 'danmaku_api', 'data_management', 'player_settings', 'danmaku_appearance', 'view_settings'],
|
||||
admin: ['player_settings', 'danmaku_appearance', 'view_settings'],
|
||||
viewer: ['view_settings'],
|
||||
};
|
||||
|
||||
export interface AuthSession {
|
||||
profileId: string;
|
||||
name: string;
|
||||
role: 'admin' | 'viewer';
|
||||
role: Role;
|
||||
}
|
||||
|
||||
const SESSION_KEY = 'kvideo-session';
|
||||
@@ -50,7 +67,20 @@ export function clearSession(): void {
|
||||
export function isAdmin(): boolean {
|
||||
const session = getSession();
|
||||
if (!session) return true; // No auth configured = full access
|
||||
return session.role === 'admin';
|
||||
return session.role === 'admin' || session.role === 'super_admin';
|
||||
}
|
||||
|
||||
export function hasPermission(permission: Permission): boolean {
|
||||
const session = getSession();
|
||||
if (!session) return true; // No auth configured = full access
|
||||
return ROLE_PERMISSIONS[session.role]?.includes(permission) ?? false;
|
||||
}
|
||||
|
||||
export function hasRole(minimumRole: Role): boolean {
|
||||
const session = getSession();
|
||||
if (!session) return true; // No auth configured = full access
|
||||
const hierarchy: Role[] = ['viewer', 'admin', 'super_admin'];
|
||||
return hierarchy.indexOf(session.role) >= hierarchy.indexOf(minimumRole);
|
||||
}
|
||||
|
||||
export function getProfileId(): string {
|
||||
|
||||
+28
-2
@@ -17,6 +17,7 @@ interface IPTVState {
|
||||
sources: IPTVSource[];
|
||||
cachedChannels: M3UChannel[];
|
||||
cachedGroups: string[];
|
||||
cachedChannelsBySource: Record<string, { channels: M3UChannel[]; groups: string[] }>;
|
||||
lastRefreshed: number;
|
||||
isLoading: boolean;
|
||||
}
|
||||
@@ -58,6 +59,7 @@ export const useIPTVStore = create<IPTVStore>()(
|
||||
sources: [],
|
||||
cachedChannels: [],
|
||||
cachedGroups: [],
|
||||
cachedChannelsBySource: {},
|
||||
lastRefreshed: 0,
|
||||
isLoading: false,
|
||||
|
||||
@@ -85,7 +87,7 @@ export const useIPTVStore = create<IPTVStore>()(
|
||||
refreshSources: async () => {
|
||||
const { sources } = get();
|
||||
if (sources.length === 0) {
|
||||
set({ cachedChannels: [], cachedGroups: [], lastRefreshed: Date.now() });
|
||||
set({ cachedChannels: [], cachedGroups: [], cachedChannelsBySource: {}, lastRefreshed: Date.now() });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,6 +96,8 @@ export const useIPTVStore = create<IPTVStore>()(
|
||||
try {
|
||||
const allChannels: M3UChannel[] = [];
|
||||
const allGroups = new Set<string>();
|
||||
const channelsBySourceRaw: Record<string, M3UChannel[]> = {};
|
||||
const groupsBySource: Record<string, Set<string>> = {};
|
||||
|
||||
const tasks = sources.map((source) => async () => {
|
||||
try {
|
||||
@@ -101,8 +105,17 @@ export const useIPTVStore = create<IPTVStore>()(
|
||||
if (!res.ok) return;
|
||||
const text = await res.text();
|
||||
const playlist = parseM3U(text);
|
||||
allChannels.push(...playlist.channels);
|
||||
// Tag channels with source info
|
||||
const tagged = playlist.channels.map(ch => ({
|
||||
...ch,
|
||||
sourceId: source.id,
|
||||
sourceName: source.name,
|
||||
}));
|
||||
allChannels.push(...tagged);
|
||||
playlist.groups.forEach((g) => allGroups.add(g));
|
||||
// Track per-source
|
||||
channelsBySourceRaw[source.id] = tagged;
|
||||
groupsBySource[source.id] = new Set(playlist.groups);
|
||||
} catch (e) {
|
||||
console.error(`Failed to fetch IPTV source: ${source.name}`, e);
|
||||
}
|
||||
@@ -113,9 +126,22 @@ export const useIPTVStore = create<IPTVStore>()(
|
||||
// Group channels with the same name into multi-route entries
|
||||
const grouped = groupChannelsByName(allChannels);
|
||||
|
||||
// Build per-source grouped data
|
||||
const cachedChannelsBySource: Record<string, { channels: M3UChannel[]; groups: string[] }> = {};
|
||||
for (const source of sources) {
|
||||
const raw = channelsBySourceRaw[source.id];
|
||||
if (raw) {
|
||||
cachedChannelsBySource[source.id] = {
|
||||
channels: groupChannelsByName(raw),
|
||||
groups: Array.from(groupsBySource[source.id] || []).sort(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
set({
|
||||
cachedChannels: grouped,
|
||||
cachedGroups: Array.from(allGroups).sort(),
|
||||
cachedChannelsBySource,
|
||||
lastRefreshed: Date.now(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface M3UChannel {
|
||||
tvgId?: string;
|
||||
tvgName?: string;
|
||||
routes?: string[];
|
||||
sourceId?: string;
|
||||
sourceName?: string;
|
||||
}
|
||||
|
||||
export interface M3UPlaylist {
|
||||
@@ -86,7 +88,7 @@ export function groupChannelsByName(channels: M3UChannel[]): M3UChannel[] {
|
||||
const groups = new Map<string, M3UChannel>();
|
||||
|
||||
for (const ch of channels) {
|
||||
const key = ch.name.toLowerCase().trim();
|
||||
const key = `${ch.sourceId || ''}::${ch.name.toLowerCase().trim()}`;
|
||||
const existing = groups.get(key);
|
||||
if (existing) {
|
||||
if (!existing.routes) {
|
||||
|
||||
Reference in New Issue
Block a user