The creation and update features of the app has been implemented. Redundant user handling has been removed and is wholly managed by Auth0.master
@@ -41,8 +41,8 @@ | |||
- [X] In the front-end, the client sends the ringtone data to the back-end. | |||
- [X] In the back-end, the server stores the ringtone data. | |||
- As a composer, I want to update a ringtone. | |||
- [ ] In the front-end, the client modifies the ringtone data retrieved from the back-end and loaded on the view. | |||
- [ ] In the front-end, the client sends the ringtone data to the back-end. | |||
- [X] In the front-end, the client modifies the ringtone data retrieved from the back-end and loaded on the view. | |||
- [X] In the front-end, the client sends the ringtone data to the back-end. | |||
- [X] In the back-end, the server stores the updated ringtone data. | |||
- As a composer, I want to soft-delete a ringtone. | |||
- [ ] In the front-end, the client provides a ringtone ID to request a ringtone's soft deletion to the back-end. | |||
@@ -1,2 +0,0 @@ | |||
Name: Happy Birthday | |||
Data: 8g4 8g4 4a4 4g4 4c5 2b4 8g4 8g4 4a4 4g4 4d5 2c5 8g4 8g4 4g5 4e5 8c5 8c5 4b4 4a4 8f5 8f5 4e5 4c5 4d5 2c5. |
@@ -0,0 +1,6 @@ | |||
- name: Happy Birthday | |||
tempo: 120 | |||
data: 8g4 8g4 4a4 4g4 4c5 2b4 8g4 8g4 4a4 4g4 4d5 2c5 8g4 8g4 4g5 4e5 8c5 8c5 4b4 4a4 8f5 8f5 4e5 4c5 4d5 2c5. | |||
- name: Quantization Test | |||
tempo: 120 | |||
data: 32c4 32g4 32e5 32e4 32c5 32g5 32g4 32e5 32c6 32c5 32g5 32e6 32e5 32c6 32g6 32g5 32e6 32c7 32c6 32g6 32e7 32e6 32c7 32g7 |
@@ -7,4 +7,3 @@ AUTH0_CLIENT_ID= | |||
AUTH0_CLIENT_SECRET= | |||
CYPRESS_USERNAME= | |||
CYPRESS_PASSWORD= | |||
AUTH0_MGMT_API_TOKEN= |
@@ -149,27 +149,16 @@ const CreateRingtoneForm: FC<Props> = ({ | |||
> | |||
<LeftSidebarWithMenu.ContentContainer> | |||
<FormContents> | |||
{ | |||
defaultValues.id | |||
&& ( | |||
<input | |||
type="hidden" | |||
name="id" | |||
defaultValue={Uuid.parse(defaultValues.id.toString()).toString()} | |||
/> | |||
) | |||
} | |||
{ | |||
defaultValues.composerUserId | |||
&& ( | |||
<input | |||
type="hidden" | |||
name="composerId" | |||
defaultValue={Uuid.parse(defaultValues.composerUserId.toString()).toString()} | |||
/> | |||
) | |||
} | |||
<input | |||
type="hidden" | |||
name="id" | |||
defaultValue={defaultValues.id ? Uuid.parse(defaultValues.id.toString()).toString() : undefined} | |||
/> | |||
<input | |||
type="hidden" | |||
name="composerUserSub" | |||
defaultValue={defaultValues.composerUserSub} | |||
/> | |||
<Primary> | |||
<TextInput | |||
label={labels['name'] || 'Name'} | |||
@@ -1,5 +1,6 @@ | |||
import {FC, FormEventHandler} from 'react' | |||
import styled from 'styled-components' | |||
import {Session} from '@auth0/nextjs-auth0' | |||
import { LeftSidebarWithMenu } from '@theoryofnekomata/viewfinder' | |||
import {models} from '@tonality/library-common' | |||
import CreateRingtoneForm from '../../organisms/forms/CreateRingtone' | |||
@@ -30,7 +31,7 @@ const Avatar = styled('img')({ | |||
type Props = { | |||
onSearch?: FormEventHandler, | |||
onSubmit?: FormEventHandler, | |||
composer: any, | |||
session: Partial<Session>, | |||
currentRingtone?: models.Ringtone, | |||
composerRingtones: models.Ringtone[], | |||
@@ -46,7 +47,7 @@ type Props = { | |||
const CreateRingtoneTemplate: FC<Props> = ({ | |||
onSearch, | |||
onSubmit, | |||
composer, | |||
session, | |||
currentRingtone = {}, | |||
composerRingtones = [], | |||
updateTempo, | |||
@@ -71,7 +72,7 @@ const CreateRingtoneTemplate: FC<Props> = ({ | |||
}} | |||
> | |||
<Avatar | |||
src={composer.picture} | |||
src={session.user.picture} | |||
/> | |||
</Link> | |||
} | |||
@@ -135,13 +136,12 @@ const CreateRingtoneTemplate: FC<Props> = ({ | |||
}, | |||
]} | |||
> | |||
<Padding> | |||
<CreateRingtoneForm | |||
onSubmit={onSubmit} | |||
defaultValues={{ | |||
...currentRingtone, | |||
composerUserId: composer.id, | |||
composerUserSub: session.user.sub, | |||
}} | |||
action="/api/a/create/ringtone" | |||
labels={{ | |||
@@ -6,21 +6,50 @@ import * as endpoints from './endpoints' | |||
export default class RingtoneClient { | |||
private readonly fetchClient: FetchClient | |||
constructor(private readonly baseUrl) { | |||
constructor(private readonly baseUrl, private readonly session?) { | |||
const headers = {} | |||
if (session) { | |||
headers['Authorization'] = `${session.token_type} ${session.idToken}` | |||
} | |||
this.fetchClient = createFetchClient({ | |||
baseUrl, | |||
headers, | |||
}) | |||
} | |||
save = async (e: FormEvent & { submitter: HTMLInputElement | HTMLButtonElement }) => { | |||
save = ({ router, }) => async (e: FormEvent & { submitter: HTMLInputElement | HTMLButtonElement }) => { | |||
e.preventDefault() | |||
const {tempo, name, data} = getFormValues(e.target as HTMLFormElement, { submitter: e.submitter }) | |||
const form = e.target as HTMLFormElement | |||
const {tempo, name, data, composerUserSub, id} = getFormValues(form, { submitter: e.submitter }) | |||
const values = { | |||
composerUserSub, | |||
name, | |||
data, | |||
tempo: Number(tempo), | |||
} | |||
const response = await this.fetchClient(endpoints.create(values)) | |||
const endpoint = id ? endpoints.update(id) : endpoints.create | |||
const response = await this.fetchClient(endpoint(values)) | |||
if (response.ok) { | |||
const newValues = await response.json() | |||
const { id: newId } = newValues.data | |||
router.replace({ | |||
pathname: '/my/create/ringtones/[id]', | |||
query: { | |||
id: newId, | |||
}, | |||
shallow: true, | |||
}) | |||
} | |||
alert(response.statusText) | |||
} | |||
load = async ({ id }) => { | |||
const response = await this.fetchClient(endpoints.get(id)) | |||
if (response.ok) { | |||
const { data } = await response.json() | |||
return data | |||
} | |||
} | |||
} |
@@ -1,15 +1,12 @@ | |||
import {models} from '@tonality/library-common'; | |||
import {FetchClientParams, Method} from '../../utils/api/fetch'; | |||
const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ijc3bnFZZjV1b0lRQ25xbld0UndaMyJ9.eyJpc3MiOiJodHRwczovL21vZGFsLmpwLmF1dGgwLmNvbS8iLCJzdWIiOiJYOE1PT2JKdk5QdG5lRDVMbzVJYjY0a1c1dVRWR2hVdUBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tb2RhbC5qcC5hdXRoMC5jb20vYXBpL3YyLyIsImlhdCI6MTYyMzA3Mzg5NywiZXhwIjoxNjIzMTYwMjk3LCJhenAiOiJYOE1PT2JKdk5QdG5lRDVMbzVJYjY0a1c1dVRWR2hVdSIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.WiRaFG375CbyHN3XIhf6joY3CYh89h7Xwt6vReWDOS7wKEMVRtXvd-VEBwxf_ibfIAdrB5qibjF5JwtCl18Kd8m2fEDVwj8z8qUKyChPipNezCGfbfqz6Kv_ykf06KEBwypsVwdk2YhAcxdspWuilMUPizAPkno8GXbjFYOpeZobwA4Y50zeKDWRP6SPCM94dlN7zf3myu98wBqOk4KiXH-cyO_dIVF42KTnnZlVfkEmJoLJUmSUUbRNPwrx6k-eQ2uP0whvwfhZRwo5u0uVxnnQBcEK0fTQ9CDJPKxbwULFkjfN0nLfxOLcRMdPFMNtcWEFcFDr6LHGnqDCEG4lEw' | |||
export const create = (body: Partial<models.Ringtone>): FetchClientParams => ({ | |||
method: Method.POST, | |||
url: ['', 'api', 'ringtones'].join('/'), | |||
body: JSON.stringify(body), | |||
headers: { | |||
'Content-Type': 'application/json', | |||
'Authorization': `Bearer ${token}` | |||
}, | |||
}) | |||
@@ -0,0 +1,93 @@ | |||
import {GetServerSideProps, NextPage} from 'next' | |||
import {getSession, Session, withPageAuthRequired} from '@auth0/nextjs-auth0'; | |||
import {useEffect, useState} from 'react'; | |||
import {models} from '@tonality/library-common' | |||
import CreateRingtoneTemplate from '../../../../components/templates/CreateRingtone' | |||
import RingtoneClient from '../../../../modules/ringtone/client' | |||
import WaveOscillator from '../../../../utils/sound/WaveOscillator' | |||
import SoundManager from '../../../../utils/sound/SoundManager' | |||
import ComposerClient from '../../../../modules/composer/client' | |||
import {useRouter} from 'next/router'; | |||
type Props = { | |||
session: Partial<Session>, | |||
currentRingtone: models.Ringtone, | |||
composerRingtones: models.Ringtone[], | |||
} | |||
const Page: NextPage<Props> = ({ | |||
session, | |||
currentRingtone, | |||
composerRingtones, | |||
}) => { | |||
const [hydrated, setHydrated] = useState(false) | |||
const [ringtoneClient, setRingtoneClient] = useState<RingtoneClient>(null) | |||
const [composerClient, setComposerClient] = useState<ComposerClient>(null) | |||
const router = useRouter() | |||
useEffect(() => { | |||
setHydrated(true) | |||
}, []) | |||
useEffect(() => { | |||
setRingtoneClient(new RingtoneClient(process.env.NEXT_PUBLIC_API_BASE_URL, session)) | |||
}, [hydrated]) | |||
useEffect(() => { | |||
const audioContext = new AudioContext() | |||
const gainNode = audioContext.createGain() | |||
gainNode.gain.value = 0.05 | |||
gainNode.connect(audioContext.destination) | |||
const oscillator = new WaveOscillator(audioContext, gainNode) | |||
const soundManager = new SoundManager(oscillator) | |||
setComposerClient(new ComposerClient(soundManager)) | |||
}, [hydrated]) | |||
return ( | |||
<CreateRingtoneTemplate | |||
session={session} | |||
currentRingtone={currentRingtone} | |||
composerRingtones={composerRingtones} | |||
addNote={composerClient ? composerClient.addNote : undefined} | |||
addRest={composerClient ? composerClient.addRest : undefined} | |||
togglePlayback={composerClient ? composerClient.togglePlayback : undefined} | |||
updateSong={composerClient ? composerClient.updateSong : undefined} | |||
updateTempo={composerClient ? composerClient.updateTempo : undefined} | |||
updateView={composerClient ? composerClient.updateView : undefined} | |||
onSubmit={ringtoneClient ? ringtoneClient.save({ router }) : undefined} | |||
play={composerClient ? composerClient.play : undefined} | |||
/> | |||
) | |||
} | |||
export const getServerSideProps: GetServerSideProps = withPageAuthRequired({ | |||
getServerSideProps: async ({ req, res, params }) => { | |||
const { id } = params | |||
const { idToken, token_type, user } = getSession(req, res) | |||
const composerRingtones = [] | |||
const session = { | |||
idToken, | |||
token_type, | |||
user, | |||
} | |||
const client = new RingtoneClient(process.env.NEXT_PUBLIC_API_BASE_URL, session) | |||
const currentRingtone = await client.load({ id }) | |||
if (!currentRingtone) { | |||
return { | |||
notFound: true, | |||
} | |||
} | |||
return { | |||
props: { | |||
session, | |||
currentRingtone, | |||
composerRingtones, | |||
}, | |||
} | |||
}, | |||
returnTo: '/my/create/ringtone' | |||
}) | |||
export default Page |
@@ -1,34 +1,34 @@ | |||
import {GetServerSideProps, NextPage} from 'next' | |||
import {getSession, Session, withPageAuthRequired} from '@auth0/nextjs-auth0'; | |||
import {useEffect, useState} from 'react'; | |||
import {models} from '@tonality/library-common' | |||
import {useUser} from '@auth0/nextjs-auth0' | |||
import CreateRingtoneTemplate from '../../../../components/templates/CreateRingtone' | |||
import RingtoneClient from '../../../../modules/ringtone/client' | |||
import {getSession, withPageAuthRequired} from '@auth0/nextjs-auth0'; | |||
import WaveOscillator from '../../../../utils/sound/WaveOscillator'; | |||
import SoundManager from '../../../../utils/sound/SoundManager'; | |||
import ComposerClient from '../../../../modules/composer/client'; | |||
import WaveOscillator from '../../../../utils/sound/WaveOscillator' | |||
import SoundManager from '../../../../utils/sound/SoundManager' | |||
import ComposerClient from '../../../../modules/composer/client' | |||
import {useRouter} from 'next/router'; | |||
type Props = { | |||
user: models.User, | |||
session: Partial<Session>, | |||
composerRingtones: models.Ringtone[], | |||
} | |||
const MyCreateRingtonePage: NextPage<Props> = ({ | |||
user, | |||
const Page: NextPage<Props> = ({ | |||
session, | |||
composerRingtones, | |||
}) => { | |||
const [hydrated, setHydrated] = useState(false) | |||
const [ringtoneClient, setRingtoneClient] = useState<RingtoneClient>(null) | |||
const [composerClient, setComposerClient] = useState<ComposerClient>(null) | |||
const theUser = useUser() | |||
const router = useRouter() | |||
useEffect(() => { | |||
setHydrated(true) | |||
}, []) | |||
useEffect(() => { | |||
setRingtoneClient(new RingtoneClient(process.env.NEXT_PUBLIC_API_BASE_URL)) | |||
setRingtoneClient(new RingtoneClient(process.env.NEXT_PUBLIC_API_BASE_URL, session)) | |||
}, [hydrated]) | |||
useEffect(() => { | |||
@@ -43,7 +43,7 @@ const MyCreateRingtonePage: NextPage<Props> = ({ | |||
return ( | |||
<CreateRingtoneTemplate | |||
composer={user} | |||
session={session} | |||
composerRingtones={composerRingtones} | |||
addNote={composerClient ? composerClient.addNote : undefined} | |||
addRest={composerClient ? composerClient.addRest : undefined} | |||
@@ -51,7 +51,7 @@ const MyCreateRingtonePage: NextPage<Props> = ({ | |||
updateSong={composerClient ? composerClient.updateSong : undefined} | |||
updateTempo={composerClient ? composerClient.updateTempo : undefined} | |||
updateView={composerClient ? composerClient.updateView : undefined} | |||
onSubmit={ringtoneClient ? ringtoneClient.save : undefined} | |||
onSubmit={ringtoneClient ? ringtoneClient.save({ router, }) : undefined} | |||
play={composerClient ? composerClient.play : undefined} | |||
/> | |||
) | |||
@@ -59,17 +59,17 @@ const MyCreateRingtonePage: NextPage<Props> = ({ | |||
export const getServerSideProps: GetServerSideProps = withPageAuthRequired({ | |||
getServerSideProps: async (ctx) => { | |||
const session = getSession(ctx.req, ctx.res) | |||
const { user } = session | |||
const { idToken, token_type, user } = getSession(ctx.req, ctx.res) | |||
const composerRingtones = [] | |||
const session = { | |||
idToken, | |||
token_type, | |||
user, | |||
} | |||
return { | |||
props: { | |||
user: { | |||
id: user.sub, | |||
name: user.nickname, | |||
bio: '', | |||
}, | |||
session, | |||
composerRingtones, | |||
}, | |||
} | |||
@@ -77,13 +77,4 @@ export const getServerSideProps: GetServerSideProps = withPageAuthRequired({ | |||
returnTo: '/my/create/ringtone' | |||
}) | |||
export default MyCreateRingtonePage | |||
/* | |||
8g4 8g4 4a4 4g4 4c5 2b4 8g4 8g4 4a4 4g4 4d5 2c5 8g4 8g4 4g5 4e5 8c5 8c5 4b4 4a4 8f5 8f5 4e5 4c5 4d5 2c5. | |||
*/ | |||
/* | |||
32c4 32g4 32e5 32e4 32c5 32g5 32g4 32e5 32c6 32c5 32g5 32e6 32e5 32c6 32g6 32g5 32e6 32c7 32c6 32g6 32e7 32e6 32c7 32g7 | |||
*/ | |||
export default Page |
@@ -1,14 +1,6 @@ | |||
import Uuid from '@tonality/library-uuid' | |||
export namespace models { | |||
export class User { | |||
id: Uuid | |||
username: string | |||
password: string | |||
} | |||
export class Ringtone { | |||
id: Uuid | |||
@@ -24,14 +16,6 @@ export namespace models { | |||
deletedAt?: Date | null | |||
composerUserId: Uuid | |||
} | |||
export class UserProfile { | |||
userId: Uuid | |||
bio?: string | null | |||
email?: string | null | |||
composerUserSub: string | |||
} | |||
} |
@@ -8,11 +8,11 @@ function toBuffer(this: Uuid) { | |||
return Buffer.from(this) | |||
} | |||
const constructUuidBase = (bytes: any) => { | |||
const uuidBase = Buffer.from(bytes) as Uuid | |||
const constructUuidBase = (bytes: any): UuidBuffer => { | |||
const uuidBase = Buffer.from(bytes) | |||
uuidBase.toString = toString.bind(uuidBase); | |||
const uuidBaseExtend = uuidBase as unknown as Record<string, unknown> | |||
uuidBaseExtend['toBuffer'] = toBuffer.bind(uuidBase) | |||
uuidBaseExtend.toBuffer = toBuffer.bind(uuidBase) | |||
return uuidBase | |||
} | |||
@@ -8,34 +8,14 @@ datasource db { | |||
} | |||
model Ringtone { | |||
id Bytes @id | |||
name String | |||
composerUserId Bytes @map("composer_user_id") | |||
tempo Int @default(120) | |||
data String @default("") | |||
createdAt DateTime @default(now()) @map("created_at") | |||
updatedAt DateTime @default(now()) @map("updated_at") | |||
deletedAt DateTime? @map("deleted_at") | |||
composer User @relation(fields: [composerUserId], references: [id]) | |||
id Bytes @id | |||
name String | |||
composerUserSub String @map("composer_user_sub") | |||
tempo Int @default(120) | |||
data String @default("") | |||
createdAt DateTime @default(now()) @map("created_at") | |||
updatedAt DateTime @default(now()) @map("updated_at") | |||
deletedAt DateTime? @map("deleted_at") | |||
@@map("ringtone") | |||
} | |||
model User { | |||
id Bytes @id | |||
username String @unique | |||
password String | |||
ringtone Ringtone[] | |||
userProfile UserProfile? | |||
@@map("user") | |||
} | |||
model UserProfile { | |||
userId Bytes @id @map("user_id") | |||
bio String? @default("") | |||
email String? @default("") | |||
user User @relation(fields: [userId], references: [id]) | |||
@@map("user_profile") | |||
} | |||
} |
@@ -16,10 +16,6 @@ const app: FastifyPluginAsync<AppOptions> = async ( | |||
// Place here your custom code! | |||
const modules = await Promise.all([ | |||
import('./global'), | |||
import('./modules/credentials'), | |||
import('./modules/auth'), | |||
import('./modules/password'), | |||
import('./modules/user'), | |||
import('./modules/ringtone'), | |||
]) | |||
@@ -1,30 +0,0 @@ | |||
import {inject, singleton} from 'tsyringe'; | |||
import AuthService from './service'; | |||
import {Controller, ControllerResponse} from '../../utils/helpers'; | |||
import Credentials from '../credentials/type'; | |||
type AuthController = Controller<{ | |||
logIn: ControllerResponse<Credentials>, | |||
}> | |||
export default AuthController | |||
@singleton() | |||
export class AuthControllerImpl implements AuthController { | |||
constructor( | |||
@inject('AuthService') | |||
private readonly authService: AuthService | |||
) {} | |||
logIn = async (request: any, reply: any) => { | |||
try { | |||
const data = await this.authService.logIn(request.body['username'], request.body['password']) | |||
return { | |||
data | |||
} | |||
} catch (err) { | |||
reply.raw.statusMessage = 'Invalid Credentials' | |||
reply.unauthorized() | |||
} | |||
} | |||
} |
@@ -1,8 +0,0 @@ | |||
import {DependencyContainer} from 'tsyringe'; | |||
import {AuthServiceImpl} from './service'; | |||
import {AuthControllerImpl} from './controller'; | |||
export default (container: DependencyContainer) => { | |||
container.register('AuthController', { useClass: AuthControllerImpl }) | |||
container.register('AuthService', { useClass: AuthServiceImpl }) | |||
} |
@@ -1,37 +0,0 @@ | |||
import {inject, singleton} from 'tsyringe'; | |||
import UserService from '../user/service'; | |||
import PasswordService from '../password/service'; | |||
import CredentialsService from '../credentials/service'; | |||
import Credentials from '../credentials/type'; | |||
export default interface AuthService { | |||
logIn(username: string, password: string): Promise<Credentials> | |||
} | |||
@singleton() | |||
export class AuthServiceImpl implements AuthService { | |||
constructor( | |||
@inject('UserService') | |||
private readonly userService: UserService, | |||
@inject('PasswordService') | |||
private readonly passwordService: PasswordService, | |||
@inject('CredentialsService') | |||
private readonly credentialsService: CredentialsService, | |||
) {} | |||
async logIn(username: string, password: string): Promise<Credentials> { | |||
const user = await this.userService.getByUsername(username) | |||
if (!user) { | |||
throw new Error('Invalid credentials.') | |||
} | |||
const valid = this.passwordService.compare(password, user.password) | |||
if (!valid) { | |||
throw new Error('Invalid credentials.') | |||
} | |||
const credentials = await this.credentialsService.request() | |||
credentials.profile = await this.userService.getProfileByUsername(username) | |||
return credentials | |||
} | |||
} |
@@ -1,6 +0,0 @@ | |||
import {DependencyContainer} from 'tsyringe'; | |||
import {CredentialsServiceImpl} from './service'; | |||
export default (container: DependencyContainer) => { | |||
container.register('CredentialsService', { useClass: CredentialsServiceImpl }) | |||
} |
@@ -1,34 +0,0 @@ | |||
import {URL} from 'url'; | |||
import unfetch from 'unfetch'; | |||
import Credentials from './type'; | |||
import {singleton} from 'tsyringe'; | |||
export default interface CredentialsService { | |||
request(): Promise<Credentials> | |||
} | |||
@singleton() | |||
export class CredentialsServiceImpl { | |||
async request(): Promise<Credentials> { | |||
const tokenUrl = new URL('/oauth/token', process.env.AUTH0_DOMAIN) | |||
const audienceUrl = new URL('/api/v2', process.env.AUTH0_DOMAIN) | |||
const response = await unfetch(tokenUrl.toString(), { | |||
method: 'POST', | |||
body: JSON.stringify({ | |||
client_id: process.env.AUTH0_CLIENT_ID, | |||
client_secret: process.env.AUTH0_SECRET, | |||
audience: audienceUrl.toString(), | |||
grant_type: 'client_credentials', | |||
}) | |||
}) | |||
if (!response.ok) { | |||
throw new Error('Unable to request credentials.') | |||
} | |||
const { access_token: accessToken, expires_in: expiresIn, token_type: tokenType } = await response.json() | |||
return { | |||
accessToken, | |||
expiresIn, | |||
tokenType, | |||
} | |||
} | |||
} |
@@ -1,6 +0,0 @@ | |||
export default interface Credentials { | |||
[k: string]: string | number | unknown, | |||
accessToken: string, | |||
expiresIn: number, | |||
tokenType: string, | |||
} |
@@ -1,6 +0,0 @@ | |||
import {DependencyContainer} from 'tsyringe'; | |||
import {PasswordServiceImpl} from './service'; | |||
export default (container: DependencyContainer) => { | |||
container.register('PasswordService', { useClass: PasswordServiceImpl }) | |||
} |
@@ -1,24 +0,0 @@ | |||
import { genSalt, hash, compare } from 'bcrypt'; | |||
import {inject, singleton} from 'tsyringe'; | |||
export default interface PasswordService { | |||
hash(password: string): Promise<string> | |||
compare(password: string, hash: string): Promise<boolean> | |||
} | |||
@singleton() | |||
export class PasswordServiceImpl implements PasswordService { | |||
constructor( | |||
@inject('PASSWORD_GEN_SALT_ROUNDS') | |||
private readonly passwordGenSaltRounds: number, | |||
) {} | |||
async hash(password: string) { | |||
const salt = await genSalt(this.passwordGenSaltRounds) | |||
return await hash(password, salt) | |||
} | |||
async compare(password: string, hash: string) { | |||
return await compare(password, hash) | |||
} | |||
} |
@@ -3,7 +3,12 @@ import {models} from '@tonality/library-common' | |||
import Uuid from '@tonality/library-uuid' | |||
import {Controller, ControllerResponse, PaginatedResponse} from '../../utils/helpers' | |||
import RingtoneService from './service' | |||
import {DoubleDeletionError} from './response' | |||
import { | |||
InsufficientDataError, | |||
OwnershipError, | |||
RingtoneNotFoundError, | |||
UpdateDeletedRingtoneError, | |||
} from './response'; | |||
type RingtoneController = Controller<{ | |||
get: ControllerResponse<models.Ringtone>, | |||
@@ -20,7 +25,6 @@ export default RingtoneController | |||
const serializeRingtone = d => ({ | |||
...d, | |||
composerUserId: d.composerUserId.toString(), | |||
id: d.id.toString(), | |||
}) | |||
@@ -34,23 +38,23 @@ export class RingtoneControllerImpl implements RingtoneController { | |||
get = async (request: any, reply: any) => { | |||
try { | |||
const id = Uuid.parse(request.params['id']) | |||
const rawData = await this.ringtoneService.get(id) | |||
const rawData = await this.ringtoneService.get(id, request.user?.sub) | |||
const data = serializeRingtone(rawData) | |||
if (typeof (data.deletedAt as Date) !== 'undefined') { | |||
if (data.deletedAt instanceof Date) { | |||
reply.raw.statusMessage = 'Ringtone Deleted Previously' | |||
reply.gone() | |||
return | |||
} | |||
if (!data) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
reply.raw.statusMessage = 'Single Ringtone Retrieved' | |||
return { | |||
data, | |||
} | |||
} catch (err) { | |||
if (err instanceof RingtoneNotFoundError) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
reply.raw.statusMessage = 'Get Ringtone Error' | |||
reply.internalServerError(err.message) | |||
} | |||
@@ -65,7 +69,7 @@ export class RingtoneControllerImpl implements RingtoneController { | |||
const skip = !isNaN(skipNumber) ? skipNumber : 0 | |||
const take = !isNaN(takeNumber) ? takeNumber : 10 | |||
const rawData = await this.ringtoneService.browse(skip, take) | |||
const rawData = await this.ringtoneService.browse(skip, take, request.user?.sub) | |||
const data = rawData.map(serializeRingtone) | |||
reply.raw.statusMessage = 'Multiple Ringtones Retrieved' | |||
@@ -83,7 +87,7 @@ export class RingtoneControllerImpl implements RingtoneController { | |||
search = async (request: any, reply: any) => { | |||
try { | |||
const { 'q': query } = request.query | |||
const rawData = await this.ringtoneService.search(query) | |||
const rawData = await this.ringtoneService.search(query, request.user?.sub) | |||
const data = rawData.map(serializeRingtone) | |||
reply.raw.statusMessage = 'Search Results Retrieved' | |||
@@ -100,11 +104,7 @@ export class RingtoneControllerImpl implements RingtoneController { | |||
// TODO parse and validate body | |||
try { | |||
const rawData = await this.ringtoneService.create({ | |||
...request.body, | |||
// TODO map auth credentials to user | |||
composerUserId: Uuid.parse(request.body.composerUserId), | |||
}) | |||
const rawData = await this.ringtoneService.create(request.body, request.user?.sub) | |||
const data = serializeRingtone(rawData) | |||
reply.raw.statusMessage = 'Ringtone Created' | |||
reply.raw.statusCode = 201 | |||
@@ -112,6 +112,16 @@ export class RingtoneControllerImpl implements RingtoneController { | |||
data, | |||
} | |||
} catch (err) { | |||
if (err instanceof InsufficientDataError) { | |||
reply.raw.statusMessage = 'Create Ringtone Insufficient Data Error' | |||
reply.badRequest(err.message) | |||
return | |||
} | |||
if (err instanceof OwnershipError) { | |||
reply.raw.statusMessage = 'Create Ringtone Ownership Error' | |||
reply.forbidden(err.message) | |||
return | |||
} | |||
reply.raw.statusMessage = 'Create Ringtone Error' | |||
reply.internalServerError(err.message) | |||
} | |||
@@ -124,23 +134,29 @@ export class RingtoneControllerImpl implements RingtoneController { | |||
const rawData = await this.ringtoneService.update({ | |||
...request.body, | |||
id, | |||
}) | |||
}, request.user?.sub) | |||
const data = serializeRingtone(rawData) | |||
if (data.deletedAt) { | |||
reply.raw.statusMessage = 'Ringtone Deleted Previously' | |||
reply.gone() | |||
return | |||
reply.raw.statusMessage = 'Ringtone Updated' | |||
return { | |||
data, | |||
} | |||
if (!data) { | |||
} catch (err) { | |||
if (err instanceof RingtoneNotFoundError) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
reply.raw.statusMessage = 'Ringtone Updated' | |||
return { | |||
data, | |||
if (err instanceof OwnershipError) { | |||
reply.raw.statusMessage = 'Update Ringtone Ownership Error' | |||
reply.forbidden(err.message) | |||
return | |||
} | |||
} catch (err) { | |||
if (err instanceof UpdateDeletedRingtoneError) { | |||
reply.raw.statusMessage = 'Ringtone Deleted Previously' | |||
reply.gone() | |||
return | |||
} | |||
reply.raw.statusMessage = 'Update Ringtone Error' | |||
reply.internalServerError(err.message) | |||
} | |||
} | |||
@@ -149,18 +165,23 @@ export class RingtoneControllerImpl implements RingtoneController { | |||
try { | |||
// TODO validate data | |||
const id = Uuid.parse(request.params['id']) | |||
const rawData = await this.ringtoneService.softDelete(id) | |||
const rawData = await this.ringtoneService.softDelete(id, request.user?.sub) | |||
const data = serializeRingtone(rawData) | |||
if (!data) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
reply.raw.statusMessage = 'Ringtone Soft-Deleted' | |||
return { | |||
data, | |||
} | |||
} catch (err) { | |||
if (err instanceof RingtoneNotFoundError) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
if (err instanceof OwnershipError) { | |||
reply.raw.statusMessage = 'Soft-Delete Ringtone Ownership Error' | |||
reply.forbidden(err.message) | |||
return | |||
} | |||
reply.raw.statusMessage = 'Soft-Delete Ringtone Error' | |||
reply.internalServerError(err.message) | |||
} | |||
@@ -170,18 +191,23 @@ export class RingtoneControllerImpl implements RingtoneController { | |||
try { | |||
// TODO validate data | |||
const id = Uuid.parse(request.params['id']) | |||
const rawData = await this.ringtoneService.undoDelete(id) | |||
const rawData = await this.ringtoneService.undoDelete(id, request.user?.sub) | |||
const data = serializeRingtone(rawData) | |||
if (!data) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
reply.raw.statusMessage = 'Ringtone Restored' | |||
return { | |||
data, | |||
} | |||
} catch (err) { | |||
if (err instanceof RingtoneNotFoundError) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
if (err instanceof OwnershipError) { | |||
reply.raw.statusMessage = 'Soft-Delete Ringtone Ownership Error' | |||
reply.forbidden(err.message) | |||
return | |||
} | |||
reply.raw.statusMessage = 'Restore Ringtone Error' | |||
reply.internalServerError(err.message) | |||
} | |||
@@ -191,13 +217,19 @@ export class RingtoneControllerImpl implements RingtoneController { | |||
try { | |||
// TODO validate data | |||
const id = Uuid.parse(request.params['id']) | |||
await this.ringtoneService.hardDelete(id) | |||
await this.ringtoneService.hardDelete(id, request.user?.sub) | |||
reply.status(204) | |||
reply.raw.statusMessage = 'Ringtone Hard-Deleted' | |||
} catch (err) { | |||
if (err instanceof DoubleDeletionError) { | |||
if (err instanceof RingtoneNotFoundError) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound(err.message) | |||
reply.notFound() | |||
return | |||
} | |||
if (err instanceof OwnershipError) { | |||
reply.raw.statusMessage = 'Delete Ringtone Ownership Error' | |||
reply.forbidden(err.message) | |||
return | |||
} | |||
reply.raw.statusMessage = 'Delete Ringtone Error' | |||
reply.internalServerError(err.message) | |||
@@ -1 +1,7 @@ | |||
export class DoubleDeletionError extends Error {} | |||
export class RingtoneNotFoundError extends Error {} | |||
export class InsufficientDataError extends Error {} | |||
export class OwnershipError extends Error {} | |||
export class UpdateDeletedRingtoneError extends Error {} |
@@ -2,102 +2,209 @@ import {inject, singleton} from 'tsyringe' | |||
import {models} from '@tonality/library-common' | |||
import Uuid, {UuidBuffer} from '@tonality/library-uuid'; | |||
import {PrismaClient} from '@prisma/client' | |||
import { | |||
InsufficientDataError, | |||
OwnershipError, | |||
RingtoneNotFoundError, | |||
UpdateDeletedRingtoneError, | |||
} from './response'; | |||
export default interface RingtoneService { | |||
get(id: Uuid): Promise<models.Ringtone> | |||
browseByComposer(composerUserId: Uuid, skip?: number, take?: number): Promise<models.Ringtone[]> | |||
browse(skip?: number, take?: number): Promise<models.Ringtone[]> | |||
search(q?: string): Promise<models.Ringtone[]> | |||
create(data: Partial<models.Ringtone>): Promise<models.Ringtone> | |||
update(data: Partial<models.Ringtone>): Promise<models.Ringtone> | |||
softDelete(id: Uuid): Promise<models.Ringtone> | |||
undoDelete(id: Uuid): Promise<models.Ringtone> | |||
hardDelete(id: Uuid): Promise<void> | |||
get(id: Uuid, requesterUserSub?: string): Promise<models.Ringtone> | |||
browseByComposer(composerUserSub: string, skip?: number, take?: number, requesterUserSub?: string): Promise<models.Ringtone[]> | |||
browse(skip?: number, take?: number, requesterUserSub?: string): Promise<models.Ringtone[]> | |||
search(q?: string, requesterUserSub?: string): Promise<models.Ringtone[]> | |||
create(data: Partial<models.Ringtone>, requesterUserSub: string): Promise<models.Ringtone> | |||
update(data: Partial<models.Ringtone>, requesterUserSub: string): Promise<models.Ringtone> | |||
softDelete(id: Uuid, requesterUserSub: string): Promise<models.Ringtone> | |||
undoDelete(id: Uuid, requesterUserSub: string): Promise<models.Ringtone> | |||
hardDelete(id: Uuid, requesterUserSub: string): Promise<void> | |||
} | |||
const serializeRingtone = d => { | |||
return ({ | |||
...d, | |||
composerUserId: Uuid.from(d.composerUserId).toString(), | |||
id: Uuid.from(d.id).toString(), | |||
}) | |||
} | |||
@singleton() | |||
export class RingtoneServiceImpl { | |||
export class RingtoneServiceImpl implements RingtoneService { | |||
constructor( | |||
@inject('PrismaClient') | |||
private readonly prismaClient: PrismaClient, | |||
) {} | |||
async browseByComposer(composerUserId: Uuid, skip: number = 0, take: number = 10): Promise<models.Ringtone[]> { | |||
const rawData = await this.prismaClient.ringtone.findMany({ | |||
where: { | |||
composer: { | |||
id: { | |||
equals: composerUserId, | |||
} | |||
} | |||
}, | |||
skip, | |||
take, | |||
}) | |||
async browseByComposer(composerUserSub: string, skip: number = 0, take: number = 10, requesterUserSub?: string): Promise<models.Ringtone[]> { | |||
let rawData | |||
if (typeof requesterUserSub === 'string' && composerUserSub === requesterUserSub) { | |||
rawData = await this.prismaClient.ringtone.findMany({ | |||
where: { | |||
composerUserSub: { | |||
equals: composerUserSub, | |||
}, | |||
}, | |||
skip, | |||
take, | |||
}) | |||
} else { | |||
rawData = await this.prismaClient.ringtone.findMany({ | |||
where: { | |||
composerUserSub: { | |||
equals: composerUserSub, | |||
}, | |||
deletedAt: { | |||
equals: null, | |||
}, | |||
}, | |||
skip, | |||
take, | |||
}) | |||
} | |||
return rawData.map(serializeRingtone) | |||
} | |||
async get(id: Uuid): Promise<models.Ringtone> { | |||
async get(id: Uuid, requesterUserSub?: string): Promise<models.Ringtone> { | |||
const ringtone = await this.prismaClient.ringtone.findUnique({ | |||
where: { | |||
id, | |||
id: (id as UuidBuffer).toBuffer(), | |||
} | |||
}) | |||
if (!ringtone) { | |||
throw new Error('Ringtone not found!') | |||
throw new RingtoneNotFoundError('Ringtone not found!') | |||
} | |||
if (ringtone.deletedAt instanceof Date) { | |||
if (typeof requesterUserSub !== 'string') { | |||
throw new RingtoneNotFoundError('Ringtone not found!') | |||
} | |||
if (requesterUserSub !== ringtone.composerUserSub) { | |||
throw new RingtoneNotFoundError('Ringtone not found!') | |||
} | |||
} | |||
return serializeRingtone(ringtone) | |||
} | |||
async browse(skip: number = 0, take: number = 10): Promise<models.Ringtone[]> { | |||
const rawData = await this.prismaClient.ringtone.findMany({ | |||
skip, | |||
take, | |||
}) | |||
async browse(skip: number = 0, take: number = 10, requesterUserSub?: string): Promise<models.Ringtone[]> { | |||
let rawData | |||
if (typeof requesterUserSub === 'string') { | |||
rawData = await this.prismaClient.ringtone.findMany({ | |||
where: { | |||
OR: [ | |||
{ | |||
deletedAt: { | |||
equals: null, | |||
}, | |||
}, | |||
{ | |||
composerUserSub: { | |||
equals: requesterUserSub, | |||
}, | |||
} | |||
], | |||
}, | |||
skip, | |||
take, | |||
}) | |||
} else { | |||
rawData = await this.prismaClient.ringtone.findMany({ | |||
where: { | |||
deletedAt: { | |||
equals: null, | |||
}, | |||
}, | |||
skip, | |||
take, | |||
}) | |||
} | |||
return rawData.map(serializeRingtone) | |||
} | |||
async search(q: string = ''): Promise<models.Ringtone[]> { | |||
return this.prismaClient.ringtone.findMany({ | |||
where: { | |||
name: { | |||
contains: q, | |||
async search(q: string = '', requesterUserSub?: string): Promise<models.Ringtone[]> { | |||
let rawData | |||
if (typeof requesterUserSub === 'string') { | |||
rawData = this.prismaClient.ringtone.findMany({ | |||
where: { | |||
OR: [ | |||
{ | |||
deletedAt: { | |||
equals: null, | |||
}, | |||
}, | |||
{ | |||
composerUserSub: { | |||
equals: requesterUserSub, | |||
}, | |||
}, | |||
], | |||
name: { | |||
contains: q, | |||
}, | |||
}, | |||
}, | |||
}) | |||
}) | |||
} else { | |||
rawData = this.prismaClient.ringtone.findMany({ | |||
where: { | |||
name: { | |||
contains: q, | |||
}, | |||
deletedAt: { | |||
equals: null, | |||
}, | |||
}, | |||
}) | |||
} | |||
return rawData.map(serializeRingtone) | |||
} | |||
async create(data: Partial<models.Ringtone>): Promise<models.Ringtone> { | |||
const { createdAt, updatedAt, deletedAt, composerUserId, name, ...safeData } = data | |||
async create(data: Partial<models.Ringtone>, requesterUserSub: string): Promise<models.Ringtone> { | |||
const { createdAt, updatedAt, deletedAt, name, composerUserSub, ...safeData } = data | |||
if (typeof composerUserSub !== 'string') { | |||
throw new InsufficientDataError('Composer user ID is required.') | |||
} | |||
if (composerUserSub !== requesterUserSub) { | |||
throw new OwnershipError('Creation is only possible with own ringtones.') | |||
} | |||
const rawData = await this.prismaClient.ringtone.create({ | |||
data: { | |||
...safeData, | |||
id: Uuid.new().toBuffer(), | |||
composerUserId: (composerUserId as UuidBuffer).toBuffer(), | |||
composerUserSub: requesterUserSub, | |||
name: name as string, | |||
}, | |||
}) | |||
return serializeRingtone(rawData) | |||
} | |||
async update(data: Partial<models.Ringtone>): Promise<models.Ringtone> { | |||
const { createdAt, updatedAt, deletedAt, ...safeData } = data | |||
async update(data: Partial<models.Ringtone>, requesterUserSub: string): Promise<models.Ringtone> { | |||
const { createdAt, updatedAt, deletedAt, id, ...safeData } = data | |||
const idBuffer = (id as UuidBuffer).toBuffer() | |||
const checkData = await this.prismaClient.ringtone.findUnique({ | |||
where: { | |||
id: idBuffer, | |||
}, | |||
}) | |||
if (!checkData) { | |||
throw new RingtoneNotFoundError('Ringtone does not exist.') | |||
} | |||
if (checkData.deletedAt instanceof Date) { | |||
throw new UpdateDeletedRingtoneError('Ringtone has been deleted.') | |||
} | |||
if (checkData.composerUserSub !== requesterUserSub) { | |||
throw new OwnershipError('Update is only possible with own ringtones.') | |||
} | |||
const rawData = await this.prismaClient.ringtone.update({ | |||
where: { | |||
id: (data.id as UuidBuffer).toBuffer(), | |||
id: idBuffer, | |||
}, | |||
data: { | |||
...safeData, | |||
@@ -107,10 +214,19 @@ export class RingtoneServiceImpl { | |||
return serializeRingtone(rawData) | |||
} | |||
async softDelete(id: Uuid): Promise<models.Ringtone> { | |||
async softDelete(id: Uuid, requesterUserSub: string): Promise<models.Ringtone> { | |||
const idBuffer = (id as UuidBuffer).toBuffer() | |||
const checkData = await this.prismaClient.ringtone.findUnique({ | |||
where: { | |||
id: idBuffer, | |||
}, | |||
}) | |||
if (checkData.composerUserSub !== requesterUserSub) { | |||
throw new OwnershipError('Soft deletion is only possible with own ringtones.') | |||
} | |||
const rawData = await this.prismaClient.ringtone.update({ | |||
where: { | |||
id, | |||
id: idBuffer, | |||
}, | |||
data: { | |||
deletedAt: new Date(), | |||
@@ -119,10 +235,19 @@ export class RingtoneServiceImpl { | |||
return serializeRingtone(rawData) | |||
} | |||
async undoDelete(id: Uuid): Promise<models.Ringtone> { | |||
async undoDelete(id: Uuid, requesterUserSub: string): Promise<models.Ringtone> { | |||
const idBuffer = (id as UuidBuffer).toBuffer() | |||
const checkData = await this.prismaClient.ringtone.findUnique({ | |||
where: { | |||
id: idBuffer, | |||
}, | |||
}) | |||
if (checkData.composerUserSub !== requesterUserSub) { | |||
throw new OwnershipError('Undo deletion is only possible with own ringtones.') | |||
} | |||
const rawData = this.prismaClient.ringtone.update({ | |||
where: { | |||
id, | |||
id: (id as UuidBuffer).toBuffer(), | |||
}, | |||
data: { | |||
deletedAt: null, | |||
@@ -131,10 +256,25 @@ export class RingtoneServiceImpl { | |||
return serializeRingtone(rawData) | |||
} | |||
async hardDelete(id: Uuid): Promise<void> { | |||
async hardDelete(id: Uuid, requesterUserSub: string): Promise<void> { | |||
const idBuffer = (id as UuidBuffer).toBuffer() | |||
const checkData = await this.prismaClient.ringtone.findUnique({ | |||
where: { | |||
id: idBuffer, | |||
}, | |||
}) | |||
if (!checkData) { | |||
throw new RingtoneNotFoundError('Ringtone does not exist before deletion.') | |||
} | |||
if (checkData.composerUserSub !== requesterUserSub) { | |||
throw new OwnershipError('Hard deletion is only possible with own ringtones.') | |||
} | |||
await this.prismaClient.ringtone.delete({ | |||
where: { | |||
id, | |||
id: idBuffer, | |||
}, | |||
}) | |||
} | |||
@@ -1,6 +0,0 @@ | |||
import {DependencyContainer} from 'tsyringe'; | |||
import {UserServiceImpl} from './service'; | |||
export default (container: DependencyContainer) => { | |||
container.register('UserService', { useClass: UserServiceImpl }) | |||
} |
@@ -1,162 +0,0 @@ | |||
import {inject, singleton} from 'tsyringe' | |||
import {PrismaClient} from '@prisma/client' | |||
import { models } from '@tonality/library-common' | |||
import Uuid from '@tonality/library-uuid' | |||
import PasswordService from '../password/service' | |||
export default interface UserService { | |||
get(id: Uuid): Promise<models.UserProfile> | |||
getByUsername(username: string): Promise<models.User> | |||
getProfileByUsername(username: string): Promise<models.UserProfile> | |||
browse(skip?: number, take?: number): Promise<models.UserProfile[]> | |||
search(q?: string): Promise<models.UserProfile[]> | |||
create(profile: Partial<models.UserProfile>, username: string, newPassword: string, confirmNewPassword: string): Promise<models.UserProfile> | |||
updateProfile(profile: Partial<models.UserProfile>): Promise<models.UserProfile> | |||
updatePassword(id: Uuid, oldPassword: string, newPassword: string, confirmNewPassword: string): Promise<void> | |||
delete(id: Uuid): Promise<void> | |||
} | |||
@singleton() | |||
export class UserServiceImpl implements UserService { | |||
constructor( | |||
@inject('PrismaClient') | |||
private readonly prismaClient: PrismaClient, | |||
@inject('PasswordService') | |||
private readonly passwordService: PasswordService | |||
) {} | |||
async get(id: Uuid): Promise<models.UserProfile> { | |||
const user = await this.prismaClient.userProfile.findUnique({ | |||
where: { | |||
userId: id, | |||
}, | |||
}) | |||
if (!user) { | |||
throw new Error('User not found!') | |||
} | |||
return user | |||
} | |||
async getByUsername(username: string): Promise<models.User> { | |||
const user = await this.prismaClient.user.findUnique({ | |||
where: { | |||
username, | |||
}, | |||
}) | |||
if (!user) { | |||
throw new Error('User not found!') | |||
} | |||
return user | |||
} | |||
async getProfileByUsername(username: string): Promise<models.UserProfile> { | |||
const user = await this.prismaClient.userProfile.findFirst({ | |||
where: { | |||
user: { | |||
username, | |||
}, | |||
}, | |||
}) | |||
if (!user) { | |||
throw new Error('User not found!') | |||
} | |||
return user | |||
} | |||
async browse(skip: number = 0, take: number = 10): Promise<models.UserProfile[]> { | |||
return this.prismaClient.userProfile.findMany({ | |||
skip, | |||
take, | |||
}) | |||
} | |||
async search(q: string = ''): Promise<models.UserProfile[]> { | |||
return this.prismaClient.userProfile.findMany({ | |||
where: { | |||
user: { | |||
username: { | |||
contains: q, | |||
}, | |||
}, | |||
}, | |||
}) | |||
} | |||
async create(profile: Partial<models.UserProfile>, username: string, newPassword: string, confirmNewPassword: string): Promise<models.UserProfile> { | |||
if (newPassword !== confirmNewPassword) { | |||
throw new Error('Passwords do not match!') | |||
} | |||
const newUserId = Uuid.new() | |||
const password = await this.passwordService.hash(newPassword) | |||
await this.prismaClient.user.create({ | |||
data: { | |||
id: newUserId, | |||
username, | |||
password, | |||
}, | |||
}) | |||
return this.prismaClient.userProfile.create({ | |||
data: { | |||
...profile, | |||
userId: newUserId, | |||
}, | |||
}) | |||
} | |||
async updateProfile(profile: Partial<models.UserProfile>): Promise<models.UserProfile> { | |||
const { userId, ...etcProfile } = profile | |||
return this.prismaClient.userProfile.update({ | |||
where: { | |||
userId: userId as Uuid, | |||
}, | |||
data: etcProfile, | |||
}) | |||
} | |||
async updatePassword(id: Uuid, oldPassword: string, newPassword: string, confirmNewPassword: string): Promise<void> { | |||
const user = await this.prismaClient.user.findUnique({ | |||
where: { | |||
id, | |||
}, | |||
}) | |||
if (!user) { | |||
throw new Error('Invalid request!') | |||
} | |||
const oldPasswordsMatch = await this.passwordService.compare(user.password, oldPassword) | |||
if (!oldPasswordsMatch) { | |||
throw new Error('Invalid request!') | |||
} | |||
if (newPassword !== confirmNewPassword) { | |||
throw new Error('Passwords do not match!') | |||
} | |||
const password = await this.passwordService.hash(newPassword) | |||
await this.prismaClient.user.update({ | |||
where: { | |||
id, | |||
}, | |||
data: { | |||
password, | |||
}, | |||
}) | |||
} | |||
async delete(id: Uuid): Promise<void> { | |||
await this.prismaClient.user.delete({ | |||
where: { | |||
id, | |||
}, | |||
}) | |||
} | |||
} |
@@ -1,14 +0,0 @@ | |||
import {FastifyPluginAsync} from 'fastify' | |||
import {container} from 'tsyringe' | |||
import AuthController from '../../../modules/auth/controller' | |||
const auth: FastifyPluginAsync = async (fastify): Promise<void> => { | |||
const ringtoneController = container.resolve<AuthController>('AuthController') | |||
fastify.route({ | |||
url: '/log-in', | |||
method: 'POST', | |||
handler: ringtoneController.logIn, | |||
}) | |||
} | |||
export default auth |