feat: auth & admin

This commit is contained in:
2026-06-03 10:41:53 +03:00
parent 612d0f0125
commit 7dc59fb3c4
120 changed files with 4683 additions and 2159 deletions
+24 -4
View File
@@ -7,12 +7,27 @@ export const adminApi = api.injectEndpoints({
query: () => '/admin/users',
providesTags: ['User'],
}),
createUser: build.mutation<User, { username: string; password: string; email?: string; role: 'admin' | 'user' }>({
createUser: build.mutation<
User,
{
username: string;
password: string;
email?: string;
role: 'admin' | 'user';
}
>({
query: (body) => ({ url: '/admin/users', method: 'POST', body }),
invalidatesTags: ['User'],
}),
updateUser: build.mutation<User, { id: string; role?: 'admin' | 'user'; email?: string }>({
query: ({ id, ...body }) => ({ url: `/admin/users/${id}`, method: 'PATCH', body }),
updateUser: build.mutation<
User,
{ id: string; role?: 'admin' | 'user'; email?: string }
>({
query: ({ id, ...body }) => ({
url: `/admin/users/${id}`,
method: 'PATCH',
body,
}),
invalidatesTags: ['User'],
}),
deleteUser: build.mutation<void, string>({
@@ -23,4 +38,9 @@ export const adminApi = api.injectEndpoints({
overrideExisting: false,
});
export const { useGetUsersQuery, useCreateUserMutation, useUpdateUserMutation, useDeleteUserMutation } = adminApi;
export const {
useGetUsersQuery,
useCreateUserMutation,
useUpdateUserMutation,
useDeleteUserMutation,
} = adminApi;
+4 -1
View File
@@ -9,7 +9,10 @@ export const authApi = api.injectEndpoints({
logout: build.mutation<void, void>({
query: () => ({ url: '/auth/logout', method: 'POST' }),
}),
refreshToken: build.mutation<{ accessToken: string; refreshToken: string; expiresIn: number }, { refreshToken: string }>({
refreshToken: build.mutation<
{ accessToken: string; refreshToken: string; expiresIn: number },
{ refreshToken: string }
>({
query: (body) => ({ url: '/auth/refresh', method: 'POST', body }),
}),
me: build.query<import('../types').User, void>({
+17 -3
View File
@@ -3,11 +3,20 @@ import type { DownloadJob } from '../types';
export const downloadsApi = api.injectEndpoints({
endpoints: (build) => ({
getDownloads: build.query<DownloadJob[], { status?: DownloadJob['status'] } | void>({
getDownloads: build.query<
DownloadJob[],
{ status?: DownloadJob['status'] } | void
>({
query: (params) => ({ url: '/downloads', params: params ?? {} }),
providesTags: ['Download'],
}),
addDownload: build.mutation<DownloadJob, { url: string; metadata?: { title?: string; artist?: string; album?: string } }>({
addDownload: build.mutation<
DownloadJob,
{
url: string;
metadata?: { title?: string; artist?: string; album?: string };
}
>({
query: (body) => ({ url: '/downloads', method: 'POST', body }),
invalidatesTags: ['Download'],
}),
@@ -23,4 +32,9 @@ export const downloadsApi = api.injectEndpoints({
overrideExisting: false,
});
export const { useGetDownloadsQuery, useAddDownloadMutation, useCancelDownloadMutation, useRetryDownloadMutation } = downloadsApi;
export const {
useGetDownloadsQuery,
useAddDownloadMutation,
useCancelDownloadMutation,
useRetryDownloadMutation,
} = downloadsApi;
+47 -9
View File
@@ -1,5 +1,11 @@
import { api } from '../index';
import type { Track, Album, Artist, PaginatedResponse, LibraryFilters } from '../types';
import type {
Track,
Album,
Artist,
PaginatedResponse,
LibraryFilters,
} from '../types';
export const libraryApi = api.injectEndpoints({
endpoints: (build) => ({
@@ -7,18 +13,32 @@ export const libraryApi = api.injectEndpoints({
query: (filters) => ({ url: '/library/tracks', params: filters ?? {} }),
providesTags: (result) =>
result
? [...result.items.map(({ id }) => ({ type: 'Track' as const, id })), 'Track']
? [
...result.items.map(({ id }) => ({ type: 'Track' as const, id })),
'Track',
]
: ['Track'],
}),
getTrack: build.query<Track, string>({
query: (id) => `/library/tracks/${id}`,
providesTags: (_r, _e, id) => [{ type: 'Track', id }],
}),
getAlbums: build.query<PaginatedResponse<Album>, { search?: string; artistId?: string; page?: number; pageSize?: number } | void>({
getAlbums: build.query<
PaginatedResponse<Album>,
{
search?: string;
artistId?: string;
page?: number;
pageSize?: number;
} | void
>({
query: (params) => ({ url: '/library/albums', params: params ?? {} }),
providesTags: (result) =>
result
? [...result.items.map(({ id }) => ({ type: 'Album' as const, id })), 'Album']
? [
...result.items.map(({ id }) => ({ type: 'Album' as const, id })),
'Album',
]
: ['Album'],
}),
getAlbum: build.query<Album, string>({
@@ -27,13 +47,25 @@ export const libraryApi = api.injectEndpoints({
}),
getAlbumTracks: build.query<Track[], string>({
query: (albumId) => `/library/albums/${albumId}/tracks`,
providesTags: (_r, _e, albumId) => [{ type: 'Album', id: albumId }, 'Track'],
providesTags: (_r, _e, albumId) => [
{ type: 'Album', id: albumId },
'Track',
],
}),
getArtists: build.query<PaginatedResponse<Artist>, { search?: string; page?: number; pageSize?: number } | void>({
getArtists: build.query<
PaginatedResponse<Artist>,
{ search?: string; page?: number; pageSize?: number } | void
>({
query: (params) => ({ url: '/library/artists', params: params ?? {} }),
providesTags: (result) =>
result
? [...result.items.map(({ id }) => ({ type: 'Artist' as const, id })), 'Artist']
? [
...result.items.map(({ id }) => ({
type: 'Artist' as const,
id,
})),
'Artist',
]
: ['Artist'],
}),
getArtist: build.query<Artist, string>({
@@ -42,9 +74,15 @@ export const libraryApi = api.injectEndpoints({
}),
getArtistAlbums: build.query<Album[], string>({
query: (artistId) => `/library/artists/${artistId}/albums`,
providesTags: (_r, _e, artistId) => [{ type: 'Artist', id: artistId }, 'Album'],
providesTags: (_r, _e, artistId) => [
{ type: 'Artist', id: artistId },
'Album',
],
}),
searchLibrary: build.query<{ tracks: Track[]; albums: Album[]; artists: Artist[] }, string>({
searchLibrary: build.query<
{ tracks: Track[]; albums: Album[]; artists: Artist[] },
string
>({
query: (q) => ({ url: '/library/search', params: { q } }),
providesTags: ['Track', 'Album', 'Artist'],
}),
+4 -1
View File
@@ -7,7 +7,10 @@ export const likesApi = api.injectEndpoints({
invalidatesTags: (_r, _e, id) => ['Like', { type: 'Track', id }],
}),
unlikeTrack: build.mutation<void, string>({
query: (trackId) => ({ url: `/likes/tracks/${trackId}`, method: 'DELETE' }),
query: (trackId) => ({
url: `/likes/tracks/${trackId}`,
method: 'DELETE',
}),
invalidatesTags: (_r, _e, id) => ['Like', { type: 'Track', id }],
}),
}),
+36 -9
View File
@@ -15,25 +15,52 @@ export const playlistsApi = api.injectEndpoints({
query: (id) => `/playlists/${id}/tracks`,
providesTags: (_r, _e, id) => [{ type: 'Playlist', id }, 'Track'],
}),
createPlaylist: build.mutation<Playlist, { name: string; description?: string; isPublic?: boolean }>({
createPlaylist: build.mutation<
Playlist,
{ name: string; description?: string; isPublic?: boolean }
>({
query: (body) => ({ url: '/playlists', method: 'POST', body }),
invalidatesTags: ['Playlist'],
}),
updatePlaylist: build.mutation<Playlist, { id: string; name?: string; description?: string; isPublic?: boolean }>({
query: ({ id, ...body }) => ({ url: `/playlists/${id}`, method: 'PATCH', body }),
updatePlaylist: build.mutation<
Playlist,
{ id: string; name?: string; description?: string; isPublic?: boolean }
>({
query: ({ id, ...body }) => ({
url: `/playlists/${id}`,
method: 'PATCH',
body,
}),
invalidatesTags: (_r, _e, { id }) => [{ type: 'Playlist', id }],
}),
deletePlaylist: build.mutation<void, string>({
query: (id) => ({ url: `/playlists/${id}`, method: 'DELETE' }),
invalidatesTags: ['Playlist'],
}),
addTrackToPlaylist: build.mutation<void, { playlistId: string; trackId: string }>({
query: ({ playlistId, trackId }) => ({ url: `/playlists/${playlistId}/tracks`, method: 'POST', body: { trackId } }),
invalidatesTags: (_r, _e, { playlistId }) => [{ type: 'Playlist', id: playlistId }],
addTrackToPlaylist: build.mutation<
void,
{ playlistId: string; trackId: string }
>({
query: ({ playlistId, trackId }) => ({
url: `/playlists/${playlistId}/tracks`,
method: 'POST',
body: { trackId },
}),
invalidatesTags: (_r, _e, { playlistId }) => [
{ type: 'Playlist', id: playlistId },
],
}),
removeTrackFromPlaylist: build.mutation<void, { playlistId: string; trackId: string; position: number }>({
query: ({ playlistId, position }) => ({ url: `/playlists/${playlistId}/tracks/${position}`, method: 'DELETE' }),
invalidatesTags: (_r, _e, { playlistId }) => [{ type: 'Playlist', id: playlistId }],
removeTrackFromPlaylist: build.mutation<
void,
{ playlistId: string; trackId: string; position: number }
>({
query: ({ playlistId, position }) => ({
url: `/playlists/${playlistId}/tracks/${position}`,
method: 'DELETE',
}),
invalidatesTags: (_r, _e, { playlistId }) => [
{ type: 'Playlist', id: playlistId },
],
}),
}),
overrideExisting: false,
+9 -2
View File
@@ -12,11 +12,18 @@ export const storageApi = api.injectEndpoints({
invalidatesTags: ['Storage', 'Track', 'Album', 'Artist'],
}),
deleteTrackFile: build.mutation<void, string>({
query: (trackId) => ({ url: `/storage/tracks/${trackId}`, method: 'DELETE' }),
query: (trackId) => ({
url: `/storage/tracks/${trackId}`,
method: 'DELETE',
}),
invalidatesTags: ['Storage', { type: 'Track', id: undefined }],
}),
}),
overrideExisting: false,
});
export const { useGetStorageStatsQuery, useScanStorageMutation, useDeleteTrackFileMutation } = storageApi;
export const {
useGetStorageStatsQuery,
useScanStorageMutation,
useDeleteTrackFileMutation,
} = storageApi;
+2 -1
View File
@@ -7,7 +7,8 @@ export function getStreamUrl(trackId: string, token: string): string {
export function getCoverUrl(artUrl: string | undefined): string | undefined {
if (!artUrl) return undefined;
if (artUrl.startsWith('http://') || artUrl.startsWith('https://')) return artUrl;
if (artUrl.startsWith('http://') || artUrl.startsWith('https://'))
return artUrl;
const base = getApiBaseUrl();
return `${base}${artUrl}`;
}