소스 검색

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
부모
커밋
5251cbbe54
28개의 변경된 파일453개의 추가작업 그리고 546개의 파일을 삭제
  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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

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

불러오는 중...
취소
저장