浏览代码

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

正在加载...
取消
保存