User authentication and per-user data (#31)

Add basic user registration and login support. Each user has their own private music library.

Reviewed-on: #31
pull/34/head
Sander Vocke 5 years ago
parent 87af6e18a4
commit f1a5597598
  1. 5
      client/src/App.tsx
  2. 40
      client/src/api.ts
  3. 72
      client/src/components/MainWindow.tsx
  4. 84
      client/src/components/appbar/AppBar.tsx
  5. 30
      client/src/components/querybuilder/QBLeafElem.tsx
  6. 10
      client/src/components/querybuilder/QBNodeElem.tsx
  7. 12
      client/src/components/querybuilder/QBSelectWithRequest.tsx
  8. 4
      client/src/components/tables/ResultsTable.tsx
  9. 14
      client/src/components/windows/Windows.tsx
  10. 21
      client/src/components/windows/album/AlbumWindow.tsx
  11. 23
      client/src/components/windows/artist/ArtistWindow.tsx
  12. 131
      client/src/components/windows/login/LoginWindow.tsx
  13. 4
      client/src/components/windows/manage_tags/ManageTagMenu.tsx
  14. 12
      client/src/components/windows/manage_tags/ManageTagsWindow.tsx
  15. 4
      client/src/components/windows/manage_tags/NewTagMenu.tsx
  16. 6
      client/src/components/windows/manage_tags/TagChange.tsx
  17. 37
      client/src/components/windows/query/QueryWindow.tsx
  18. 139
      client/src/components/windows/register/RegisterWindow.tsx
  19. 20
      client/src/components/windows/song/SongWindow.tsx
  20. 25
      client/src/components/windows/tag/TagWindow.tsx
  21. 12
      client/src/lib/query/Query.tsx
  22. 102
      client/src/lib/useAuth.tsx
  23. 1446
      package-lock.json
  24. 3
      package.json
  25. 108
      scripts/gpm_retrieve/gpm_retrieve.py
  26. 140
      server/app.ts
  27. 29
      server/endpoints/AlbumDetails.ts
  28. 18
      server/endpoints/ArtistDetails.ts
  29. 6
      server/endpoints/CreateAlbum.ts
  30. 7
      server/endpoints/CreateArtist.ts
  31. 7
      server/endpoints/CreateSong.ts
  32. 7
      server/endpoints/CreateTag.ts
  33. 14
      server/endpoints/DeleteTag.ts
  34. 7
      server/endpoints/MergeTag.ts
  35. 5
      server/endpoints/ModifyAlbum.ts
  36. 6
      server/endpoints/ModifyArtist.ts
  37. 5
      server/endpoints/ModifySong.ts
  38. 6
      server/endpoints/ModifyTag.ts
  39. 25
      server/endpoints/Query.ts
  40. 49
      server/endpoints/RegisterUser.ts
  41. 22
      server/endpoints/SongDetails.ts
  42. 16
      server/endpoints/TagDetails.ts
  43. 73
      server/migrations/20201110170100_add_users.ts
  44. 32
      server/package-lock.json
  45. 3
      server/package.json
  46. 132
      server/test/integration/flows/AlbumFlow.js
  47. 126
      server/test/integration/flows/ArtistFlow.js
  48. 145
      server/test/integration/flows/AuthFlow.js
  49. 457
      server/test/integration/flows/QueryFlow.js
  50. 170
      server/test/integration/flows/SongFlow.js
  51. 108
      server/test/integration/flows/TagFlow.js
  52. 45
      server/test/integration/flows/helpers.js

@ -4,11 +4,14 @@ import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import MainWindow from './components/MainWindow';
import { ProvideAuth } from './lib/useAuth';
function App() {
return (
<DndProvider backend={HTML5Backend}>
<MainWindow />
<ProvideAuth>
<MainWindow />
</ProvideAuth>
</DndProvider>
);
}

@ -318,4 +318,42 @@ 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";

@ -1,16 +1,17 @@
import React, { useReducer, Reducer, useContext } from 'react';
import React from 'react';
import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core';
import { grey } from '@material-ui/core/colors';
import AppBar, { AppBarTab } from './appbar/AppBar';
import QueryWindow, { QueryWindowReducer } from './windows/query/QueryWindow';
import { newWindowState, newWindowReducer, WindowType } from './windows/Windows';
import QueryWindow from './windows/query/QueryWindow';
import ArtistWindow from './windows/artist/ArtistWindow';
import AlbumWindow from './windows/album/AlbumWindow';
import TagWindow from './windows/tag/TagWindow';
import SongWindow from './windows/song/SongWindow';
import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow';
import { BrowserRouter, Switch, Route, useParams, Redirect } from 'react-router-dom';
var _ = require('lodash');
import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom';
import LoginWindow from './windows/login/LoginWindow';
import { useAuth } from '../lib/useAuth';
import RegisterWindow from './windows/register/RegisterWindow';
const darkTheme = createMuiTheme({
palette: {
@ -21,6 +22,25 @@ const darkTheme = createMuiTheme({
},
});
function PrivateRoute(props: any) {
const { children, ...rest } = props;
let auth = useAuth();
return <Route {...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
}
export default function MainWindow(props: any) {
return <ThemeProvider theme={darkTheme}>
<CssBaseline />
@ -29,30 +49,38 @@ export default function MainWindow(props: any) {
<Route exact path="/">
<Redirect to={"/query"} />
</Route>
<Route path="/query">
<AppBar selectedTab={AppBarTab.Query} />
<QueryWindow/>
</Route>
<Route path="/artist/:id">
<Route path="/login">
<AppBar selectedTab={null} />
<ArtistWindow/>
<LoginWindow />
</Route>
<Route path="/tag/:id">
<Route path="/register">
<AppBar selectedTab={null} />
<TagWindow/>
<RegisterWindow />
</Route>
<Route path="/album/:id">
<PrivateRoute path="/query">
<AppBar selectedTab={AppBarTab.Query} />
<QueryWindow />
</PrivateRoute>
<PrivateRoute path="/artist/:id">
<AppBar selectedTab={null} />
<AlbumWindow/>
</Route>
<Route path="/song/:id">
<ArtistWindow />
</PrivateRoute>
<PrivateRoute path="/tag/:id">
<AppBar selectedTab={null} />
<SongWindow/>
</Route>
<Route path="/tags">
<TagWindow />
</PrivateRoute>
<PrivateRoute path="/album/:id">
<AppBar selectedTab={null} />
<AlbumWindow />
</PrivateRoute>
<PrivateRoute path="/song/:id">
<AppBar selectedTab={null} />
<SongWindow />
</PrivateRoute>
<PrivateRoute path="/tags">
<AppBar selectedTab={AppBarTab.Tags} />
<ManageTagsWindow/>
</Route>
<ManageTagsWindow />
</PrivateRoute>
</Switch>
</BrowserRouter>
</ThemeProvider>

@ -1,10 +1,9 @@
import React from 'react';
import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton, Typography } from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close';
import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton, Typography, Menu, MenuItem } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import { Link, useHistory } from 'react-router-dom';
import { WindowType } from '../windows/Windows';
import { useAuth } from '../../lib/useAuth';
export enum AppBarTab {
Query = 0,
@ -13,19 +12,58 @@ export enum AppBarTab {
export const appBarTabProps: Record<any, any> = {
[AppBarTab.Query]: {
label: <Box display="flex"><SearchIcon/><Typography variant="button">Query</Typography></Box>,
label: <Box display="flex"><SearchIcon /><Typography variant="button">Query</Typography></Box>,
path: "/query",
},
[AppBarTab.Tags]: {
label: <Box display="flex"><LocalOfferIcon/><Typography variant="button">Tags</Typography></Box>,
label: <Box display="flex"><LocalOfferIcon /><Typography variant="button">Tags</Typography></Box>,
path: "/tags",
},
}
export function UserMenu(props: {
position: null | number[],
open: boolean,
onLogout: () => void,
onClose: () => void,
}) {
let auth = useAuth();
const pos = props.open && props.position ?
{ left: props.position[0], top: props.position[1] }
: { left: 0, top: 0 }
return <Menu
open={props.open}
anchorReference="anchorPosition"
anchorPosition={pos}
keepMounted
onClose={props.onClose}
>
<Box p={2}>
{auth.user?.email || "Unknown user"}
<MenuItem
onClick={() => {
props.onClose();
props.onLogout();
}}
>Sign out</MenuItem>
</Box>
</Menu>
}
export default function AppBar(props: {
selectedTab: AppBarTab | null
}) {
const history = useHistory();
let history = useHistory();
let auth = useAuth();
const [userMenuPos, setUserMenuPos] = React.useState<null | number[]>(null);
const onOpenUserMenu = (e: any) => {
setUserMenuPos([e.clientX, e.clientY])
};
const onCloseUserMenu = () => {
setUserMenuPos(null);
};
return <>
<MuiAppBar position="static" style={{ background: 'grey' }}>
@ -35,18 +73,30 @@ export default function AppBar(props: {
<img height="30px" src={process.env.PUBLIC_URL + "/logo.svg"} alt="error"></img>
</Box>
</Link>
<Tabs
value={props.selectedTab}
onChange={(e: any, val: AppBarTab) => history.push(appBarTabProps[val].path)}
variant="scrollable"
scrollButtons="auto"
>
{Object.keys(appBarTabProps).map((tab: any, idx: number) => <MuiTab
label={appBarTabProps[tab].label}
value={idx}
/>)}
</Tabs>
<Box flexGrow={1}>
{auth.user && <Tabs
value={props.selectedTab}
onChange={(e: any, val: AppBarTab) => history.push(appBarTabProps[val].path)}
variant="scrollable"
scrollButtons="auto"
>
{Object.keys(appBarTabProps).map((tab: any, idx: number) => <MuiTab
label={appBarTabProps[tab].label}
value={idx}
/>)}
</Tabs>}
</Box>
{auth.user && <IconButton
color="primary"
onClick={(e: any) => { onOpenUserMenu(e) }}
>{auth.user.icon}</IconButton>}
</Box>
</MuiAppBar>
<UserMenu
position={userMenuPos}
open={userMenuPos !== null}
onClose={onCloseUserMenu}
onLogout={auth.signout}
/>
</>
}

@ -110,56 +110,56 @@ export function QBLeafElem(props: IProps) {
</Box>
: undefined;
if (e.a == QueryLeafBy.ArtistName &&
e.leafOp == QueryLeafOp.Equals &&
if (e.a === QueryLeafBy.ArtistName &&
e.leafOp === QueryLeafOp.Equals &&
typeof e.b == "string") {
return <QBQueryElemArtistEquals
{...props}
extraElements={extraElements}
/>
} else if (e.a == QueryLeafBy.ArtistName &&
e.leafOp == QueryLeafOp.Like &&
} else if (e.a === QueryLeafBy.ArtistName &&
e.leafOp === QueryLeafOp.Like &&
typeof e.b == "string") {
return <QBQueryElemArtistLike
{...props}
extraElements={extraElements}
/>
} else if (e.a == QueryLeafBy.AlbumName &&
e.leafOp == QueryLeafOp.Equals &&
} else if (e.a === QueryLeafBy.AlbumName &&
e.leafOp === QueryLeafOp.Equals &&
typeof e.b == "string") {
return <QBQueryElemAlbumEquals
{...props}
extraElements={extraElements}
/>
} else if (e.a == QueryLeafBy.AlbumName &&
e.leafOp == QueryLeafOp.Like &&
} else if (e.a === QueryLeafBy.AlbumName &&
e.leafOp === QueryLeafOp.Like &&
typeof e.b == "string") {
return <QBQueryElemAlbumLike
{...props}
extraElements={extraElements}
/>
} if (e.a == QueryLeafBy.SongTitle &&
e.leafOp == QueryLeafOp.Equals &&
} if (e.a === QueryLeafBy.SongTitle &&
e.leafOp === QueryLeafOp.Equals &&
typeof e.b == "string") {
return <QBQueryElemTitleEquals
{...props}
extraElements={extraElements}
/>
} else if (e.a == QueryLeafBy.SongTitle &&
e.leafOp == QueryLeafOp.Like &&
} else if (e.a === QueryLeafBy.SongTitle &&
e.leafOp === QueryLeafOp.Like &&
typeof e.b == "string") {
return <QBQueryElemTitleLike
{...props}
extraElements={extraElements}
/>
} else if (e.a == QueryLeafBy.TagInfo &&
e.leafOp == QueryLeafOp.Equals &&
} else if (e.a === QueryLeafBy.TagInfo &&
e.leafOp === QueryLeafOp.Equals &&
isTagQueryInfo(e.b)) {
return <QBQueryElemTagEquals
{...props}
extraElements={extraElements}
/>
}else if (e.leafOp == QueryLeafOp.Placeholder) {
}else if (e.leafOp === QueryLeafOp.Placeholder) {
return <QBPlaceholder
onReplace={props.onReplace}
requestFunctions={props.requestFunctions}

@ -1,10 +1,8 @@
import React from 'react';
import QBOrBlock from './QBOrBlock';
import QBAndBlock from './QBAndBlock';
import { QueryNodeElem, QueryNodeOp, QueryElem, isNodeElem, simplify } from '../../lib/query/Query';
import { QBLeafElem } from './QBLeafElem';
import { QueryNodeElem, QueryNodeOp, QueryElem, simplify } from '../../lib/query/Query';
import { QBQueryElem } from './QBQueryElem';
import { O_APPEND } from 'constants';
import { Requests } from './QueryBuilder';
export interface NodeProps {
@ -37,11 +35,11 @@ export function QBNodeElem(props: NodeProps) {
/>
});
if (e.nodeOp == QueryNodeOp.And) {
if (e.nodeOp === QueryNodeOp.And) {
return <QBAndBlock>{children}</QBAndBlock>
} else if (e.nodeOp == QueryNodeOp.Or) {
} else if (e.nodeOp === QueryNodeOp.Or) {
return <QBOrBlock>{children}</QBOrBlock>
}
throw "Unsupported node element";
throw new Error("Unsupported node element");
}

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import TextField from '@material-ui/core/TextField';
import Autocomplete from '@material-ui/lab/Autocomplete';
import CircularProgress from '@material-ui/core/CircularProgress';
@ -26,26 +26,26 @@ export default function QBSelectWithRequest(props: IProps & any) {
const loading: boolean = !options || options.forInput !== input;
const updateOptions = (forInput: string, options: any[]) => {
const updateOptions = useCallback((forInput: string, options: any[]) => {
if (forInput === input) {
setOptions({
forInput: forInput,
options: options,
});
}
}
}, [setOptions, input]);
const startRequest = (_input: string) => {
const startRequest = useCallback((_input: string) => {
setInput(_input);
(async () => {
const newOptions = await getNewOptions(_input);
updateOptions(_input, newOptions);
})();
};
}, [setInput, getNewOptions, updateOptions]);
useEffect(() => {
startRequest(input);
}, [input]);
}, [input, startRequest]);
const onInputChange = (e: any, val: any, reason: any) => {
if (reason === 'reset') {

@ -46,14 +46,12 @@ export default function SongTable(props: {
<TableBody>
{props.songs.map((song: any) => {
const title = props.songGetters.getTitle(song);
// TODO / FIXME: display artists and albums separately!
// TODO: display artists and albums separately!
const artistNames = props.songGetters.getArtistNames(song);
const artist = stringifyList(artistNames);
const mainArtistId = props.songGetters.getArtistIds(song)[0];
const mainArtistName = artistNames[0];
const albumNames = props.songGetters.getAlbumNames(song);
const album = stringifyList(albumNames);
const mainAlbumName = albumNames[0];
const mainAlbumId = props.songGetters.getAlbumIds(song)[0];
const songId = props.songGetters.getId(song);
const tagIds = props.songGetters.getTagIds(song);

@ -12,6 +12,8 @@ import AlbumWindow, { AlbumWindowReducer } from './album/AlbumWindow';
import TagWindow, { TagWindowReducer } from './tag/TagWindow';
import { songGetters } from '../../lib/songGetters';
import ManageTagsWindow, { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow';
import { RegisterWindowReducer } from './register/RegisterWindow';
import { LoginWindowReducer } from './login/LoginWindow';
export enum WindowType {
Query = "Query",
@ -20,6 +22,8 @@ export enum WindowType {
Tag = "Tag",
Song = "Song",
ManageTags = "ManageTags",
Login = "Login",
Register = "Register",
}
export interface WindowState { }
@ -31,6 +35,8 @@ export const newWindowReducer = {
[WindowType.Song]: SongWindowReducer,
[WindowType.Tag]: TagWindowReducer,
[WindowType.ManageTags]: ManageTagsWindowReducer,
[WindowType.Login]: LoginWindowReducer,
[WindowType.Register]: RegisterWindowReducer,
}
export const newWindowState = {
@ -81,5 +87,11 @@ export const newWindowState = {
alert: null,
pendingChanges: [],
}
}
},
[WindowType.Login]: () => {
return {}
},
[WindowType.Register]: () => {
return {}
},
}

@ -12,7 +12,6 @@ import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryAlbums, querySongs } from '../../../lib/backend/queries';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
var _ = require('lodash');
export type AlbumMetadata = serverApi.AlbumDetails;
export type AlbumMetadataChanges = serverApi.ModifyAlbumRequest;
@ -76,40 +75,40 @@ export function AlbumWindowControlled(props: {
state: AlbumWindowState,
dispatch: (action: any) => void,
}) {
let metadata = props.state.metadata;
let pendingChanges = props.state.pendingChanges;
let { id: albumId, metadata, pendingChanges, songsOnAlbum } = props.state;
let { dispatch } = props;
// Effect to get the album's metadata.
useEffect(() => {
getAlbumMetadata(props.state.id)
getAlbumMetadata(albumId)
.then((m: AlbumMetadata) => {
props.dispatch({
dispatch({
type: AlbumWindowStateActions.SetMetadata,
value: m
});
})
}, [metadata?.name]);
}, [albumId, dispatch]);
// Effect to get the album's songs.
useEffect(() => {
if (props.state.songsOnAlbum) { return; }
if (songsOnAlbum) { return; }
(async () => {
const songs = await querySongs({
query: {
a: QueryLeafBy.AlbumId,
b: props.state.id,
b: albumId,
leafOp: QueryLeafOp.Equals,
},
offset: 0,
limit: -1,
});
props.dispatch({
dispatch({
type: AlbumWindowStateActions.SetSongs,
value: songs,
});
})();
}, [props.state.songsOnAlbum]);
}, [songsOnAlbum, albumId, dispatch]);
const [editingName, setEditingName] = useState<string | null>(null);
const name = <Typography variant="h4"><EditableText
@ -132,7 +131,7 @@ export function AlbumWindowControlled(props: {
const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => {
const store = whichStore(link);
return store && <a
href={link} target="_blank"
href={link} target="_blank" rel="noopener noreferrer"
>
<IconButton><StoreLinkIcon
whichStore={store}

@ -1,5 +1,5 @@
import React, { useEffect, useState, useReducer } from 'react';
import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core';
import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core';
import PersonIcon from '@material-ui/icons/Person';
import * as serverApi from '../../../api';
import { WindowState } from '../Windows';
@ -12,7 +12,6 @@ import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryArtists, querySongs } from '../../../lib/backend/queries';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
var _ = require('lodash');
export type ArtistMetadata = serverApi.ArtistDetails;
export type ArtistMetadataChanges = serverApi.ModifyArtistRequest;
@ -81,40 +80,40 @@ export function ArtistWindowControlled(props: {
state: ArtistWindowState,
dispatch: (action: any) => void,
}) {
let metadata = props.state.metadata;
let pendingChanges = props.state.pendingChanges;
let { metadata, id: artistId, pendingChanges, songsByArtist } = props.state;
let { dispatch } = props;
// Effect to get the artist's metadata.
useEffect(() => {
getArtistMetadata(props.state.id)
getArtistMetadata(artistId)
.then((m: ArtistMetadata) => {
props.dispatch({
dispatch({
type: ArtistWindowStateActions.SetMetadata,
value: m
});
})
}, [metadata?.name]);
}, [artistId, dispatch]);
// Effect to get the artist's songs.
useEffect(() => {
if (props.state.songsByArtist) { return; }
if (songsByArtist) { return; }
(async () => {
const songs = await querySongs({
query: {
a: QueryLeafBy.ArtistId,
b: props.state.id,
b: artistId,
leafOp: QueryLeafOp.Equals,
},
offset: 0,
limit: -1,
});
props.dispatch({
dispatch({
type: ArtistWindowStateActions.SetSongs,
value: songs,
});
})();
}, [props.state.songsByArtist]);
}, [songsByArtist, dispatch, artistId]);
const [editingName, setEditingName] = useState<string | null>(null);
const name = <Typography variant="h4"><EditableText
@ -137,7 +136,7 @@ export function ArtistWindowControlled(props: {
const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => {
const store = whichStore(link);
return store && <a
href={link} target="_blank"
href={link} target="_blank" rel="noopener noreferrer"
>
<IconButton><StoreLinkIcon
whichStore={store}

@ -0,0 +1,131 @@
import React, { useReducer } from 'react';
import { WindowState } from "../Windows";
import { Box, Paper, Typography, TextField, Button } from "@material-ui/core";
import { useHistory, useLocation } from 'react-router';
import { useAuth, Auth } from '../../../lib/useAuth';
import Alert from '@material-ui/lab/Alert';
export enum LoginStatus {
NoneSubmitted = 0,
Unsuccessful,
// Note: no "successful" status because that would lead to a redirect.
}
export interface LoginWindowState extends WindowState {
email: string,
password: string,
status: LoginStatus,
}
export enum LoginWindowStateActions {
SetEmail = "SetEmail",
SetPassword = "SetPassword",
SetStatus = "SetStatus",
}
export function LoginWindowReducer(state: LoginWindowState, action: any) {
switch (action.type) {
case LoginWindowStateActions.SetEmail:
return { ...state, email: action.value }
case LoginWindowStateActions.SetPassword:
return { ...state, password: action.value }
case LoginWindowStateActions.SetStatus:
return { ...state, status: action.value }
default:
throw new Error("Unimplemented LoginWindow state update.")
}
}
export default function LoginWindow(props: {}) {
const [state, dispatch] = useReducer(LoginWindowReducer, {
email: "",
password: "",
status: LoginStatus.NoneSubmitted,
});
return <LoginWindowControlled state={state} dispatch={dispatch} />
}
export function LoginWindowControlled(props: {
state: LoginWindowState,
dispatch: (action: any) => void,
}) {
let history: any = useHistory();
let location: any = useLocation();
let auth: Auth = useAuth();
let { from } = location.state || { from: { pathname: "/" } };
const onSubmit = (event: any) => {
event.preventDefault();
auth.signin(props.state.email, props.state.password)
.then(() => {
history.replace(from);
}).catch((e: any) => {
props.dispatch({
type: LoginWindowStateActions.SetStatus,
value: LoginStatus.Unsuccessful,
})
})
}
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
mt={4}
width="500px"
>
<Paper>
<Box p={3}>
<Typography variant="h5">Sign in</Typography>
<form noValidate onSubmit={onSubmit}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email"
name="email"
autoFocus
onInput={(e: any) => props.dispatch({
type: LoginWindowStateActions.SetEmail,
value: e.target.value
})}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="password"
label="Password"
name="password"
type="password"
onInput={(e: any) => props.dispatch({
type: LoginWindowStateActions.SetPassword,
value: e.target.value
})}
/>
{props.state.status === LoginStatus.Unsuccessful && <Alert severity="error">
Login failed - Please check your credentials.
</Alert>
}
<Button
type="submit"
fullWidth
variant="outlined"
color="primary"
>Sign in</Button>
<Box display="flex" alignItems="center" mt={2}>
<Typography>Need an account?</Typography>
<Box flexGrow={1} ml={2}><Button
onClick={() => history.replace("/register")}
fullWidth
variant="outlined"
color="primary"
>Sign up</Button></Box>
</Box>
</form>
</Box>
</Paper>
</Box>
</Box>
}

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Menu, MenuItem, TextField, Input } from '@material-ui/core';
import React from 'react';
import { Menu, MenuItem } from '@material-ui/core';
import NestedMenuItem from "material-ui-nested-menu-item";
import MenuEditText from '../../common/MenuEditText';

@ -1,5 +1,5 @@
import React, { useEffect, useState, ReactFragment, useReducer } from 'react';
import { WindowState, newWindowReducer, WindowType } from '../Windows';
import { WindowState } from '../Windows';
import { Box, Typography, Chip, IconButton, useTheme, Button } from '@material-ui/core';
import LoyaltyIcon from '@material-ui/icons/Loyalty';
import ArrowRightIcon from '@material-ui/icons/ArrowRight';
@ -9,8 +9,6 @@ import ControlTagChanges, { TagChange, TagChangeType, submitTagChanges } from '.
import { queryTags } from '../../../lib/backend/queries';
import NewTagMenu from './NewTagMenu';
import { v4 as genUuid } from 'uuid';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import { songGetters } from '../../../lib/songGetters';
import Alert from '@material-ui/lab/Alert';
import { useHistory } from 'react-router';
var _ = require('lodash');
@ -355,6 +353,8 @@ export function ManageTagsWindowControlled(props: {
dispatch: (action: any) => void,
}) {
const [newTagMenuPos, setNewTagMenuPos] = React.useState<null | number[]>(null);
let { fetchedTags } = props.state;
let { dispatch } = props;
const onOpenNewTagMenu = (e: any) => {
setNewTagMenuPos([e.clientX, e.clientY])
@ -364,19 +364,19 @@ export function ManageTagsWindowControlled(props: {
};
useEffect(() => {
if (props.state.fetchedTags !== null) {
if (fetchedTags !== null) {
return;
}
(async () => {
const allTags = await getAllTags();
// We have the tags in list form. Now, we want to organize
// them hierarchically by giving each tag a "children" prop.
props.dispatch({
dispatch({
type: ManageTagsWindowActions.SetFetchedTags,
value: allTags,
});
})();
}, [props.state.fetchedTags]);
}, [fetchedTags, dispatch]);
const tagsWithChanges = annotateTagsWithChanges(props.state.fetchedTags || {}, props.state.pendingChanges || [])
const changedTags = organiseTags(

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Menu, MenuItem, TextField, Input } from '@material-ui/core';
import React from 'react';
import { Menu } from '@material-ui/core';
import NestedMenuItem from "material-ui-nested-menu-item";
import MenuEditText from '../../common/MenuEditText';

@ -1,7 +1,5 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import { Typography, Chip, CircularProgress, Box, Paper } from '@material-ui/core';
import { queryTags } from '../../../lib/backend/queries';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import DiscardChangesButton from '../../common/DiscardChangesButton';
import SubmitChangesButton from '../../common/SubmitChangesButton';
import { createTag, modifyTag, deleteTag, mergeTag } from '../../../lib/backend/tags';
@ -31,7 +29,7 @@ export async function submitTagChanges(changes: TagChange[]) {
var id_lookup: Record<string, number> = {}
const getId = (id_string: string) => {
return (Number(id_string) === NaN) ?
return (isNaN(Number(id_string))) ?
id_lookup[id_string] : Number(id_string);
}

@ -1,24 +1,13 @@
import React, { useEffect, useReducer } from 'react';
import { createMuiTheme, Box, LinearProgress } from '@material-ui/core';
import { QueryElem, toApiQuery, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import React, { useEffect, useReducer, useCallback } from 'react';
import { Box, LinearProgress } from '@material-ui/core';
import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import QueryBuilder from '../../querybuilder/QueryBuilder';
import * as serverApi from '../../../api';
import SongTable from '../../tables/ResultsTable';
import { songGetters } from '../../../lib/songGetters';
import { queryArtists, querySongs, queryAlbums, queryTags } from '../../../lib/backend/queries';
import { grey } from '@material-ui/core/colors';
import { WindowState } from '../Windows';
var _ = require('lodash');
const darkTheme = createMuiTheme({
palette: {
type: 'dark',
primary: {
main: grey[100],
}
},
});
export interface ResultsForQuery {
for: QueryElem,
results: any[],
@ -112,23 +101,23 @@ export function QueryWindowControlled(props: {
state: QueryWindowState,
dispatch: (action: any) => void,
}) {
let query = props.state.query;
let editing = props.state.editingQuery;
let resultsFor = props.state.resultsForQuery;
let { query, editingQuery: editing, resultsForQuery: resultsFor } = props.state;
let { dispatch } = props;
let setQuery = (q: QueryElem | null) => {
props.dispatch({ type: QueryWindowStateActions.SetQuery, value: q });
}
let setEditingQuery = (e: boolean) => {
props.dispatch({ type: QueryWindowStateActions.SetEditingQuery, value: e });
}
let setResultsForQuery = (r: ResultsForQuery | null) => {
props.dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r });
}
let setResultsForQuery = useCallback((r: ResultsForQuery | null) => {
dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r });
}, [ dispatch ]);
const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query));
const showResults = (query && resultsFor && query == resultsFor.for) ? resultsFor.results : [];
const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : [];
const doQuery = async (_query: QueryElem) => {
const doQuery = useCallback(async (_query: QueryElem) => {
const songs = await querySongs({
query: _query,
offset: 0,
@ -141,7 +130,7 @@ export function QueryWindowControlled(props: {
results: songs,
})
}
}
}, [query, setResultsForQuery]);
useEffect(() => {
if (query) {
@ -149,7 +138,7 @@ export function QueryWindowControlled(props: {
} else {
setResultsForQuery(null);
}
}, [query]);
}, [query, doQuery, setResultsForQuery]);
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box

@ -0,0 +1,139 @@
import React, { useReducer } from 'react';
import { WindowState } from "../Windows";
import { Box, Paper, Typography, TextField, Button } from "@material-ui/core";
import { useHistory } from 'react-router';
import { useAuth, Auth } from '../../../lib/useAuth';
import Alert from '@material-ui/lab/Alert';
import { Link } from 'react-router-dom';
export enum RegistrationStatus {
NoneSubmitted = 0,
Successful,
Unsuccessful,
}
export interface RegisterWindowState extends WindowState {
email: string,
password: string,
status: RegistrationStatus,
}
export enum RegisterWindowStateActions {
SetEmail = "SetEmail",
SetPassword = "SetPassword",
SetStatus = "SetStatus",
}
export function RegisterWindowReducer(state: RegisterWindowState, action: any) {
switch (action.type) {
case RegisterWindowStateActions.SetEmail:
return { ...state, email: action.value }
case RegisterWindowStateActions.SetPassword:
return { ...state, password: action.value }
case RegisterWindowStateActions.SetStatus:
return { ...state, status: action.value }
default:
throw new Error("Unimplemented RegisterWindow state update.")
}
}
export default function RegisterWindow(props: {}) {
const [state, dispatch] = useReducer(RegisterWindowReducer, {
email: "",
password: "",
status: RegistrationStatus.NoneSubmitted,
});
return <RegisterWindowControlled state={state} dispatch={dispatch} />
}
export function RegisterWindowControlled(props: {
state: RegisterWindowState,
dispatch: (action: any) => void,
}) {
let history: any = useHistory();
let auth: Auth = useAuth();
const onSubmit = (event: any) => {
event.preventDefault();
auth.signup(props.state.email, props.state.password)
.then(() => {
console.log("succes!")
props.dispatch({
type: RegisterWindowStateActions.SetStatus,
value: RegistrationStatus.Successful,
})
}).catch((e: any) => {
console.log("Fail!")
props.dispatch({
type: RegisterWindowStateActions.SetStatus,
value: RegistrationStatus.Unsuccessful,
})
})
}
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
mt={4}
width="500px"
>
<Paper>
<Box p={3}>
<Typography variant="h5">Sign up</Typography>
<form noValidate onSubmit={onSubmit}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email"
name="email"
autoFocus
onInput={(e: any) => props.dispatch({
type: RegisterWindowStateActions.SetEmail,
value: e.target.value
})}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="password"
label="Password"
name="password"
type="password"
onInput={(e: any) => props.dispatch({
type: RegisterWindowStateActions.SetPassword,
value: e.target.value
})}
/>
{props.state.status === RegistrationStatus.Successful && <Alert severity="success">
Registration successful! Please {<Link to="/login">sign in</Link>} to continue.
</Alert>
}
{props.state.status === RegistrationStatus.Unsuccessful && <Alert severity="error">
Registration failed - please check your inputs and try again.
</Alert>
}
{props.state.status !== RegistrationStatus.Successful && <Button
type="submit"
fullWidth
variant="outlined"
color="primary"
>Sign up</Button>}
<Box display="flex" alignItems="center" mt={2}>
<Typography>Already have an account?</Typography>
<Box flexGrow={1} ml={2}><Button
onClick={() => history.replace("/login")}
fullWidth
variant="outlined"
color="primary"
>Sign in</Button></Box>
</Box>
</form>
</Box>
</Paper>
</Box>
</Box>
}

@ -1,5 +1,5 @@
import React, { useEffect, useState, useReducer } from 'react';
import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core';
import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core';
import AudiotrackIcon from '@material-ui/icons/Audiotrack';
import PersonIcon from '@material-ui/icons/Person';
import AlbumIcon from '@material-ui/icons/Album';
@ -13,7 +13,6 @@ import SubmitChangesButton from '../../common/SubmitChangesButton';
import { saveSongChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { querySongs } from '../../../lib/backend/queries';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
export type SongMetadata = serverApi.SongDetails;
@ -44,11 +43,6 @@ export function SongWindowReducer(state: SongWindowState, action: any) {
}
}
export interface IProps {
state: SongWindowState,
dispatch: (action: any) => void,
}
export async function getSongMetadata(id: number) {
return (await querySongs({
query: {
@ -76,18 +70,18 @@ export function SongWindowControlled(props: {
state: SongWindowState,
dispatch: (action: any) => void,
}) {
let metadata = props.state.metadata;
let pendingChanges = props.state.pendingChanges;
let { pendingChanges, metadata, id: songId } = props.state;
let { dispatch } = props;
useEffect(() => {
getSongMetadata(props.state.id)
getSongMetadata(songId)
.then((m: SongMetadata) => {
props.dispatch({
dispatch({
type: SongWindowStateActions.SetMetadata,
value: m
});
})
}, [metadata?.title]);
}, [songId, dispatch]);
const [editingTitle, setEditingTitle] = useState<string | null>(null);
const title = <Typography variant="h4"><EditableText
@ -122,7 +116,7 @@ export function SongWindowControlled(props: {
const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => {
const store = whichStore(link);
return store && <a
href={link} target="_blank"
href={link} target="_blank" rel="noopener noreferrer"
>
<IconButton><StoreLinkIcon
whichStore={store}

@ -12,7 +12,6 @@ import { queryTags, querySongs } from '../../../lib/backend/queries';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
var _ = require('lodash');
export interface FullTagMetadata extends serverApi.TagDetails {
fullName: string[],
@ -78,7 +77,7 @@ export async function getTagMetadata(id: number) {
export default function TagWindow(props: {}) {
const { id } = useParams();
const [state, dispatch] = useReducer(TagWindowReducer,{
const [state, dispatch] = useReducer(TagWindowReducer, {
id: id,
metadata: null,
pendingChanges: null,
@ -95,38 +94,40 @@ export function TagWindowControlled(props: {
}) {
let metadata = props.state.metadata;
let pendingChanges = props.state.pendingChanges;
let { id: tagId, songsWithTag } = props.state;
let dispatch = props.dispatch;
// Effect to get the tag's metadata.
useEffect(() => {
getTagMetadata(props.state.id)
getTagMetadata(tagId)
.then((m: TagMetadata) => {
props.dispatch({
dispatch({
type: TagWindowStateActions.SetMetadata,
value: m
});
})
}, [metadata?.name]);
}, [tagId, dispatch]);
// Effect to get the tag's songs.
useEffect(() => {
if (props.state.songsWithTag) { return; }
if (songsWithTag) { return; }
(async () => {
const songs = await querySongs({
query: {
a: QueryLeafBy.TagId,
b: props.state.id,
b: tagId,
leafOp: QueryLeafOp.Equals,
},
offset: 0,
limit: -1,
});
props.dispatch({
dispatch({
type: TagWindowStateActions.SetSongs,
value: songs,
});
});
})();
}, [props.state.songsWithTag]);
}, [songsWithTag, tagId, dispatch]);
const [editingName, setEditingName] = useState<string | null>(null);
const name = <Typography variant="h4"><EditableText
@ -147,7 +148,7 @@ export function TagWindowControlled(props: {
/></Typography>
const fullName = <Box display="flex" alignItems="center">
{metadata?.fullName.map((n: string, i: number) => {
if (metadata?.fullName && i == metadata?.fullName.length - 1) {
if (metadata?.fullName && i === metadata?.fullName.length - 1) {
return name;
} else if (i >= (metadata?.fullName.length || 0) - 1) {
return undefined;
@ -160,7 +161,7 @@ export function TagWindowControlled(props: {
const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => {
const store = whichStore(link);
return store && <a
href={link} target="_blank"
href={link} target="_blank" rel="noopener noreferrer"
>
<IconButton><StoreLinkIcon
whichStore={store}

@ -104,11 +104,11 @@ export function addPlaceholders(
return newBlock;
}
} else if (isLeafElem(q) &&
q.leafOp != QueryLeafOp.Placeholder &&
q.leafOp !== QueryLeafOp.Placeholder &&
inNode !== null) {
return { operands: [q, makePlaceholder()], nodeOp: otherOp[inNode] };
} else if (isLeafElem(q) &&
q.leafOp != QueryLeafOp.Placeholder &&
q.leafOp !== QueryLeafOp.Placeholder &&
inNode === null) {
return {
operands: [
@ -127,7 +127,7 @@ export function removePlaceholders(q: QueryElem | null): QueryElem | null {
var newOperands: QueryElem[] = [];
q.operands.forEach((op: any) => {
if (isLeafElem(op) && op.leafOp == QueryLeafOp.Placeholder) {
if (isLeafElem(op) && op.leafOp === QueryLeafOp.Placeholder) {
return;
}
const newOp = removePlaceholders(op);
@ -136,14 +136,14 @@ export function removePlaceholders(q: QueryElem | null): QueryElem | null {
}
})
if (newOperands.length == 0) {
if (newOperands.length === 0) {
return null;
}
if (newOperands.length == 1) {
if (newOperands.length === 1) {
return newOperands[0];
}
return { operands: newOperands, nodeOp: q.nodeOp };
} else if (q && isLeafElem(q) && q.leafOp == QueryLeafOp.Placeholder) {
} else if (q && isLeafElem(q) && q.leafOp === QueryLeafOp.Placeholder) {
return null;
}

@ -0,0 +1,102 @@
// Note: Based on https://usehooks.com/useAuth/
import React, { useState, useContext, createContext, ReactFragment } from "react";
import PersonIcon from '@material-ui/icons/Person';
import * as serverApi from '../api';
export interface AuthUser {
id: number,
email: string,
icon: ReactFragment,
}
export interface Auth {
user: AuthUser | null,
signout: () => void,
signin: (email: string, password: string) => Promise<AuthUser>,
signup: (email: string, password: string) => Promise<void>,
};
const authContext = createContext<Auth>({
user: null,
signout: () => { },
signin: (email: string, password: string) => {
throw new Error("Auth object not initialized.");
},
signup: (email: string, password: string) => {
throw new Error("Auth object not initialized.");
},
});
export function ProvideAuth(props: { children: any }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{props.children}</authContext.Provider>;
}
export const useAuth = () => {
return useContext(authContext);
};
function useProvideAuth() {
const [user, setUser] = useState<AuthUser | null>(null);
// TODO: password maybe shouldn't be encoded into the URL.
const signin = (email: string, password: string) => {
return (async () => {
const urlBase = (process.env.REACT_APP_BACKEND || "") + serverApi.LoginEndpoint;
const url = `${urlBase}?username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`;
const response = await fetch(url, { method: "POST" });
const json = await response.json();
if (!("userId" in json)) {
throw new Error("No UserID received from login.");
}
const user = {
id: json.userId,
email: email,
icon: <PersonIcon />,
}
setUser(user);
return user;
})();
};
const signup = (email: string, password: string) => {
return (async () => {
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email,
password: password,
})
};
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.RegisterUserEndpoint, requestOpts)
if (!response.ok) {
throw new Error("Failed to register user.")
}
})();
};
const signout = () => {
return (async () => {
const url = (process.env.REACT_APP_BACKEND || "") + serverApi.LogoutEndpoint;
const response = await fetch(url, { method: "POST" });
if (!response.ok) {
throw new Error("Failed to log out.");
}
setUser(null);
})();
};
// Return the user object and auth methods
return {
user,
signin,
signup,
signout,
};
}

1446
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -8,6 +8,7 @@
"start": "npm run-script dev"
},
"dependencies": {
"concurrently": "^4.0.1"
"concurrently": "^4.0.1",
"express-session": "^1.17.1"
}
}

@ -1,36 +1,49 @@
#!/usr/bin/env python3
from gmusicapi import Mobileclient
import Mobileclient from gmusicapi
import argparse
import sys
import requests
import json
import urllib.parse
creds_path = sys.path[0] + '/mobileclient.cred'
creds_path=sys.path[0] + '/mobileclient.cred'
def authenticate(api):
creds = api.perform_oauth(storage_filepath=creds_path, open_browser=False)
def uploadLibrary(mudbase_api, songs):
def uploadLibrary(mudbase_api, mudbase_user, mudbase_password, songs):
# First, attempt to login and start a session.
s = requests.Session()
response = s.post(mudbase_api + '/login?username='
+ urllib.parse.quote(mudbase_user)
+ '&password='
+ urllib.parse.quote(mudbase_password))
if response.status_code != 200:
print("Unable to log in to MuDBase API.")
# Helpers
def getArtistStoreIds(song):
if 'artistId' in song:
return [ song['artistId'][0] ]
return [];
return [song['artistId'][0]]
return []
def getSongStoreIds(song):
if 'storeId' in song:
return [ song['storeId'] ]
return [song['storeId']]
return []
# Create GPM import tag
gpmTagIdResponse = requests.post(mudbase_api + '/tag', data = {
gpmTagIdResponse = s.post(mudbase_api + '/tag', data={
'name': 'GPM Import'
}).json()
gpmTagId = gpmTagIdResponse['id']
print(f"Created tag \"GPM Import\", response: {gpmTagIdResponse}")
# Create the root genre tag
genreRootResponse = requests.post(mudbase_api + '/tag', data = {
genreRootResponse = s.post(mudbase_api + '/tag', data={
'name': 'Genre'
}).json()
genreRootTagId = genreRootResponse['id']
@ -47,75 +60,80 @@ def uploadLibrary(mudbase_api, songs):
# Determine artist properties.
artist = {
'name': song['artist'],
'storeLinks': [ 'https://play.google.com/music/m' + id for id in getArtistStoreIds(song) ],
'tagIds': [ gpmTagId ]
'storeLinks': ['https://play.google.com/music/m' + id for id in getArtistStoreIds(song)],
'tagIds': [gpmTagId]
} if 'artist' in song else None
# Determine album properties.
album = {
'name': song['album'],
'tagIds': [ gpmTagId ]
'tagIds': [gpmTagId]
} if 'album' in song else None
# Determine genre properties.
genre = {
'name': song['genre'],
'parentId': genreRootTagId
'parentId': genreRootTagId
} if 'genre' in song else None
# Upload artist if not already done
artistId = None
if artist:
for key,value in storedArtists.items():
for key, value in storedArtists.items():
if value == artist:
artistId = key
break
if not artistId:
response = requests.post(mudbase_api + '/artist', json = artist).json()
response = s.post(mudbase_api + '/artist', json=artist).json()
artistId = response['id']
print(f"Created artist \"{artist['name']}\", response: {response}")
print(
f"Created artist \"{artist['name']}\", response: {response}")
storedArtists[artistId] = artist
# Upload album if not already done
albumId = None
if album:
for key,value in storedAlbums.items():
for key, value in storedAlbums.items():
if value == album:
albumId = key
break
if not albumId:
response = requests.post(mudbase_api + '/album', json = album).json()
response = s.post(mudbase_api + '/album', json=album).json()
albumId = response['id']
print(f"Created album \"{album['name']}\", response: {response}")
print(
f"Created album \"{album['name']}\", response: {response}")
storedAlbums[albumId] = album
# Upload genre if not already done
genreTagId = None
if genre:
for key,value in storedGenreTags.items():
for key, value in storedGenreTags.items():
if value == genre:
genreTagId = key
break
if not genreTagId:
response = requests.post(mudbase_api + '/tag', json = genre).json()
response = s.post(mudbase_api + '/tag', json=genre).json()
genreTagId = response['id']
print(f"Created genre tag \"Genre / {genre['name']}\", response: {response}")
print(
f"Created genre tag \"Genre / {genre['name']}\", response: {response}")
storedGenreTags[genreTagId] = genre
# Upload the song itself
tagIds = [ gpmTagId ]
tagIds = [gpmTagId]
if genreTagId:
tagIds.append(genreTagId)
_song = {
'title': song['title'],
'artistIds': [ artistId ] if artistId != None else [],
'albumIds': [ albumId ] if albumId != None else [],
'artistIds': [artistId] if artistId != None else [],
'albumIds': [albumId] if albumId != None else [],
'tagIds': tagIds,
'storeLinks': [ 'https://play.google.com/music/m/' + id for id in getSongStoreIds(song) ],
'storeLinks': ['https://play.google.com/music/m/' + id for id in getSongStoreIds(song)],
}
response = requests.post(mudbase_api + '/song', json = _song).json()
print(f"Created song \"{song['title']}\" with artist ID {artistId}, album ID {albumId}, response: {response}")
response = s.post(mudbase_api + '/song', json=_song).json()
print(
f"Created song \"{song['title']}\" with artist ID {artistId}, album ID {albumId}, response: {response}")
def getData(api):
return {
"songs": api.get_all_songs(),
@ -125,7 +143,7 @@ def getData(api):
def getSongs(data):
# Get songs from library
songs = [] #data['songs']
songs = [] # data['songs']
# Append songs from playlists
for playlist in data['playlists']:
@ -135,16 +153,27 @@ def getSongs(data):
# Uniquify by using a dict. After all, same song may appear in
# multiple playlists.
sI = lambda song: song['artist'] + '-' + song['title'] if 'artist' in song and 'title' in song else 'z'
def sI(song): return song['artist'] + '-' + \
song['title'] if 'artist' in song and 'title' in song else 'z'
return list(dict((sI(song), song) for song in songs).values())
api = Mobileclient()
parser = argparse.ArgumentParser(description="Import Google Music library into MudBase.")
parser.add_argument('--authenticate', help="Generate credentials for authentication", action="store_true")
parser.add_argument('--store-to', help="Store GPM library to JSON for later upload", action='store', dest='store_to')
parser.add_argument('--load-from', help="Load GPM library from JSON for upload", action='store', dest='load_from')
parser.add_argument('--mudbase_api', help="Address for the Mudbase back-end API to upload to", action='store', dest='mudbase_api')
parser = argparse.ArgumentParser(
description="Import Google Music library into MudBase.")
parser.add_argument(
'--authenticate', help="Generate credentials for authentication", action="store_true")
parser.add_argument('--store-to', help="Store GPM library to JSON for later upload",
action='store', dest='store_to')
parser.add_argument('--load-from', help="Load GPM library from JSON for upload",
action='store', dest='load_from')
parser.add_argument('--mudbase_api', help="Address for the Mudbase back-end API to upload to",
action='store', dest='mudbase_api')
parser.add_argument('--mudbase_user', help="Username for the Mudbase API",
action='store', dest="mudbase_user")
parser.add_argument('--mudbase_password', help="Password for the Mudbase API",
action='store', dest="mudbase_password")
args = parser.parse_args()
@ -155,7 +184,8 @@ data = None
# Determine whether we need to log in to GPM and get songs
if args.store_to or (not args.load_from and args.mudbase_api):
api.oauth_login(Mobileclient.FROM_MAC_ADDRESS, oauth_credentials=creds_path)
api.oauth_login(Mobileclient.FROM_MAC_ADDRESS,
oauth_credentials=creds_path)
data = getData(api)
# Determine whether to save to a file
@ -172,5 +202,7 @@ songs = getSongs(data)
print(f"Found {len(songs)} songs.")
if args.mudbase_api:
api.oauth_login(Mobileclient.FROM_MAC_ADDRESS, oauth_credentials=creds_path)
uploadLibrary(args.mudbase_api, songs)
api.oauth_login(Mobileclient.FROM_MAC_ADDRESS,
oauth_credentials=creds_path)
uploadLibrary(args.mudbase_api, args.mudbase_user,
args.mudbase_password, songs)

@ -2,33 +2,39 @@ const bodyParser = require('body-parser');
import * as api from '../client/src/api';
import Knex from 'knex';
import { CreateSongEndpointHandler } from './endpoints/CreateSongEndpointHandler';
import { CreateArtistEndpointHandler } from './endpoints/CreateArtistEndpointHandler';
import { QueryEndpointHandler } from './endpoints/QueryEndpointHandler';
import { ArtistDetailsEndpointHandler } from './endpoints/ArtistDetailsEndpointHandler'
import { SongDetailsEndpointHandler } from './endpoints/SongDetailsEndpointHandler';
import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtistEndpointHandler';
import { ModifySongEndpointHandler } from './endpoints/ModifySongEndpointHandler';
import { CreateTagEndpointHandler } from './endpoints/CreateTagEndpointHandler';
import { ModifyTagEndpointHandler } from './endpoints/ModifyTagEndpointHandler';
import { TagDetailsEndpointHandler } from './endpoints/TagDetailsEndpointHandler';
import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbumEndpointHandler';
import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbumEndpointHandler';
import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetailsEndpointHandler';
import { DeleteTagEndpointHandler } from './endpoints/DeleteTagEndpointHandler';
import { MergeTagEndpointHandler } from './endpoints/MergeTagEndpointHandler';
import { CreateSongEndpointHandler } from './endpoints/CreateSong';
import { CreateArtistEndpointHandler } from './endpoints/CreateArtist';
import { QueryEndpointHandler } from './endpoints/Query';
import { ArtistDetailsEndpointHandler } from './endpoints/ArtistDetails'
import { SongDetailsEndpointHandler } from './endpoints/SongDetails';
import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtist';
import { ModifySongEndpointHandler } from './endpoints/ModifySong';
import { CreateTagEndpointHandler } from './endpoints/CreateTag';
import { ModifyTagEndpointHandler } from './endpoints/ModifyTag';
import { TagDetailsEndpointHandler } from './endpoints/TagDetails';
import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbum';
import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbum';
import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetails';
import { DeleteTagEndpointHandler } from './endpoints/DeleteTag';
import { MergeTagEndpointHandler } from './endpoints/MergeTag';
import { RegisterUserEndpointHandler } from './endpoints/RegisterUser';
import * as endpointTypes from './endpoints/types';
import { sha512 } from 'js-sha512';
const invokeHandler = (handler:endpointTypes.EndpointHandler, knex: Knex) => {
// For authentication
var passport = require('passport');
var Strategy = require('passport-local').Strategy;
const invokeHandler = (handler: endpointTypes.EndpointHandler, knex: Knex) => {
return async (req: any, res: any) => {
console.log("Incoming", req.method, " @ ", req.url);
await handler(req, res, knex)
.catch(endpointTypes.catchUnhandledErrors)
.catch((_e:endpointTypes.EndpointError) => {
let e:endpointTypes.EndpointError = _e;
console.log("Error handling request: ", e.internalMessage);
res.sendStatus(e.httpStatus);
})
.catch(endpointTypes.catchUnhandledErrors)
.catch((_e: endpointTypes.EndpointError) => {
let e: endpointTypes.EndpointError = _e;
console.log("Error handling request: ", e.internalMessage);
res.sendStatus(e.httpStatus);
})
console.log("Finished handling", req.method, "@", req.url);
};
}
@ -37,26 +43,84 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => {
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const invokeWithKnex = (handler: endpointTypes.EndpointHandler) => {
// Set up auth. See: https://github.com/passport/express-4.x-local-example.git
passport.use(new Strategy(
function (email: string, password: string, cb: any) {
(async () => {
try {
const user = await knex.select(['email', 'passwordHash', 'id'])
.from('users')
.where({ 'email': email })
.then((users: any) => users[0]);
if (!user) { cb(null, false); }
if (sha512(password) != user.passwordHash) {
return cb(null, false);
}
return cb(null, user);
} catch (error) { cb(error); }
})();
}));
passport.serializeUser(function (user: any, cb: any) {
cb(null, user.id);
});
passport.deserializeUser(function (id: number, cb: any) {
(async () => {
try {
const user = await knex.select(['email', 'passwordHash', 'id'])
.from('users')
.where({ 'id': id })
.then((users: any) => users[0]);
if (!user) { cb(null, false); }
return cb(null, user);
} catch (error) { cb(error); }
})();
});
app.use(require('express-session')({ secret: 'EA9q5cukt7UFhN', resave: false, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());
const _invoke = (handler: endpointTypes.EndpointHandler) => {
return invokeHandler(handler, knex);
}
const checkLogin = () => {
return function (req: any, res: any, next: any) {
if (!req.isAuthenticated || !req.isAuthenticated()) {
return res
.status(401)
.json({ reason: "NotLoggedIn" })
.send();
}
next();
}
}
// Set up REST API endpoints
app.post(apiBaseUrl + api.CreateSongEndpoint, invokeWithKnex(CreateSongEndpointHandler));
app.post(apiBaseUrl + api.QueryEndpoint, invokeWithKnex(QueryEndpointHandler));
app.post(apiBaseUrl + api.CreateArtistEndpoint, invokeWithKnex(CreateArtistEndpointHandler));
app.put(apiBaseUrl + api.ModifyArtistEndpoint, invokeWithKnex(ModifyArtistEndpointHandler));
app.put(apiBaseUrl + api.ModifySongEndpoint, invokeWithKnex(ModifySongEndpointHandler));
app.get(apiBaseUrl + api.SongDetailsEndpoint, invokeWithKnex(SongDetailsEndpointHandler));
app.get(apiBaseUrl + api.ArtistDetailsEndpoint, invokeWithKnex(ArtistDetailsEndpointHandler));
app.post(apiBaseUrl + api.CreateTagEndpoint, invokeWithKnex(CreateTagEndpointHandler));
app.put(apiBaseUrl + api.ModifyTagEndpoint, invokeWithKnex(ModifyTagEndpointHandler));
app.get(apiBaseUrl + api.TagDetailsEndpoint, invokeWithKnex(TagDetailsEndpointHandler));
app.post(apiBaseUrl + api.CreateAlbumEndpoint, invokeWithKnex(CreateAlbumEndpointHandler));
app.put(apiBaseUrl + api.ModifyAlbumEndpoint, invokeWithKnex(ModifyAlbumEndpointHandler));
app.get(apiBaseUrl + api.AlbumDetailsEndpoint, invokeWithKnex(AlbumDetailsEndpointHandler));
app.delete(apiBaseUrl + api.DeleteTagEndpoint, invokeWithKnex(DeleteTagEndpointHandler));
app.post(apiBaseUrl + api.MergeTagEndpoint, invokeWithKnex(MergeTagEndpointHandler));
app.post(apiBaseUrl + api.CreateSongEndpoint, checkLogin(), _invoke(CreateSongEndpointHandler));
app.post(apiBaseUrl + api.QueryEndpoint, checkLogin(), _invoke(QueryEndpointHandler));
app.post(apiBaseUrl + api.CreateArtistEndpoint, checkLogin(), _invoke(CreateArtistEndpointHandler));
app.put(apiBaseUrl + api.ModifyArtistEndpoint, checkLogin(), _invoke(ModifyArtistEndpointHandler));
app.put(apiBaseUrl + api.ModifySongEndpoint, checkLogin(), _invoke(ModifySongEndpointHandler));
app.get(apiBaseUrl + api.SongDetailsEndpoint, checkLogin(), _invoke(SongDetailsEndpointHandler));
app.get(apiBaseUrl + api.ArtistDetailsEndpoint, checkLogin(), _invoke(ArtistDetailsEndpointHandler));
app.post(apiBaseUrl + api.CreateTagEndpoint, checkLogin(), _invoke(CreateTagEndpointHandler));
app.put(apiBaseUrl + api.ModifyTagEndpoint, checkLogin(), _invoke(ModifyTagEndpointHandler));
app.get(apiBaseUrl + api.TagDetailsEndpoint, checkLogin(), _invoke(TagDetailsEndpointHandler));
app.post(apiBaseUrl + api.CreateAlbumEndpoint, checkLogin(), _invoke(CreateAlbumEndpointHandler));
app.put(apiBaseUrl + api.ModifyAlbumEndpoint, checkLogin(), _invoke(ModifyAlbumEndpointHandler));
app.get(apiBaseUrl + api.AlbumDetailsEndpoint, checkLogin(), _invoke(AlbumDetailsEndpointHandler));
app.delete(apiBaseUrl + api.DeleteTagEndpoint, checkLogin(), _invoke(DeleteTagEndpointHandler));
app.post(apiBaseUrl + api.MergeTagEndpoint, checkLogin(), _invoke(MergeTagEndpointHandler));
app.post(apiBaseUrl + api.RegisterUserEndpoint, _invoke(RegisterUserEndpointHandler));
app.post('/login', passport.authenticate('local'), (req: any, res: any) => {
res.status(200).send({ userId: req.user.id });
});
app.post('/logout', function (req: any, res: any) {
req.logout();
res.status(200).send();
});
}
export { SetupApp }

@ -12,6 +12,8 @@ export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res
throw e;
}
const { id: userId } = req.user;
try {
// Start transfers for songs, tags and artists.
// Also request the album itself.
@ -35,6 +37,7 @@ export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res
});
const albumPromise = knex.select('name', 'storeLinks')
.from('albums')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((albums: any) => albums[0]);
@ -43,17 +46,19 @@ export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res
await Promise.all([albumPromise, tagIdsPromise, songIdsPromise, artistIdsPromise]);
// Respond to the request.
console.log("ALBUM: ", album);
const response: api.AlbumDetailsResponse = {
name: album['name'],
artistIds: artists,
tagIds: tags,
songIds: songs,
storeLinks: asJson(album['storeLinks']),
};
await res.send(response);
if (album) {
const response: api.AlbumDetailsResponse = {
name: album['name'],
artistIds: artists,
tagIds: tags,
songIds: songs,
storeLinks: asJson(album['storeLinks']),
};
await res.send(response);
} else {
await res.status(404).send({});
}
} catch (e) {
catchUnhandledErrors(e);
}
catchUnhandledErrors(e);
}
}

@ -12,6 +12,8 @@ export const ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, re
throw e;
}
const { id: userId } = req.user;
try {
const tagIds = Array.from(new Set((await knex.select('tagId')
.from('artists_tags')
@ -20,15 +22,19 @@ export const ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, re
const results = await knex.select(['id', 'name', 'storeLinks'])
.from('artists')
.where({ 'user': userId })
.where({ 'id': req.params.id });
const response: api.ArtistDetailsResponse = {
name: results[0].name,
tagIds: tagIds,
storeLinks: asJson(results[0].storeLinks),
if (results[0]) {
const response: api.ArtistDetailsResponse = {
name: results[0].name,
tagIds: tagIds,
storeLinks: asJson(results[0].storeLinks),
}
await res.send(response);
} else {
await res.status(404).send({});
}
await res.send(response);
} catch (e) {
catchUnhandledErrors(e)
}

@ -11,8 +11,9 @@ export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res:
throw e;
}
const reqObject: api.CreateAlbumRequest = req.body;
const { id: userId } = req.user;
console.log("Create Album:", reqObject);
console.log("User ", userId, ": Create Album ", reqObject);
await knex.transaction(async (trx) => {
try {
@ -20,6 +21,7 @@ export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res:
const artistIdsPromise = reqObject.artistIds ?
trx.select('id')
.from('artists')
.where({ 'user': userId })
.whereIn('id', reqObject.artistIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
@ -28,6 +30,7 @@ export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res:
const tagIdsPromise = reqObject.tagIds ?
trx.select('id')
.from('tags')
.where({ 'user': userId })
.whereIn('id', reqObject.tagIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
@ -50,6 +53,7 @@ export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res:
.insert({
name: reqObject.name,
storeLinks: JSON.stringify(reqObject.storeLinks || []),
user: userId,
})
.returning('id') // Needed for Postgres
)[0];

@ -11,8 +11,9 @@ export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res
throw e;
}
const reqObject: api.CreateArtistRequest = req.body;
const { id: userId } = req.user;
console.log("Create artist:", reqObject)
console.log("User ", userId, ": Create artist ", reqObject)
await knex.transaction(async (trx) => {
try {
@ -20,13 +21,12 @@ export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res
const tags: number[] = reqObject.tagIds ?
Array.from(new Set(
(await trx.select('id').from('tags')
.where({ 'user': userId })
.whereIn('id', reqObject.tagIds))
.map((tag: any) => tag['id'])
))
: [];
console.log("Found artist tags:", tags)
if (reqObject.tagIds && tags && tags.length !== reqObject.tagIds.length) {
const e: EndpointError = {
internalMessage: 'Not all tags exist for CreateArtist request: ' + JSON.stringify(req.body),
@ -40,6 +40,7 @@ export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res
.insert({
name: reqObject.name,
storeLinks: JSON.stringify(reqObject.storeLinks || []),
user: userId,
})
.returning('id') // Needed for Postgres
)[0];

@ -11,8 +11,9 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res:
throw e;
}
const reqObject: api.CreateSongRequest = req.body;
const { id: userId } = req.user;
console.log("Create Song:", reqObject);
console.log("User ", userId, ": Create Song ", reqObject);
await knex.transaction(async (trx) => {
try {
@ -20,6 +21,7 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res:
const artistIdsPromise = reqObject.artistIds ?
trx.select('id')
.from('artists')
.where({ 'user': userId })
.whereIn('id', reqObject.artistIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
@ -28,6 +30,7 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res:
const tagIdsPromise = reqObject.tagIds ?
trx.select('id')
.from('tags')
.where({ 'user': userId })
.whereIn('id', reqObject.tagIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
@ -36,6 +39,7 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res:
const albumIdsPromise = reqObject.albumIds ?
trx.select('id')
.from('albums')
.where({ 'user': userId })
.whereIn('id', reqObject.albumIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
@ -59,6 +63,7 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res:
.insert({
title: reqObject.title,
storeLinks: JSON.stringify(reqObject.storeLinks || []),
user: userId,
})
.returning('id') // Needed for Postgres
)[0];

@ -11,8 +11,9 @@ export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: a
throw e;
}
const reqObject: api.CreateTagRequest = req.body;
const { id: userId } = req.user;
console.log("Create Tag: ", reqObject);
console.log("User ", userId, ": Create Tag ", reqObject);
await knex.transaction(async (trx) => {
try {
@ -21,6 +22,7 @@ export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: a
reqObject.parentId ?
(await trx.select('id')
.from('tags')
.where({ 'user': userId })
.where({ 'id': reqObject.parentId }))[0]['id'] :
undefined;
@ -35,7 +37,8 @@ export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: a
// Create the new tag.
var tag: any = {
name: reqObject.name
name: reqObject.name,
user: userId,
};
if (maybeParent) {
tag['parentId'] = maybeParent;

@ -2,13 +2,14 @@ import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
async function getChildrenRecursive(id: number, trx: any) {
async function getChildrenRecursive(id: number, userId: number, trx: any) {
const directChildren = (await trx.select('id')
.from('tags')
.where({ 'user': userId })
.where({ 'parentId': id })).map((r: any) => r.id);
const indirectChildrenPromises = directChildren.map(
(child: number) => getChildrenRecursive(child, trx)
(child: number) => getChildrenRecursive(child, userId, trx)
);
const indirectChildrenNested = await Promise.all(indirectChildrenPromises);
const indirectChildren = indirectChildrenNested.flat();
@ -28,19 +29,20 @@ export const DeleteTagEndpointHandler: EndpointHandler = async (req: any, res: a
throw e;
}
const reqObject: api.DeleteTagRequest = req.body;
const { id: userId } = req.user;
console.log("Delete Tag:", reqObject);
console.log("User ", userId, ": Delete Tag ", reqObject);
await knex.transaction(async (trx) => {
try {
// Start retrieving any child tags.
const childTagsPromise =
getChildrenRecursive(req.params.id, trx);
getChildrenRecursive(req.params.id, userId, trx);
// Start retrieving the tag itself.
const tagPromise = trx.select('id')
.from('tags')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
@ -49,7 +51,6 @@ export const DeleteTagEndpointHandler: EndpointHandler = async (req: any, res: a
// Merge all IDs.
const toDelete = [ tag, ...children ];
console.log ("deleting tags: ", toDelete);
// Check that we found all objects we need.
if (!tag) {
@ -62,6 +63,7 @@ export const DeleteTagEndpointHandler: EndpointHandler = async (req: any, res: a
// Delete the tag and its children.
await trx('tags')
.where({ 'user': userId })
.whereIn('id', toDelete)
.del();

@ -11,8 +11,9 @@ export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: an
throw e;
}
const reqObject: api.DeleteTagRequest = req.body;
const { id: userId } = req.user;
console.log("Merge Tag:", reqObject);
console.log("User ", userId, ": Merge Tag ", reqObject);
const fromId = req.params.id;
const toId = req.params.toId;
@ -21,12 +22,14 @@ export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: an
// Start retrieving the "from" tag.
const fromTagPromise = trx.select('id')
.from('tags')
.where({ 'user': userId })
.where({ id: fromId })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
// Start retrieving the "to" tag.
const toTagPromise = trx.select('id')
.from('tags')
.where({ 'user': userId })
.where({ id: toId })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
@ -44,6 +47,7 @@ export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: an
// Assign new tag ID to any objects referencing the to-be-merged tag.
const cPromise = trx('tags')
.where({ 'user': userId })
.where({ 'parentId': fromId })
.update({ 'parentId': toId });
const sPromise = trx('songs_tags')
@ -59,6 +63,7 @@ export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: an
// Delete the original tag.
await trx('tags')
.where({ 'user': userId })
.where({ 'id': fromId })
.del();

@ -11,8 +11,9 @@ export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res:
throw e;
}
const reqObject: api.ModifyAlbumRequest = req.body;
const { id: userId } = req.user;
console.log("Modify Album:", reqObject);
console.log("User ", userId, ": Modify Album ", reqObject);
await knex.transaction(async (trx) => {
try {
@ -20,6 +21,7 @@ export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res:
// Start retrieving the album itself.
const albumPromise = trx.select('id')
.from('albums')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
@ -58,6 +60,7 @@ export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res:
if ("name" in reqObject) { update["name"] = reqObject.name; }
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); }
const modifyAlbumPromise = trx('albums')
.where({ 'user': userId })
.where({ 'id': req.params.id })
.update(update)

@ -11,7 +11,9 @@ export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res
throw e;
}
const reqObject: api.ModifyArtistRequest = req.body;
console.log("Modify Artist:", reqObject);
const { id: userId } = req.user;
console.log("User ", userId, ": Modify Artist ", reqObject);
await knex.transaction(async (trx) => {
try {
@ -20,6 +22,7 @@ export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res
// Start retrieving the artist itself.
const artistPromise = trx.select('id')
.from('artists')
.where({ 'user': userId })
.where({ id: artistId })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
@ -49,6 +52,7 @@ export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res
if ("name" in reqObject) { update["name"] = reqObject.name; }
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); }
const modifyArtistPromise = trx('artists')
.where({ 'user': userId })
.where({ 'id': artistId })
.update(update)

@ -11,14 +11,16 @@ export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res:
throw e;
}
const reqObject: api.ModifySongRequest = req.body;
const { id: userId } = req.user;
console.log("Modify Song:", reqObject);
console.log("User ", userId, ": Modify Song ", reqObject);
await knex.transaction(async (trx) => {
try {
// Retrieve the song to be modified itself.
const songPromise = trx.select('id')
.from('songs')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
@ -67,6 +69,7 @@ export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res:
if ("title" in reqObject) { update["title"] = reqObject.title; }
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); }
const modifySongPromise = trx('songs')
.where({ 'user': userId })
.where({ 'id': req.params.id })
.update(update)

@ -11,8 +11,9 @@ export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: a
throw e;
}
const reqObject: api.ModifyTagRequest = req.body;
const { id: userId } = req.user;
console.log("Modify Tag:", reqObject);
console.log("User ", userId, ": Modify Tag ", reqObject);
await knex.transaction(async (trx) => {
try {
@ -20,6 +21,7 @@ export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: a
const parentTagPromise = reqObject.parentId ?
trx.select('id')
.from('tags')
.where({ 'user': userId })
.where({ 'id': reqObject.parentId })
.then((ts: any) => ts.map((t: any) => t['tagId'])) :
(async () => { return [] })();
@ -27,6 +29,7 @@ export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: a
// Start retrieving the tag itself.
const tagPromise = trx.select('id')
.from('tags')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
@ -45,6 +48,7 @@ export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: a
// Modify the tag.
await trx('tags')
.where({ 'user': userId })
.where({ 'id': req.params.id })
.update({
name: reqObject.name,

@ -178,7 +178,7 @@ const objectColumns = {
[ObjectType.Tag]: ['tags.id as tags.id', 'tags.name as tags.name', 'tags.parentId as tags.parentId']
};
function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryElem, ordering: api.Ordering,
function constructQuery(knex: Knex, userId: number, queryFor: ObjectType, queryElem: api.QueryElem, ordering: api.Ordering,
offset: number, limit: number | null) {
const joinObjects = getRequiredDatabaseObjects(queryElem);
joinObjects.delete(queryFor); // We are already querying this object in the base query.
@ -194,6 +194,7 @@ function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryEl
// First, we create a base query for the type of object we need to yield.
var q = knex.select(columns)
.where({ [objectTables[queryFor] + '.user']: userId })
.groupBy(objectTables[queryFor] + '.' + 'id')
.from(objectTables[queryFor]);
@ -223,7 +224,7 @@ function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryEl
return q;
}
async function getLinkedObjects(knex: Knex, base: ObjectType, linked: ObjectType, baseIds: number[]) {
async function getLinkedObjects(knex: Knex, userId: number, base: ObjectType, linked: ObjectType, baseIds: number[]) {
var result: Record<number, any[]> = {};
const otherTable = objectTables[linked];
const linkingTable = getLinkingTable(base, linked);
@ -232,6 +233,7 @@ async function getLinkedObjects(knex: Knex, base: ObjectType, linked: ObjectType
await Promise.all(baseIds.map((baseId: number) => {
return knex.select(columns).groupBy(otherTable + '.id').from(otherTable)
.join(linkingTable, { [linkingTable + '.' + linkingTableIdNames[linked]]: otherTable + '.id' })
.where({ [otherTable + '.user']: userId })
.where({ [linkingTable + '.' + linkingTableIdNames[base]]: baseId })
.then((others: any) => { result[baseId] = others; })
}))
@ -241,11 +243,12 @@ async function getLinkedObjects(knex: Knex, base: ObjectType, linked: ObjectType
}
// Resolve a tag into the full nested structure of its ancestors.
async function getFullTag(knex: Knex, tag: any): Promise<any> {
async function getFullTag(knex: Knex, userId: number, tag: any): Promise<any> {
const resolveTag = async (t: any) => {
if (t['tags.parentId']) {
const parent = (await knex.select(objectColumns[ObjectType.Tag])
.from('tags')
.where({ 'user': userId })
.where({ [objectTables[ObjectType.Tag] + '.id']: t['tags.parentId'] }))[0];
t.parent = await resolveTag(parent);
}
@ -264,7 +267,9 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
throw e;
}
const reqObject: api.QueryRequest = req.body;
console.log("Query: ", reqObject);
const { id: userId } = req.user;
console.log("User ", userId, ": Query ", reqObject);
try {
const songLimit = reqObject.offsetsLimits.songLimit;
@ -278,6 +283,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
const artistsPromise: Promise<any> = (artistLimit && artistLimit !== 0) ?
constructQuery(knex,
userId,
ObjectType.Artist,
reqObject.query,
reqObject.ordering,
@ -288,6 +294,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
const albumsPromise: Promise<any> = (albumLimit && albumLimit !== 0) ?
constructQuery(knex,
userId,
ObjectType.Album,
reqObject.query,
reqObject.ordering,
@ -298,6 +305,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
const songsPromise: Promise<any> = (songLimit && songLimit !== 0) ?
constructQuery(knex,
userId,
ObjectType.Song,
reqObject.query,
reqObject.ordering,
@ -308,6 +316,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
const tagsPromise: Promise<any> = (tagLimit && tagLimit !== 0) ?
constructQuery(knex,
userId,
ObjectType.Tag,
reqObject.query,
reqObject.ordering,
@ -325,18 +334,18 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
})();
const songsArtistsPromise: Promise<Record<number, any[]>> = (songLimit && songLimit !== 0) ?
(async () => {
return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Artist, await songIdsPromise);
return await getLinkedObjects(knex, userId, ObjectType.Song, ObjectType.Artist, await songIdsPromise);
})() :
(async () => { return {}; })();
const songsTagsPromise: Promise<Record<number, any[]>> = (songLimit && songLimit !== 0) ?
(async () => {
const tagsPerSong: Record<number, any> = await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise);
const tagsPerSong: Record<number, any> = await getLinkedObjects(knex, userId, ObjectType.Song, ObjectType.Tag, await songIdsPromise);
var result: Record<number, any> = {};
for (var key in tagsPerSong) {
const tags = tagsPerSong[key];
var fullTags: any[] = [];
for (var idx in tags) {
fullTags.push(await getFullTag(knex, tags[idx]));
fullTags.push(await getFullTag(knex, userId, tags[idx]));
}
result[key] = fullTags;
}
@ -345,7 +354,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
(async () => { return {}; })();
const songsAlbumsPromise: Promise<Record<number, any[]>> = (songLimit && songLimit !== 0) ?
(async () => {
return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Album, await songIdsPromise);
return await getLinkedObjects(knex, userId, ObjectType.Song, ObjectType.Album, await songIdsPromise);
})() :
(async () => { return {}; })();

@ -0,0 +1,49 @@
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
import { sha512 } from 'js-sha512';
export const RegisterUserEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkRegisterUserRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid RegisterUser request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.RegisterUserRequest = req.body;
console.log("Register User: ", reqObject);
await knex.transaction(async (trx) => {
try {
// check if the user already exists
const user = (await trx
.select('id')
.from('users')
.where({ email: reqObject.email }))[0];
if(user) {
res.status(400).send();
return;
}
// Create the new user.
const passwordHash = sha512(reqObject.password);
const userId = (await trx('users')
.insert({
email: reqObject.email,
passwordHash: passwordHash,
})
.returning('id') // Needed for Postgres
)[0];
// Respond to the request.
res.status(200).send();
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
})
}

@ -12,6 +12,8 @@ export const SongDetailsEndpointHandler: EndpointHandler = async (req: any, res:
throw e;
}
const { id: userId } = req.user;
try {
const tagIdsPromise: Promise<number[]> = knex.select('tagId')
.from('songs_tags')
@ -41,21 +43,25 @@ export const SongDetailsEndpointHandler: EndpointHandler = async (req: any, res:
})
const songPromise = await knex.select(['id', 'title', 'storeLinks'])
.from('songs')
.where({ 'user': userId })
.where({ 'id': req.params.id })
.then((ss: any) => ss[0])
const [tags, albums, artists, song] =
await Promise.all([tagIdsPromise, albumIdsPromise, artistIdsPromise, songPromise]);
const response: api.SongDetailsResponse = {
title: song.title,
tagIds: tags,
artistIds: artists,
albumIds: albums,
storeLinks: asJson(song.storeLinks),
if (song) {
const response: api.SongDetailsResponse = {
title: song.title,
tagIds: tags,
artistIds: artists,
albumIds: albums,
storeLinks: asJson(song.storeLinks),
}
await res.send(response);
} else {
await res.status(404).send({});
}
await res.send(response);
} catch (e) {
catchUnhandledErrors(e)
}

@ -11,17 +11,23 @@ export const TagDetailsEndpointHandler: EndpointHandler = async (req: any, res:
throw e;
}
const { id: userId } = req.user;
try {
const results = await knex.select(['id', 'name', 'parentId'])
.from('tags')
.where({ 'user': userId })
.where({ 'id': req.params.id });
const response: api.TagDetailsResponse = {
name: results[0].name,
parentId: results[0].parentId,
if (results[0]) {
const response: api.TagDetailsResponse = {
name: results[0].name,
parentId: results[0].parentId || undefined,
}
await res.send(response);
} else {
await res.status(404).send({});
}
await res.send(response);
} catch (e) {
catchUnhandledErrors(e)
}

@ -0,0 +1,73 @@
import * as Knex from "knex";
import { sha512 } from "js-sha512";
export async function up(knex: Knex): Promise<void> {
// Users table.
await knex.schema.createTable(
'users',
(table: any) => {
table.increments('id');
table.string('email');
table.string('passwordHash')
}
)
// Add user column to other object tables.
await knex.schema.alterTable(
'songs',
(table: any) => {
table.integer('user').unsigned().notNullable().defaultTo(1);
}
)
await knex.schema.alterTable(
'albums',
(table: any) => {
table.integer('user').unsigned().notNullable().defaultTo(1);
}
)
await knex.schema.alterTable(
'tags',
(table: any) => {
table.integer('user').unsigned().notNullable().defaultTo(1);
}
)
await knex.schema.alterTable(
'artists',
(table: any) => {
table.integer('user').unsigned().notNullable().defaultTo(1);
}
)
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable('users');
// Remove the user column
await knex.schema.alterTable(
'songs',
(table: any) => {
table.dropColumn('user');
}
)
await knex.schema.alterTable(
'albums',
(table: any) => {
table.dropColumn('user');
}
)
await knex.schema.alterTable(
'tags',
(table: any) => {
table.dropColumn('user');
}
)
await knex.schema.alterTable(
'artists',
(table: any) => {
table.dropColumn('user');
}
)
}

@ -1936,6 +1936,11 @@
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA=="
},
"js-sha512": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz",
"integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ=="
},
"jsbi": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.3.tgz",
@ -2756,6 +2761,28 @@
"resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
"integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ="
},
"passport": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz",
"integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==",
"requires": {
"passport-strategy": "1.x.x",
"pause": "0.0.1"
}
},
"passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
"integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -2789,6 +2816,11 @@
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA="
},
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",

@ -13,12 +13,15 @@
"chai-http": "^4.3.0",
"express": "^4.16.4",
"jasmine": "^3.5.0",
"js-sha512": "^0.8.0",
"knex": "^0.21.5",
"mssql": "^6.2.1",
"mysql": "^2.18.1",
"mysql2": "^2.1.0",
"nodemon": "^2.0.4",
"oracledb": "^5.0.0",
"passport": "^0.4.1",
"passport-local": "^1.0.0",
"pg": "^8.3.3",
"sqlite3": "^5.0.0",
"ts-node": "^8.10.2",

@ -4,92 +4,100 @@ const express = require('express');
import { SetupApp } from '../../../app';
import { expect } from 'chai';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app, await helpers.initTestDB(), '');
return app;
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /album with no name', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.post('/album')
.send({})
.then((res) => {
expect(res).to.have.status(400);
done();
});
});
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createAlbum(req, {}, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /album with a correct request', () => {
it('should succeed', done => {
init().then((app) => {
chai
.request(app)
.post('/album')
.send({
name: "MyAlbum"
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
id: 1
});
done();
});
});
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /album on nonexistent album', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.put('/album/1')
.send({
id: 1,
name: "NewAlbumName"
})
.then((res) => {
expect(res).to.have.status(400);
done();
})
})
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.modifyAlbum(req, 1, { id: 1, name: "NewAlbumName" }, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /album with an existing album', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 })
.then(() => helpers.modifyAlbum(req, 1, { name: "MyNewAlbum" }, 200))
.then(() => helpers.checkAlbum(req, 1, 200, { name: "MyNewAlbum", storeLinks: [], tagIds: [], songIds: [], artistIds: [] }))
.then(req.close)
.then(done);
});
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 });
await helpers.modifyAlbum(req, 1, { name: "MyNewAlbum" }, 200);
await helpers.checkAlbum(req, 1, 200, { name: "MyNewAlbum", storeLinks: [], tagIds: [], songIds: [], artistIds: [] });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /album with tags', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createTag(req, { name: "Root" }, 200, { id: 1 })
.then(() => helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 }))
.then(() => helpers.createAlbum(req, { name: "MyAlbum", tagIds: [ 1, 2 ] }, 200, { id: 1 }))
.then(() => helpers.checkAlbum(req, 1, 200, { name: "MyAlbum", storeLinks: [], tagIds: [ 1, 2 ], songIds: [], artistIds: [] }))
.then(req.close)
.then(done);
});
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "Root" }, 200, { id: 1 })
await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 })
await helpers.createAlbum(req, { name: "MyAlbum", tagIds: [1, 2] }, 200, { id: 1 })
await helpers.checkAlbum(req, 1, 200, { name: "MyAlbum", storeLinks: [], tagIds: [1, 2], songIds: [], artistIds: [] })
} finally {
req.close();
agent.close();
done();
}
});
});

@ -2,85 +2,101 @@ const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { SetupApp } from '../../../app';
import { expect } from 'chai';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app, await helpers.initTestDB(), '');
return app;
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /artist with no name', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.post('/artist')
.send({})
.then((res) => {
expect(res).to.have.status(400);
done();
});
});
it('should fail', async done => {
let agent = await init();
var req = agent.keepOpen();
try {
await helpers.createArtist(req, {}, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /artist with a correct request', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 })
.then(() => helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [] }))
.then(req.close)
.then(done);
});
it('should succeed', async done => {
let agent = await init();
var req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 });
await helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [] });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /artist on nonexistent artist', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.put('/artist/0')
.send({
id: 0,
name: "NewArtistName"
})
.then((res) => {
expect(res).to.have.status(400);
done();
})
})
it('should fail', async done => {
let agent = await init();
var req = agent.keepOpen();
try {
await helpers.modifyArtist(req, 0, { id: 0, name: "NewArtistName" }, 400)
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /artist with an existing artist', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 })
.then(() => helpers.modifyArtist(req, 1, { name: "MyNewArtist" }, 200))
.then(() => helpers.checkArtist(req, 1, 200, { name: "MyNewArtist", storeLinks: [], tagIds: [] }))
.then(req.close)
.then(done);
});
it('should succeed', async done => {
let agent = await init();
var req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 });
await helpers.modifyArtist(req, 1, { name: "MyNewArtist" }, 200);
await helpers.checkArtist(req, 1, 200, { name: "MyNewArtist", storeLinks: [], tagIds: [] });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /artist with tags', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createTag(req, { name: "Root" }, 200, { id: 1 })
.then(() => helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 }))
.then(() => helpers.createArtist(req, { name: "MyArtist", tagIds: [ 1, 2 ] }, 200, { id: 1 }))
.then(() => helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [ 1, 2 ] }))
.then(req.close)
.then(done);
});
it('should succeed', async done => {
let agent = await init();
var req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "Root" }, 200, { id: 1 });
await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 });
await helpers.createArtist(req, { name: "MyArtist", tagIds: [1, 2] }, 200, { id: 1 });
await helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [1, 2] });
} finally {
req.close();
agent.close();
done();
}
});
});

@ -0,0 +1,145 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { SetupApp } from '../../../app';
import * as helpers from './helpers';
async function init() {
chai.use(chaiHttp);
const app = express();
const knex = await helpers.initTestDB();
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
return agent;
}
describe('Auth registration password and email constraints', () => {
it('are enforced', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone", "password1A!", 400); //no valid email
await helpers.createUser(req, "someone@email.com", "password1A", 400); //no special char
await helpers.createUser(req, "someone@email.com", "password1!", 400); //no capital letter
await helpers.createUser(req, "someone@email.com", "passwordA!", 400); //no number
await helpers.createUser(req, "someone@email.com", "Ϭassword1A!", 400); //non-ASCII in password
await helpers.createUser(req, "Ϭomeone@email.com", "password1A!", 400); //non-ASCII in email
await helpers.createUser(req, "someone@email.com", "pass1A!", 400); //password too short
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
} finally {
req.close();
done();
}
});
});
describe('Attempting to register an already registered user', () => {
it('should fail', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.createUser(req, "someone@email.com", "password1A!", 400);
} finally {
req.close();
done();
}
});
});
describe('Auth login access for users', () => {
it('is correctly enforced', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200);
await helpers.login(req, "someone@email.com", "password2B!", 401);
await helpers.login(req, "someoneelse@other.com", "password1A!", 401);
await helpers.login(req, "someone@email.com", "password1A!", 200);
await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
} finally {
req.close();
done();
}
});
});
describe('Auth access to objects', () => {
it('is only possible when logged in', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.login(req, "someone@email.com", "password1A!", 200);
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 });
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} );
await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 });
await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 });
await helpers.checkTag(req, 1, 200);
await helpers.checkAlbum(req, 1, 200);
await helpers.checkArtist(req, 1, 200);
await helpers.checkSong(req, 1, 200);
await helpers.logout(req, 200);
await helpers.checkTag(req, 1, 401);
await helpers.checkAlbum(req, 1, 401);
await helpers.checkArtist(req, 1, 401);
await helpers.checkSong(req, 1, 401);
} finally {
req.close();
done();
}
});
});
describe('Auth access to user objects', () => {
it('is restricted to each user', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200);
await helpers.login(req, "someone@email.com", "password1A!", 200);
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 });
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} );
await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 });
await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 });
await helpers.logout(req, 200);
await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
await helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 });
await helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 } );
await helpers.createAlbum(req, { name: "Album2" }, 200, { id: 2 });
await helpers.createSong(req, { title: "Song2" }, 200, { id: 2 });
await helpers.logout(req, 200);
await helpers.login(req, "someone@email.com", "password1A!", 200);
await helpers.checkTag(req, 2, 404);
await helpers.checkAlbum(req, 2, 404);
await helpers.checkArtist(req, 2, 404);
await helpers.checkSong(req, 2, 404);
await helpers.checkTag(req, 1, 200);
await helpers.checkAlbum(req, 1, 200);
await helpers.checkArtist(req, 1, 200);
await helpers.checkSong(req, 1, 200);
await helpers.logout(req, 200);
await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
await helpers.checkTag(req, 1, 404);
await helpers.checkAlbum(req, 1, 404);
await helpers.checkArtist(req, 1, 404);
await helpers.checkSong(req, 1, 404);
await helpers.checkTag(req, 2, 200);
await helpers.checkAlbum(req, 2, 200);
await helpers.checkArtist(req, 2, 200);
await helpers.checkSong(req, 2, 200);
await helpers.logout(req, 200);
} finally {
req.close();
done();
}
});
});

@ -4,19 +4,32 @@ const express = require('express');
import { SetupApp } from '../../../app';
import { expect } from 'chai';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app, await helpers.initTestDB(), '');;
return app;
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /query with no songs', () => {
it('should give empty list', done => {
init().then((app) => {
chai
.request(app)
it('should give empty list', async done => {
let agent = await init();
try {
let res = await agent
.post('/query')
.send({
'query': {},
@ -31,241 +44,243 @@ describe('POST /query with no songs', () => {
'ascending': true
}
})
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [],
tags: [],
artists: [],
albums: [],
});
} finally {
agent.close();
done();
}
});
});
describe('POST /query with several songs and filters', () => {
it('should give all correct results', async done => {
const song1 = {
songId: 1,
title: 'Song1',
storeLinks: [],
artists: [
{
artistId: 1,
name: 'Artist1',
storeLinks: [],
}
],
tags: [],
albums: []
};
const song2 = {
songId: 2,
title: 'Song2',
storeLinks: [],
artists: [
{
artistId: 1,
name: 'Artist1',
storeLinks: [],
}
],
tags: [],
albums: []
};
const song3 = {
songId: 3,
title: 'Song3',
storeLinks: [],
artists: [
{
artistId: 2,
name: 'Artist2',
storeLinks: [],
}
],
tags: [],
albums: []
};
async function checkAllSongs(req) {
await req
.post('/query')
.send({
"query": {},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 0,
},
'ascending': true
}
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [],
tags: [],
songs: [song1, song2, song3],
artists: [],
tags: [],
albums: [],
});
done();
});
})
});
});
}
describe('POST /query with several songs and filters', () => {
it('should give all correct results', done => {
init().then((app) => {
const song1 = {
songId: 1,
title: 'Song1',
storeLinks: [],
artists: [
{
artistId: 1,
name: 'Artist1',
storeLinks: [],
}
],
tags: [],
albums: []
};
const song2 = {
songId: 2,
title: 'Song2',
storeLinks: [],
artists: [
{
artistId: 1,
name: 'Artist1',
storeLinks: [],
}
],
tags: [],
albums: []
};
const song3 = {
songId: 3,
title: 'Song3',
storeLinks: [],
artists: [
{
artistId: 2,
name: 'Artist2',
storeLinks: [],
}
],
tags: [],
albums: []
};
async function checkAllSongs(req) {
await req
.post('/query')
.send({
"query": {},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
async function checkIdIn(req) {
await req
.post('/query')
.send({
"query": {
"prop": "songId",
"propOperator": "IN",
"propOperand": [1, 3, 5]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 0,
},
'ordering': {
'orderBy': {
'type': 0,
},
'ascending': true
}
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [ song1, song2, song3 ],
artists: [],
tags: [],
albums: [],
});
'ascending': true
}
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song1, song3],
artists: [],
tags: [],
albums: [],
});
}
});
}
async function checkIdIn(req) {
await req
.post('/query')
.send({
"query": {
"prop": "songId",
"propOperator": "IN",
"propOperand": [1, 3, 5]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
async function checkIdNotIn(req) {
await req
.post('/query')
.send({
"query": {
"prop": "songId",
"propOperator": "NOTIN",
"propOperand": [1, 3, 5]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 0,
},
'ordering': {
'orderBy': {
'type': 0,
},
'ascending': true
}
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [ song1, song3 ],
artists: [],
tags: [],
albums: [],
});
'ascending': true
}
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song2],
artists: [],
tags: [],
albums: [],
});
}
});
}
async function checkIdNotIn(req) {
await req
.post('/query')
.send({
"query": {
"prop": "songId",
"propOperator": "NOTIN",
"propOperand": [1, 3, 5]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
async function checkArtistIdIn(req) {
await req
.post('/query')
.send({
"query": {
"prop": "artistId",
"propOperator": "IN",
"propOperand": [1]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 0,
},
'ordering': {
'orderBy': {
'type': 0,
},
'ascending': true
}
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [ song2 ],
artists: [],
tags: [],
albums: [],
});
'ascending': true
}
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song1, song2],
artists: [],
tags: [],
albums: [],
});
}
});
}
async function checkArtistIdIn(req) {
await req
.post('/query')
.send({
"query": {
"prop": "artistId",
"propOperator": "IN",
"propOperand": [1]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 0,
async function checkOrRelation(req) {
await req
.post('/query')
.send({
"query": {
"childrenOperator": "OR",
"children": [
{
"prop": "artistId",
"propOperator": "IN",
"propOperand": [2]
},
'ascending': true
}
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [ song1, song2 ],
artists: [],
tags: [],
albums: [],
});
});
}
async function checkOrRelation(req) {
await req
.post('/query')
.send({
"query": {
"childrenOperator": "OR",
"children": [
{
"prop": "artistId",
"propOperator": "IN",
"propOperand": [2]
},
{
"prop": "songId",
"propOperator": "EQ",
"propOperand": 1
}
]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
{
"prop": "songId",
"propOperator": "EQ",
"propOperand": 1
}
]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 0,
},
'ordering': {
'orderBy': {
'type': 0,
},
'ascending': true
}
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [ song1, song3 ],
artists: [],
tags: [],
albums: [],
});
'ascending': true
}
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song1, song3],
artists: [],
tags: [],
albums: [],
});
}
var req = chai.request(app).keepOpen();
});
}
helpers.createArtist(req, { name: "Artist1" }, 200)
.then(() => helpers.createArtist(req, { name: "Artist2" }, 200))
.then(() => helpers.createSong(req, { title: "Song1", artistIds: [1] }, 200))
.then(() => helpers.createSong(req, { title: "Song2", artistIds: [1] }, 200))
.then(() => helpers.createSong(req, { title: "Song3", artistIds: [2] }, 200))
.then(() => checkAllSongs(req))
.then(() => checkIdIn(req))
.then(() => checkIdNotIn(req))
.then(() => checkArtistIdIn(req))
.then(() => checkOrRelation(req))
.then(req.close)
.then(done)
})
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "Artist1" }, 200);
await helpers.createArtist(req, { name: "Artist2" }, 200);
await helpers.createSong(req, { title: "Song1", artistIds: [1] }, 200);
await helpers.createSong(req, { title: "Song2", artistIds: [1] }, 200);
await helpers.createSong(req, { title: "Song3", artistIds: [2] }, 200);
await checkAllSongs(req);
await checkIdIn(req);
await checkIdNotIn(req);
await checkArtistIdIn(req);
await checkOrRelation(req);
} finally {
req.close();
agent.close();
done();
}
});
});

