feat: auth & admin
This commit is contained in:
+41
-20
@@ -1,4 +1,9 @@
|
||||
import { fetchBaseQuery, type BaseQueryFn, type FetchArgs, type FetchBaseQueryError } from '@reduxjs/toolkit/query/react';
|
||||
import {
|
||||
fetchBaseQuery,
|
||||
type BaseQueryFn,
|
||||
type FetchArgs,
|
||||
type FetchBaseQueryError,
|
||||
} from '@reduxjs/toolkit/query/react';
|
||||
import type { RootState } from '../store';
|
||||
import { getApiBaseUrl } from '../config/runtime-config';
|
||||
import { logout, setTokens } from '../store/slices/auth';
|
||||
@@ -13,33 +18,49 @@ const rawBaseQuery = () =>
|
||||
},
|
||||
});
|
||||
|
||||
export const baseQueryWithReauth: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> =
|
||||
async (args, api, extraOptions) => {
|
||||
let result = await rawBaseQuery()(args, api, extraOptions);
|
||||
export const baseQueryWithReauth: BaseQueryFn<
|
||||
string | FetchArgs,
|
||||
unknown,
|
||||
FetchBaseQueryError
|
||||
> = async (args, api, extraOptions) => {
|
||||
let result = await rawBaseQuery()(args, api, extraOptions);
|
||||
|
||||
if (result.error?.status === 401) {
|
||||
const state = api.getState() as RootState;
|
||||
const refreshToken = state.auth.refreshToken;
|
||||
if (result.error?.status === 401) {
|
||||
const state = api.getState() as RootState;
|
||||
const refreshToken = state.auth.refreshToken;
|
||||
|
||||
if (refreshToken) {
|
||||
const refreshResult = await rawBaseQuery()({
|
||||
if (refreshToken) {
|
||||
const refreshResult = await rawBaseQuery()(
|
||||
{
|
||||
url: '/auth/refresh',
|
||||
method: 'POST',
|
||||
body: { refreshToken },
|
||||
}, api, extraOptions);
|
||||
},
|
||||
api,
|
||||
extraOptions,
|
||||
);
|
||||
|
||||
if (refreshResult.data) {
|
||||
const { accessToken, refreshToken: newRefresh, expiresIn } =
|
||||
(refreshResult.data as { accessToken: string; refreshToken: string; expiresIn: number });
|
||||
api.dispatch(setTokens({ accessToken, refreshToken: newRefresh, expiresIn }));
|
||||
result = await rawBaseQuery()(args, api, extraOptions);
|
||||
} else {
|
||||
api.dispatch(logout());
|
||||
}
|
||||
if (refreshResult.data) {
|
||||
const {
|
||||
accessToken,
|
||||
refreshToken: newRefresh,
|
||||
expiresIn,
|
||||
} = refreshResult.data as {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
};
|
||||
api.dispatch(
|
||||
setTokens({ accessToken, refreshToken: newRefresh, expiresIn }),
|
||||
);
|
||||
result = await rawBaseQuery()(args, api, extraOptions);
|
||||
} else {
|
||||
api.dispatch(logout());
|
||||
}
|
||||
} else {
|
||||
api.dispatch(logout());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'],
|
||||
}),
|
||||
|
||||
@@ -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 }],
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
+10
-1
@@ -4,6 +4,15 @@ import { baseQueryWithReauth } from './baseQuery';
|
||||
export const api = createApi({
|
||||
reducerPath: 'api',
|
||||
baseQuery: baseQueryWithReauth,
|
||||
tagTypes: ['Track', 'Album', 'Artist', 'Playlist', 'Download', 'Like', 'User', 'Storage'],
|
||||
tagTypes: [
|
||||
'Track',
|
||||
'Album',
|
||||
'Artist',
|
||||
'Playlist',
|
||||
'Download',
|
||||
'Like',
|
||||
'User',
|
||||
'Storage',
|
||||
],
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user