Sfoglia il codice sorgente

Integrate front-end to back-end

The creation and update features of the app has been implemented.
Redundant user handling has been removed and is wholly managed by
Auth0.
master
parent
commit
5251cbbe54
28 ha cambiato i file con 453 aggiunte e 546 eliminazioni
  1. +2
    -2
      REQUIREMENTS.md
  2. +0
    -2
      SAMPLES.md
  3. +6
    -0
      SAMPLES.yml
  4. +0
    -1
      packages/app-web/.env.example
  5. +10
    -21
      packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx
  6. +5
    -5
      packages/app-web/src/components/templates/CreateRingtone/index.tsx
  7. +33
    -4
      packages/app-web/src/modules/ringtone/client.ts
  8. +0
    -3
      packages/app-web/src/modules/ringtone/endpoints.ts
  9. +93
    -0
      packages/app-web/src/pages/my/create/ringtones/[id].tsx
  10. +20
    -29
      packages/app-web/src/pages/my/create/ringtones/index.tsx
  11. +1
    -17
      packages/library-common/src/index.ts
  12. +3
    -3
      packages/library-uuid/src/index.ts
  13. +9
    -29
      packages/service-core/prisma/schema.prisma
  14. +0
    -4
      packages/service-core/src/app.ts
  15. +0
    -30
      packages/service-core/src/modules/auth/controller.ts
  16. +0
    -8
      packages/service-core/src/modules/auth/index.ts
  17. +0
    -37
      packages/service-core/src/modules/auth/service.ts
  18. +0
    -6
      packages/service-core/src/modules/credentials/index.ts
  19. +0
    -34
      packages/service-core/src/modules/credentials/service.ts
  20. +0
    -6
      packages/service-core/src/modules/credentials/type.ts
  21. +0
    -6
      packages/service-core/src/modules/password/index.ts
  22. +0
    -24
      packages/service-core/src/modules/password/service.ts
  23. +73
    -41
      packages/service-core/src/modules/ringtone/controller.ts
  24. +7
    -1
      packages/service-core/src/modules/ringtone/response.ts
  25. +191
    -51
      packages/service-core/src/modules/ringtone/service.ts
  26. +0
    -6
      packages/service-core/src/modules/user/index.ts
  27. +0
    -162
      packages/service-core/src/modules/user/service.ts
  28. +0
    -14
      packages/service-core/src/routes/api/auth/index.ts

+ 2
- 2
REQUIREMENTS.md Vedi File

@@ -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.


+ 0
- 2
SAMPLES.md Vedi File

@@ -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.

+ 6
- 0
SAMPLES.yml Vedi File

@@ -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

+ 0
- 1
packages/app-web/.env.example Vedi File

@@ -7,4 +7,3 @@ AUTH0_CLIENT_ID=
AUTH0_CLIENT_SECRET=
CYPRESS_USERNAME=
CYPRESS_PASSWORD=
AUTH0_MGMT_API_TOKEN=

+ 10
- 21
packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx Vedi File

@@ -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'}


+ 5
- 5
packages/app-web/src/components/templates/CreateRingtone/index.tsx Vedi File

@@ -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={{


+ 33
- 4
packages/app-web/src/modules/ringtone/client.ts Vedi File

@@ -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
}
}
}

+ 0
- 3
packages/app-web/src/modules/ringtone/endpoints.ts Vedi File

@@ -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}`
},
})



+ 93
- 0
packages/app-web/src/pages/my/create/ringtones/[id].tsx Vedi File

@@ -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

packages/app-web/src/pages/my/create/ringtone/index.tsx → packages/app-web/src/pages/my/create/ringtones/index.tsx Vedi File

@@ -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
- 17
packages/library-common/src/index.ts Vedi File

@@ -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
}
}

+ 3
- 3
packages/library-uuid/src/index.ts Vedi File

@@ -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
}



+ 9
- 29
packages/service-core/prisma/schema.prisma Vedi File

@@ -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")
}
}

+ 0
- 4
packages/service-core/src/app.ts Vedi File

@@ -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'),
])



+ 0
- 30
packages/service-core/src/modules/auth/controller.ts Vedi File

@@ -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()
}
}
}

+ 0
- 8
packages/service-core/src/modules/auth/index.ts Vedi File

@@ -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 })
}

+ 0
- 37
packages/service-core/src/modules/auth/service.ts Vedi File

@@ -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
}
}

+ 0
- 6
packages/service-core/src/modules/credentials/index.ts Vedi File

@@ -1,6 +0,0 @@
import {DependencyContainer} from 'tsyringe';
import {CredentialsServiceImpl} from './service';

export default (container: DependencyContainer) => {
container.register('CredentialsService', { useClass: CredentialsServiceImpl })
}

+ 0
- 34
packages/service-core/src/modules/credentials/service.ts Vedi File

@@ -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,
}
}
}

+ 0
- 6
packages/service-core/src/modules/credentials/type.ts Vedi File

@@ -1,6 +0,0 @@
export default interface Credentials {
[k: string]: string | number | unknown,
accessToken: string,
expiresIn: number,
tokenType: string,
}

+ 0
- 6
packages/service-core/src/modules/password/index.ts Vedi File

@@ -1,6 +0,0 @@
import {DependencyContainer} from 'tsyringe';
import {PasswordServiceImpl} from './service';

export default (container: DependencyContainer) => {
container.register('PasswordService', { useClass: PasswordServiceImpl })
}

+ 0
- 24
packages/service-core/src/modules/password/service.ts Vedi File

@@ -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)
}
}

+ 73
- 41
packages/service-core/src/modules/ringtone/controller.ts Vedi File

@@ -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)


+ 7
- 1
packages/service-core/src/modules/ringtone/response.ts Vedi File

@@ -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 {}

+ 191
- 51
packages/service-core/src/modules/ringtone/service.ts Vedi File

@@ -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,
},
})
}


+ 0
- 6
packages/service-core/src/modules/user/index.ts Vedi File

@@ -1,6 +0,0 @@
import {DependencyContainer} from 'tsyringe';
import {UserServiceImpl} from './service';

export default (container: DependencyContainer) => {
container.register('UserService', { useClass: UserServiceImpl })
}

+ 0
- 162
packages/service-core/src/modules/user/service.ts Vedi File

@@ -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,
},
})
}
}

+ 0
- 14
packages/service-core/src/routes/api/auth/index.ts Vedi File

@@ -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

Caricamento…
Annulla
Salva