@ -4,114 +4,128 @@ const express = require('express');
import { SetupApp } from '../../../app';
import { expect } from 'chai';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app, await helpers.initTestDB(), '');
return app;
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /song with no title', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.post('/song')
.send({})
.then((res) => {
expect(res).to.have.status(400);
done();
});
})
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createSong(req, {}, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with only a title', () => {
it('should return the first available id', done => {
init().then(async(app) => {
chai
.request(app)
.post('/song')
.send({
title: "MySong"
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
id: 1
});
done();
});
})
it('should return the first available id', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createSong(req, { title: "MySong" }, 200, { id: 1 });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with a nonexistent artist Id', () => {
it('should fail', done => {
init().then(async (app) => {
chai
.request(app)
.post('/song')
.send({
title: "MySong",
artistIds: [1]
})
.then((res) => {
expect(res).to.have.status(400);
done();
});
})
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createSong(req, { title: "MySong", artistIds: [1] }, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with an existing artist Id', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 })
.then(() => helpers.createSong(req, { title: "MySong", artistIds: [ 1 ] }, 200, { id: 1 }) )
.then(req.close)
.then(done);
});
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 });
await helpers.createSong(req, { title: "MySong", artistIds: [1] }, 200, { id: 1 });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with two existing artist Ids', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 })
.then(() => helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 }) )
.then(() => helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 200, { id: 1 }) )
.then(req.close)
.then(done);
});
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 })
await helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 })
await helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 200, { id: 1 })
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with an existent and a nonexistent artist Id', () => {
it('should fail', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 })
.then(() => helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 400) )
.then(req.close)
.then(done);
});
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 })
await helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 400)
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with tags', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createTag(req, { name: "Root" }, 200, { id: 1 })
.then(() => helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 }))
.then(() => helpers.createSong(req, { title: "Song", tagIds: [ 1, 2 ] }, 200, { id: 1 }))
.then(() => helpers.checkSong(req, 1, 200, { title: "Song", storeLinks: [], tagIds: [ 1, 2 ], albumIds: [], artistIds: [] }))
.then(req.close)
.then(done);
});
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "Root" }, 200, { id: 1 })
await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 })
await helpers.createSong(req, { title: "Song", tagIds: [1, 2] }, 200, { id: 1 })
await helpers.checkSong(req, 1, 200, { title: "Song", storeLinks: [], tagIds: [1, 2], albumIds: [], artistIds: [] })
} finally {
req.close();
agent.close();
done();
}
});
});

