mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
check authentication for all servers on initialization and update permission roles
This commit is contained in:
@@ -586,6 +586,20 @@ export const controller: GeneralController = {
|
|||||||
server.type,
|
server.type,
|
||||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||||
},
|
},
|
||||||
|
getUserInfo(args) {
|
||||||
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error(
|
||||||
|
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getUserInfo`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiController(
|
||||||
|
'getUserInfo',
|
||||||
|
server.type,
|
||||||
|
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||||
|
},
|
||||||
getUserList(args) {
|
getUserList(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,14 @@ export const contract = c.router({
|
|||||||
400: jfType._response.error,
|
400: jfType._response.error,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getUser: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'users/:id',
|
||||||
|
responses: {
|
||||||
|
200: jfType._response.user,
|
||||||
|
400: jfType._response.error,
|
||||||
|
},
|
||||||
|
},
|
||||||
movePlaylistItem: {
|
movePlaylistItem: {
|
||||||
body: null,
|
body: null,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -1127,6 +1127,25 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
totalRecordCount: res.body.TotalRecordCount,
|
totalRecordCount: res.body.TotalRecordCount,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getUserInfo: async (args) => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
const res = await jfApiClient(apiClientProps).getUser({
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get user info');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: res.body.Id,
|
||||||
|
isAdmin: Boolean(res.body.Policy.IsAdministrator),
|
||||||
|
name: res.body.Name,
|
||||||
|
};
|
||||||
|
},
|
||||||
movePlaylistItem: async (args) => {
|
movePlaylistItem: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -699,6 +699,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
getTopSongs: SubsonicController.getTopSongs,
|
getTopSongs: SubsonicController.getTopSongs,
|
||||||
|
getUserInfo: SubsonicController.getUserInfo,
|
||||||
getUserList: async (args) => {
|
getUserList: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,14 @@ export const contract = c.router({
|
|||||||
200: ssType._response.topSongsList,
|
200: ssType._response.topSongsList,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getUser: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getUser.view',
|
||||||
|
query: ssType._parameters.user,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.user,
|
||||||
|
},
|
||||||
|
},
|
||||||
ping: {
|
ping: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'ping.view',
|
path: 'ping.view',
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
credential,
|
credential,
|
||||||
isAdmin: resp.body.user.adminRoles,
|
isAdmin: Boolean(resp.body.user.adminRole),
|
||||||
userId: resp.body.user.username,
|
userId: resp.body.user.username,
|
||||||
username: body.username,
|
username: body.username,
|
||||||
};
|
};
|
||||||
@@ -1472,6 +1472,25 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getUserInfo: async (args) => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
const res = await ssApiClient(apiClientProps).getUser({
|
||||||
|
query: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get user info');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: res.body.user.username,
|
||||||
|
isAdmin: Boolean(res.body.user.adminRole),
|
||||||
|
name: res.body.user.username,
|
||||||
|
};
|
||||||
|
},
|
||||||
removeFromPlaylist: async ({ apiClientProps, query }) => {
|
removeFromPlaylist: async ({ apiClientProps, query }) => {
|
||||||
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
||||||
query: {
|
query: {
|
||||||
@@ -1583,7 +1602,6 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
search: async (args) => {
|
search: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -4,75 +4,217 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { getServerById, useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
|
import { getServerById, useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
|
||||||
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
|
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
import { AuthState } from '/@/shared/types/types';
|
||||||
import { AuthState, ServerListItem, ServerType } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
|
|
||||||
export const useServerAuthenticated = () => {
|
export const useServerAuthenticated = () => {
|
||||||
const priorServerId = useRef<string | undefined>(undefined);
|
const priorServerId = useRef<string | undefined>(undefined);
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const [ready, setReady] = useState(
|
const [ready, setReady] = useState(AuthState.VALID);
|
||||||
server?.type === ServerType.NAVIDROME ? AuthState.VALID : AuthState.VALID,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { updateServer } = useAuthStoreActions();
|
const { setCurrentServer, updateServer } = useAuthStoreActions();
|
||||||
|
|
||||||
const authenticateNavidrome = useCallback(
|
const authenticateServer = useCallback(
|
||||||
async (server: ServerListItem) => {
|
async (serverWithAuth: NonNullable<ReturnType<typeof getServerById>>) => {
|
||||||
// This trick works because navidrome-api.ts will internally check for authentication
|
|
||||||
// failures and try to log in again (where available). So, all that's necessary is
|
|
||||||
// making one request first
|
|
||||||
try {
|
try {
|
||||||
await api.controller.getSongList({
|
setReady(AuthState.LOADING);
|
||||||
apiClientProps: { serverId: server?.id || '' },
|
|
||||||
query: {
|
// Use userId if available, otherwise fall back to username (for Subsonic/Navidrome)
|
||||||
limit: 1,
|
const userId = serverWithAuth.userId || serverWithAuth.username;
|
||||||
sortBy: SongListSort.NAME,
|
|
||||||
sortOrder: SortOrder.ASC,
|
if (!userId) {
|
||||||
startIndex: 0,
|
throw new Error('No user ID or username available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, try getUserInfo to check if current credentials are still valid
|
||||||
|
logFn.info(logMsg[LogCategory.SYSTEM].authenticatingServer, {
|
||||||
|
category: LogCategory.SYSTEM,
|
||||||
|
meta: {
|
||||||
|
method: 'getUserInfo',
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
serverName: serverWithAuth.name,
|
||||||
|
serverType: serverWithAuth.type,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setReady(AuthState.VALID);
|
try {
|
||||||
|
const userInfo = await api.controller.getUserInfo({
|
||||||
|
apiClientProps: {
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userInfo) {
|
||||||
|
throw new Error('Failed to get user info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update server with user info (in case isAdmin changed)
|
||||||
|
updateServer(serverWithAuth.id, {
|
||||||
|
isAdmin: userInfo.isAdmin,
|
||||||
|
});
|
||||||
|
|
||||||
|
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||||
|
category: LogCategory.SYSTEM,
|
||||||
|
meta: {
|
||||||
|
isAdmin: userInfo.isAdmin,
|
||||||
|
method: 'getUserInfo',
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
serverName: serverWithAuth.name,
|
||||||
|
serverType: serverWithAuth.type,
|
||||||
|
userId: userInfo.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setReady(AuthState.VALID);
|
||||||
|
return;
|
||||||
|
} catch (getUserInfoError: any) {
|
||||||
|
// Check if it's a forbidden/authentication error (401 or 403)
|
||||||
|
const isForbiddenError =
|
||||||
|
getUserInfoError?.response?.status === 401 ||
|
||||||
|
getUserInfoError?.response?.status === 403 ||
|
||||||
|
getUserInfoError?.message?.toLowerCase().includes('forbidden') ||
|
||||||
|
getUserInfoError?.message?.toLowerCase().includes('unauthorized');
|
||||||
|
|
||||||
|
// Only reauthenticate if it's a forbidden error AND password is saved
|
||||||
|
if (isForbiddenError && serverWithAuth.savePassword && localSettings) {
|
||||||
|
const password = await localSettings.passwordGet(serverWithAuth.id);
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
logFn.info(logMsg[LogCategory.SYSTEM].authenticatingServer, {
|
||||||
|
category: LogCategory.SYSTEM,
|
||||||
|
meta: {
|
||||||
|
method: 'authenticate',
|
||||||
|
reason: 'getUserInfo failed with forbidden error',
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
serverName: serverWithAuth.name,
|
||||||
|
serverType: serverWithAuth.type,
|
||||||
|
url: serverWithAuth.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Authenticate using the API controller
|
||||||
|
const authData = await api.controller.authenticate(
|
||||||
|
serverWithAuth.url,
|
||||||
|
{
|
||||||
|
legacy: false,
|
||||||
|
password,
|
||||||
|
username: serverWithAuth.username,
|
||||||
|
},
|
||||||
|
serverWithAuth.type,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!authData) {
|
||||||
|
throw new Error('Authentication failed: No data returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update server with new credentials
|
||||||
|
const updatedServer = {
|
||||||
|
credential: authData.credential,
|
||||||
|
isAdmin: authData.isAdmin,
|
||||||
|
userId: authData.userId,
|
||||||
|
username: authData.username,
|
||||||
|
...(authData.ndCredential !== undefined && {
|
||||||
|
ndCredential: authData.ndCredential,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
updateServer(serverWithAuth.id, updatedServer);
|
||||||
|
|
||||||
|
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||||
|
category: LogCategory.SYSTEM,
|
||||||
|
meta: {
|
||||||
|
isAdmin: authData.isAdmin,
|
||||||
|
method: 'authenticate',
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
serverName: serverWithAuth.name,
|
||||||
|
serverType: serverWithAuth.type,
|
||||||
|
userId: authData.userId,
|
||||||
|
username: authData.username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setReady(AuthState.VALID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not a forbidden error, or no password saved, rethrow the error
|
||||||
|
throw getUserInfoError;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Clear server credentials (and saved password).
|
const errorMessage = (error as Error).message || 'Authentication failed';
|
||||||
if (server.savePassword && localSettings) {
|
|
||||||
localSettings.passwordRemove(server.id);
|
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
|
||||||
|
category: LogCategory.SYSTEM,
|
||||||
|
meta: {
|
||||||
|
error: errorMessage,
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
serverName: serverWithAuth.name,
|
||||||
|
serverType: serverWithAuth.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear server credentials and saved password on failure
|
||||||
|
if (serverWithAuth.savePassword && localSettings) {
|
||||||
|
localSettings.passwordRemove(serverWithAuth.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.credential = '';
|
toast.error({
|
||||||
updateServer(server.id, server);
|
message: errorMessage,
|
||||||
|
});
|
||||||
toast.error({ message: (error as Error).message });
|
|
||||||
|
|
||||||
|
// Log the user out by setting current server to null
|
||||||
|
setCurrentServer(null);
|
||||||
setReady(AuthState.INVALID);
|
setReady(AuthState.INVALID);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateServer],
|
[updateServer, setCurrentServer],
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedAuth = debounce((server: ServerListItem) => {
|
const debouncedAuth = debounce(
|
||||||
authenticateNavidrome(server).catch(console.error);
|
(serverWithAuth: NonNullable<ReturnType<typeof getServerById>>) => {
|
||||||
}, 300);
|
authenticateServer(serverWithAuth).catch(console.error);
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!server) {
|
if (!server) {
|
||||||
|
logFn.debug(logMsg[LogCategory.SYSTEM].serverAuthenticationInvalid, {
|
||||||
|
category: LogCategory.SYSTEM,
|
||||||
|
meta: {
|
||||||
|
reason: 'No server selected',
|
||||||
|
},
|
||||||
|
});
|
||||||
setReady(AuthState.INVALID);
|
setReady(AuthState.INVALID);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priorServerId.current !== server?.id) {
|
if (priorServerId.current !== server.id) {
|
||||||
const serverWithAuth = getServerById(server!.id);
|
const serverWithAuth = getServerById(server.id);
|
||||||
priorServerId.current = server?.id || '';
|
priorServerId.current = server.id;
|
||||||
|
|
||||||
if (server?.type === ServerType.NAVIDROME) {
|
if (!serverWithAuth) {
|
||||||
setReady(AuthState.LOADING);
|
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationError, {
|
||||||
debouncedAuth(serverWithAuth!);
|
category: LogCategory.SYSTEM,
|
||||||
} else {
|
meta: {
|
||||||
setReady(AuthState.VALID);
|
reason: 'Server not found in store',
|
||||||
|
serverId: server.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setReady(AuthState.INVALID);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setReady(AuthState.LOADING);
|
||||||
|
debouncedAuth(serverWithAuth);
|
||||||
}
|
}
|
||||||
}, [debouncedAuth, server]);
|
}, [debouncedAuth, server]);
|
||||||
|
|
||||||
|
|||||||
@@ -66,5 +66,12 @@ export const logMsg = {
|
|||||||
scrobbledTimeupdate: 'Scrobbled a timeupdate event',
|
scrobbledTimeupdate: 'Scrobbled a timeupdate event',
|
||||||
scrobbledUnpause: 'Scrobbled an unpause event',
|
scrobbledUnpause: 'Scrobbled an unpause event',
|
||||||
},
|
},
|
||||||
[LogCategory.SYSTEM]: {},
|
[LogCategory.SYSTEM]: {
|
||||||
|
authenticatingServer: 'Authenticating server',
|
||||||
|
serverAuthenticationAborted: 'Server authentication aborted',
|
||||||
|
serverAuthenticationError: 'Server authentication error',
|
||||||
|
serverAuthenticationFailed: 'Server authentication failed',
|
||||||
|
serverAuthenticationInvalid: 'Server authentication invalid',
|
||||||
|
serverAuthenticationSuccess: 'Server authentication successful',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,9 +7,13 @@ const baseResponse = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const authenticate = z.object({
|
const userParameters = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = z.object({
|
||||||
user: z.object({
|
user: z.object({
|
||||||
adminRoles: z.boolean(),
|
adminRole: z.boolean(),
|
||||||
commentRole: z.boolean(),
|
commentRole: z.boolean(),
|
||||||
coverArtRole: z.boolean(),
|
coverArtRole: z.boolean(),
|
||||||
downloadRole: z.boolean(),
|
downloadRole: z.boolean(),
|
||||||
@@ -26,6 +30,8 @@ const authenticate = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authenticate = user;
|
||||||
|
|
||||||
const authenticateParameters = z.object({
|
const authenticateParameters = z.object({
|
||||||
c: z.string(),
|
c: z.string(),
|
||||||
f: z.string(),
|
f: z.string(),
|
||||||
@@ -641,6 +647,7 @@ export const ssType = {
|
|||||||
structuredLyrics: structuredLyricsParameters,
|
structuredLyrics: structuredLyricsParameters,
|
||||||
topSongsList: topSongsListParameters,
|
topSongsList: topSongsListParameters,
|
||||||
updatePlaylist: updatePlaylistParameters,
|
updatePlaylist: updatePlaylistParameters,
|
||||||
|
user: userParameters,
|
||||||
},
|
},
|
||||||
_response: {
|
_response: {
|
||||||
album,
|
album,
|
||||||
@@ -683,5 +690,6 @@ export const ssType = {
|
|||||||
song,
|
song,
|
||||||
structuredLyrics,
|
structuredLyrics,
|
||||||
topSongsList,
|
topSongsList,
|
||||||
|
user,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1276,7 +1276,6 @@ export type ControllerEndpoint = {
|
|||||||
getAlbumInfo?: (args: AlbumDetailArgs) => Promise<AlbumInfo>;
|
getAlbumInfo?: (args: AlbumDetailArgs) => Promise<AlbumInfo>;
|
||||||
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
|
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
|
||||||
getAlbumListCount: (args: AlbumListCountArgs) => Promise<number>;
|
getAlbumListCount: (args: AlbumListCountArgs) => Promise<number>;
|
||||||
// getArtistInfo?: (args: any) => void;
|
|
||||||
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
||||||
getArtistListCount: (args: ArtistListCountArgs) => Promise<number>;
|
getArtistListCount: (args: ArtistListCountArgs) => Promise<number>;
|
||||||
getDownloadUrl: (args: DownloadArgs) => string;
|
getDownloadUrl: (args: DownloadArgs) => string;
|
||||||
@@ -1299,6 +1298,8 @@ export type ControllerEndpoint = {
|
|||||||
getStructuredLyrics?: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
|
getStructuredLyrics?: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
|
||||||
getTagList?: (args: TagListArgs) => Promise<TagListResponse>;
|
getTagList?: (args: TagListArgs) => Promise<TagListResponse>;
|
||||||
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
||||||
|
// getArtistInfo?: (args: any) => void;
|
||||||
|
getUserInfo: (args: UserInfoArgs) => Promise<UserInfoResponse>;
|
||||||
getUserList?: (args: UserListArgs) => Promise<UserListResponse>;
|
getUserList?: (args: UserListArgs) => Promise<UserListResponse>;
|
||||||
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
|
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
|
||||||
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
||||||
@@ -1393,6 +1394,7 @@ export type InternalControllerEndpoint = {
|
|||||||
) => Promise<StructuredLyric[]>;
|
) => Promise<StructuredLyric[]>;
|
||||||
getTagList?: (args: ReplaceApiClientProps<TagListArgs>) => Promise<TagListResponse>;
|
getTagList?: (args: ReplaceApiClientProps<TagListArgs>) => Promise<TagListResponse>;
|
||||||
getTopSongs: (args: ReplaceApiClientProps<TopSongListArgs>) => Promise<TopSongListResponse>;
|
getTopSongs: (args: ReplaceApiClientProps<TopSongListArgs>) => Promise<TopSongListResponse>;
|
||||||
|
getUserInfo: (args: ReplaceApiClientProps<UserInfoArgs>) => Promise<UserInfoResponse>;
|
||||||
getUserList?: (args: ReplaceApiClientProps<UserListArgs>) => Promise<UserListResponse>;
|
getUserList?: (args: ReplaceApiClientProps<UserListArgs>) => Promise<UserListResponse>;
|
||||||
movePlaylistItem?: (args: ReplaceApiClientProps<MoveItemArgs>) => Promise<void>;
|
movePlaylistItem?: (args: ReplaceApiClientProps<MoveItemArgs>) => Promise<void>;
|
||||||
removeFromPlaylist: (
|
removeFromPlaylist: (
|
||||||
@@ -1509,6 +1511,18 @@ export type TagListResponse = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserInfoArgs = BaseEndpointArgs & { query: UserInfoQuery };
|
||||||
|
|
||||||
|
export type UserInfoQuery = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserInfoResponse = {
|
||||||
|
id: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
type BaseEndpointArgsWithServer = {
|
type BaseEndpointArgsWithServer = {
|
||||||
apiClientProps: {
|
apiClientProps: {
|
||||||
server: null | ServerListItemWithCredential;
|
server: null | ServerListItemWithCredential;
|
||||||
|
|||||||
Reference in New Issue
Block a user