You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
451 lines
13 KiB
451 lines
13 KiB
// TODO: this file is located in the client src folder because |
|
// otherwise, Create React App will refuse to compile it. |
|
// Putting it in the server folder or in its own folder makes more sense. |
|
|
|
// This file represents the API interface for Mudbase's back-end. |
|
// Each endpoint is described by its endpoint address, |
|
// a request structure, a response structure and |
|
// a checking function which determines request validity. |
|
|
|
export enum ItemType { |
|
Song = 0, |
|
Artist, |
|
Album, |
|
Tag |
|
} |
|
|
|
export interface ArtistDetails { |
|
artistId: number, |
|
name: string, |
|
storeLinks?: string[], |
|
} |
|
export function isArtistDetails(q: any): q is ArtistDetails { |
|
return 'artistId' in q; |
|
} |
|
export interface AlbumDetails { |
|
albumId: number, |
|
name: string, |
|
storeLinks?: string[], |
|
} |
|
export function isAlbumDetails(q: any): q is ArtistDetails { |
|
return 'albumId' in q; |
|
} |
|
export interface TagDetails { |
|
tagId: number, |
|
name: string, |
|
parent?: TagDetails, |
|
storeLinks?: string[], |
|
} |
|
export function isTagDetails(q: any): q is TagDetails { |
|
return 'tagId' in q; |
|
} |
|
export interface RankingDetails { |
|
rankingId: number, |
|
type: ItemType, // The item type being ranked |
|
rankedId: number, // The item being ranked |
|
context: ArtistDetails | TagDetails, |
|
value: number, // The ranking (higher = better) |
|
} |
|
export function isRankingDetails(q: any): q is RankingDetails { |
|
return 'rankingId' in q; |
|
} |
|
export interface SongDetails { |
|
songId: number, |
|
title: string, |
|
artists?: ArtistDetails[], |
|
albums?: AlbumDetails[], |
|
tags?: TagDetails[], |
|
storeLinks?: string[], |
|
rankings?: RankingDetails[], |
|
} |
|
export function isSongDetails(q: any): q is SongDetails { |
|
return 'songId' in q; |
|
} |
|
|
|
// Query for items (POST). |
|
export const QueryEndpoint = '/query'; |
|
export enum QueryElemOp { |
|
And = "AND", |
|
Or = "OR", |
|
} |
|
export enum QueryFilterOp { |
|
Eq = "EQ", |
|
Ne = "NE", |
|
In = "IN", |
|
NotIn = "NOTIN", |
|
Like = "LIKE", |
|
} |
|
export enum QueryElemProperty { |
|
songTitle = "songTitle", |
|
songId = "songId", |
|
artistName = "artistName", |
|
artistId = "artistId", |
|
albumName = "albumName", |
|
albumId = "albumId", |
|
tagId = "tagId", |
|
songStoreLinks = "songStoreLinks", //Note: treated as a JSON string for filter operations |
|
artistStoreLinks = "artistStoreLinks", //Note: treated as a JSON string for filter operations |
|
albumStoreLinks = "albumStoreLinks", //Note: treated as a JSON string for filter operations |
|
} |
|
export enum OrderByType { |
|
Name = 'name', |
|
} |
|
export enum QueryResponseType { |
|
Details = 'details', // Returns detailed result items. |
|
Ids = 'ids', // Returns IDs only. |
|
Count = 'count', // Returns an item count only. |
|
} |
|
export interface QueryElem { |
|
prop?: QueryElemProperty, |
|
propOperand?: any, |
|
propOperator?: QueryFilterOp, |
|
children?: QueryElem[] |
|
childrenOperator?: QueryElemOp, |
|
} |
|
export interface Ordering { |
|
orderBy: { |
|
type: OrderByType, |
|
} |
|
ascending: boolean, |
|
} |
|
export interface Query extends QueryElem { } |
|
export interface QueryRequest { |
|
query: Query, |
|
offsetsLimits: OffsetsLimits, |
|
ordering: Ordering, |
|
responseType: QueryResponseType |
|
} |
|
export interface QueryResponse { |
|
songs: SongDetails[] | number[] | number, // Details | IDs | count, depending on QueryResponseType |
|
artists: ArtistDetails[] | number[] | number, |
|
tags: TagDetails[] | number[] | number, |
|
albums: AlbumDetails[] | number[] | number, |
|
} |
|
// Note: use -1 as an infinity limit. |
|
export interface OffsetsLimits { |
|
songOffset?: number, |
|
songLimit?: number, |
|
artistOffset?: number, |
|
artistLimit?: number, |
|
tagOffset?: number, |
|
tagLimit?: number, |
|
albumOffset?: number, |
|
albumLimit?: number, |
|
} |
|
export function checkQueryElem(elem: any): boolean { |
|
if (elem.childrenOperator && elem.children) { |
|
elem.children.forEach((child: any) => { |
|
if (!checkQueryElem(child)) { |
|
return false; |
|
} |
|
}); |
|
} |
|
return (elem.childrenOperator && elem.children) || |
|
("prop" in elem && "propOperand" in elem && "propOperator" in elem) || |
|
Object.keys(elem).length === 0; |
|
} |
|
export function checkQueryRequest(req: any): boolean { |
|
return 'query' in req |
|
&& 'offsetsLimits' in req |
|
&& 'ordering' in req |
|
&& 'responseType' in req |
|
&& checkQueryElem(req.query); |
|
} |
|
|
|
// Get song details (GET). |
|
export const SongDetailsEndpoint = '/song/:id'; |
|
export interface SongDetailsRequest { } |
|
export interface SongDetailsResponse { |
|
title: string, |
|
storeLinks: string[], |
|
artistIds: number[], |
|
albumIds: number[], |
|
tagIds: number[], |
|
} |
|
export function checkSongDetailsRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Get artist details (GET). |
|
export const ArtistDetailsEndpoint = '/artist/:id'; |
|
export interface ArtistDetailsRequest { } |
|
export interface ArtistDetailsResponse { |
|
name: string, |
|
tagIds: number[], |
|
storeLinks: string[], |
|
} |
|
export function checkArtistDetailsRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Create a new song (POST). |
|
export const CreateSongEndpoint = '/song'; |
|
export interface CreateSongRequest { |
|
title: string; |
|
artistIds?: number[]; |
|
albumIds?: number[]; |
|
tagIds?: number[]; |
|
storeLinks?: string[]; |
|
} |
|
export interface CreateSongResponse { |
|
id: number; |
|
} |
|
export function checkCreateSongRequest(req: any): boolean { |
|
return "body" in req && |
|
"title" in req.body; |
|
} |
|
|
|
// Modify an existing song (PUT). |
|
export const ModifySongEndpoint = '/song/:id'; |
|
export interface ModifySongRequest { |
|
title?: string; |
|
artistIds?: number[]; |
|
albumIds?: number[]; |
|
tagIds?: number[]; |
|
storeLinks?: string[]; |
|
} |
|
export interface ModifySongResponse { } |
|
export function checkModifySongRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Create a new album (POST). |
|
export const CreateAlbumEndpoint = '/album'; |
|
export interface CreateAlbumRequest { |
|
name: string; |
|
tagIds?: number[]; |
|
artistIds?: number[]; |
|
storeLinks?: string[]; |
|
} |
|
export interface CreateAlbumResponse { |
|
id: number; |
|
} |
|
export function checkCreateAlbumRequest(req: any): boolean { |
|
return "body" in req && |
|
"name" in req.body; |
|
} |
|
|
|
// Modify an existing album (PUT). |
|
export const ModifyAlbumEndpoint = '/album/:id'; |
|
export interface ModifyAlbumRequest { |
|
name?: string; |
|
tagIds?: number[]; |
|
artistIds?: number[]; |
|
storeLinks?: string[]; |
|
} |
|
export interface ModifyAlbumResponse { } |
|
export function checkModifyAlbumRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Get album details (GET). |
|
export const AlbumDetailsEndpoint = '/album/:id'; |
|
export interface AlbumDetailsRequest { } |
|
export interface AlbumDetailsResponse { |
|
name: string; |
|
tagIds: number[]; |
|
artistIds: number[]; |
|
songIds: number[]; |
|
storeLinks: string[]; |
|
} |
|
export function checkAlbumDetailsRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Create a new artist (POST). |
|
export const CreateArtistEndpoint = '/artist'; |
|
export interface CreateArtistRequest { |
|
name: string; |
|
tagIds?: number[]; |
|
storeLinks?: string[]; |
|
} |
|
export interface CreateArtistResponse { |
|
id: number; |
|
} |
|
export function checkCreateArtistRequest(req: any): boolean { |
|
return "body" in req && |
|
"name" in req.body; |
|
} |
|
|
|
// Modify an existing artist (PUT). |
|
export const ModifyArtistEndpoint = '/artist/:id'; |
|
export interface ModifyArtistRequest { |
|
name?: string, |
|
tagIds?: number[]; |
|
storeLinks?: string[], |
|
} |
|
export interface ModifyArtistResponse { } |
|
export function checkModifyArtistRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Create a new tag (POST). |
|
export const CreateTagEndpoint = '/tag'; |
|
export interface CreateTagRequest { |
|
name: string; |
|
parentId?: number; |
|
} |
|
export interface CreateTagResponse { |
|
id: number; |
|
} |
|
export function checkCreateTagRequest(req: any): boolean { |
|
return "body" in req && |
|
"name" in req.body; |
|
} |
|
|
|
// Modify an existing tag (PUT). |
|
export const ModifyTagEndpoint = '/tag/:id'; |
|
export interface ModifyTagRequest { |
|
name?: string, |
|
parentId?: number; |
|
} |
|
export interface ModifyTagResponse { } |
|
export function checkModifyTagRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Get tag details (GET). |
|
export const TagDetailsEndpoint = '/tag/:id'; |
|
export interface TagDetailsRequest { } |
|
export interface TagDetailsResponse { |
|
name: string, |
|
parentId?: number, |
|
} |
|
export function checkTagDetailsRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Delete tag (DELETE). |
|
export const DeleteTagEndpoint = '/tag/:id'; |
|
export interface DeleteTagRequest { } |
|
export interface DeleteTagResponse { } |
|
export function checkDeleteTagRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Merge tag (POST). |
|
export const MergeTagEndpoint = '/tag/:id/merge/:toId'; |
|
export interface MergeTagRequest { } |
|
export interface MergeTagResponse { } |
|
export function checkMergeTagRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Register a user (POST). |
|
// TODO: add e-mail verification. |
|
export const RegisterUserEndpoint = '/register'; |
|
export interface RegisterUserRequest { |
|
email: string, |
|
password: string, |
|
} |
|
export interface RegisterUserResponse { } |
|
export function checkPassword(password: string): boolean { |
|
const result = (password.length < 32) && |
|
(password.length >= 8) && |
|
password.split("").every(char => char.charCodeAt(0) <= 127) && // is ASCII |
|
(/[a-z]/g.test(password)) && // has lowercase |
|
(/[A-Z]/g.test(password)) && // has uppercase |
|
(/[0-9]/g.test(password)) && // has number |
|
(/[!@#$%^&*()_+/]/g.test(password)) // has special character; |
|
|
|
console.log("Password check for ", password, ": ", result); |
|
return result; |
|
} |
|
export function checkEmail(email: string): boolean { |
|
const re = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; |
|
const result = re.test(String(email).toLowerCase()); |
|
console.log("Email check for ", email, ": ", result); |
|
return result; |
|
} |
|
export function checkRegisterUserRequest(req: any): boolean { |
|
return "body" in req && |
|
"email" in req.body && |
|
"password" in req.body && |
|
checkEmail(req.body.email) && |
|
checkPassword(req.body.password); |
|
} |
|
|
|
// Note: Login is handled by Passport.js, so it is not explicitly written here. |
|
export const LoginEndpoint = "/login"; |
|
export const LogoutEndpoint = "/logout"; |
|
|
|
export enum IntegrationType { |
|
SpotifyClientCredentials = "SpotifyClientCredentials", |
|
YoutubeWebScraper = "YoutubeWebScraper", |
|
} |
|
|
|
export interface SpotifyClientCredentialsDetails { |
|
clientId: string, |
|
} |
|
export interface SpotifyClientCredentialsSecretDetails { |
|
clientSecret: string, |
|
} |
|
|
|
export interface YoutubeMusicWebScraperDetails {} |
|
export interface YoutubeMusicWebScraperSecretDetails {} |
|
|
|
export type IntegrationDetails = SpotifyClientCredentialsDetails | YoutubeMusicWebScraperDetails; |
|
export type IntegrationSecretDetails = SpotifyClientCredentialsSecretDetails | YoutubeMusicWebScraperSecretDetails; |
|
|
|
// Create a new integration (POST). |
|
export const CreateIntegrationEndpoint = '/integration'; |
|
export interface CreateIntegrationRequest { |
|
name: string, |
|
type: IntegrationType, |
|
details: IntegrationDetails, |
|
secretDetails: IntegrationSecretDetails, |
|
} |
|
export interface CreateIntegrationResponse { |
|
id: number; |
|
} |
|
export function checkCreateIntegrationRequest(req: any): boolean { |
|
return "body" in req && |
|
"name" in req.body && |
|
"type" in req.body && |
|
"details" in req.body && |
|
"secretDetails" in req.body && |
|
(req.body.type in IntegrationType); |
|
} |
|
|
|
// Modify an existing integration (PUT). |
|
export const ModifyIntegrationEndpoint = '/integration/:id'; |
|
export interface ModifyIntegrationRequest { |
|
name?: string, |
|
type?: IntegrationType, |
|
details?: IntegrationDetails, |
|
secretDetails?: IntegrationSecretDetails, |
|
} |
|
export interface ModifyIntegrationResponse { } |
|
export function checkModifyIntegrationRequest(req: any): boolean { |
|
if("type" in req.body && !(req.body.type in IntegrationType)) return false; |
|
return true; |
|
} |
|
|
|
// Get integration details (GET). |
|
export const IntegrationDetailsEndpoint = '/integration/:id'; |
|
export interface IntegrationDetailsRequest { } |
|
export interface IntegrationDetailsResponse { |
|
name: string, |
|
type: IntegrationType, |
|
details: IntegrationDetails, |
|
} |
|
export function checkIntegrationDetailsRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// List integrations (GET). |
|
export const ListIntegrationsEndpoint = '/integration'; |
|
export interface ListIntegrationsRequest { } |
|
export interface ListIntegrationsItem extends IntegrationDetailsResponse { id: number } |
|
export type ListIntegrationsResponse = ListIntegrationsItem[]; |
|
export function checkListIntegrationsRequest(req: any): boolean { |
|
return true; |
|
} |
|
|
|
// Delete integration (DELETE). |
|
export const DeleteIntegrationEndpoint = '/integration/:id'; |
|
export interface DeleteIntegrationRequest { } |
|
export interface DeleteIntegrationResponse { } |
|
export function checkDeleteIntegrationRequest(req: any): boolean { |
|
return true; |
|
} |