@ -4,72 +4,84 @@ const express = require('express');
import { SetupApp } from '../../../app';
import { expect } from 'chai';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app, await helpers.initTestDB(), '');
return app;
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /tag with no name', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.post('/tag')
.send({})
.then((res) => {
expect(res).to.have.status(400);
done();
});
});
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, {}, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /tag with a correct request', () => {
it('should succeed', done => {
init().then((app) => {
chai
.request(app)
.post('/tag')
.send({
name: "MyTag"
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
id: 1
});
done();
});
});
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "MyTag" }, 200, { id: 1 });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /tag with a parent', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 })
.then(() => helpers.createTag(req, { name: "Tag2", parentId: 1 }, 200, { id: 2 }) )
.then(() => helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 }))
.then(req.close)
.then(done);
});
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 })
await helpers.createTag(req, { name: "Tag2", parentId: 1 }, 200, { id: 2 })
await helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 })
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /tag with a new parent', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 })
.then(() => helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 }) )
.then(() => helpers.modifyTag(req, 2, { parentId: 1 }, 200) )
.then(() => helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 }))
.then(req.close)
.then(done);
});
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 })
await helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 })
await helpers.modifyTag(req, 2, { parentId: 1 }, 200)
await helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 })
} finally {
req.close();
agent.close();
done();
}
});
});

@ -1,4 +1,5 @@
import { expect } from "chai";
import { sha512 } from "js-sha512";
export async function initTestDB() {
// Allow different database configs - but fall back to SQLite in memory if necessary.
@ -11,6 +12,7 @@ export async function initTestDB() {
// Undoing and doing the migrations is a test in itself.
await knex.migrate.rollback(undefined, true);
await knex.migrate.latest();
return knex;
}
@ -184,4 +186,47 @@ export async function checkAlbum(
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
})
}
export async function createUser(
req,
email,
password,
expectStatus = undefined,
expectResponse = undefined,
) {
const res = await req
.post('/register')
.send({
email: email,
password: password,
});
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
}
export async function login(
req,
email,
password,
expectStatus = undefined,
expectResponse = undefined,
) {
const res = await req
.post('/login?username=' + encodeURIComponent(email) + '&password=' + encodeURIComponent(password))
.send({});
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
}
export async function logout(
req,
expectStatus = undefined,
expectResponse = undefined,
) {
const res = await req
.post('/logout')
.send({});
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
}
Loading…
Cancel
Save