|  | @@ -1,14 +1,15 @@
 | 
	
		
			
				|  |  |  import * as v from '../validate'
 | 
	
		
			
				|  |  | -import type { Account } from './types'
 | 
	
		
			
				|  |  | +import type { AuthRequestHandler } from '../auth'
 | 
	
		
			
				|  |  |  import type { Context } from '../types'
 | 
	
		
			
				|  |  |  import { ObjectId } from 'mongodb'
 | 
	
		
			
				|  |  |  import type { RequestHandler } from 'express'
 | 
	
		
			
				|  |  |  import type { WithId } from 'mongodb'
 | 
	
		
			
				|  |  | -import { sendBadRequest, sendNotFound } from '../http'
 | 
	
		
			
				|  |  | +import type { Account, AccountCreate, AccountUpdate } from './types'
 | 
	
		
			
				|  |  | +import { sendBadRequest, sendForbidden, sendNotFound, sendUnauthorized } from '../http'
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  export function createAccount({ model }: Context): RequestHandler {
 | 
	
		
			
				|  |  |    interface RequestData {
 | 
	
		
			
				|  |  | -    account: Account
 | 
	
		
			
				|  |  | +    account: AccountCreate
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    interface ResponseData {
 | 
	
	
		
			
				|  | @@ -18,6 +19,7 @@ export function createAccount({ model }: Context): RequestHandler {
 | 
	
		
			
				|  |  |    const readRequestData = v.validate<RequestData>({
 | 
	
		
			
				|  |  |      account: {
 | 
	
		
			
				|  |  |        email: v.email,
 | 
	
		
			
				|  |  | +      password: v.seq(v.minLength(8)),
 | 
	
		
			
				|  |  |      },
 | 
	
		
			
				|  |  |    })
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -26,6 +28,7 @@ export function createAccount({ model }: Context): RequestHandler {
 | 
	
		
			
				|  |  |        const input = readRequestData(req.body)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        const account = await model.account.create(input.account)
 | 
	
		
			
				|  |  | +      if (!account) return sendNotFound(res, next, { reason: 'unexpectedly failed to get new account' })
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        const output: ResponseData = { account }
 | 
	
		
			
				|  |  |        res.send(output)
 | 
	
	
		
			
				|  | @@ -41,15 +44,17 @@ export function createAccount({ model }: Context): RequestHandler {
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -export function deleteAccount({ model }: Context): RequestHandler {
 | 
	
		
			
				|  |  | +export function deleteAccount({ model }: Context): AuthRequestHandler {
 | 
	
		
			
				|  |  |    interface ResponseData {
 | 
	
		
			
				|  |  |      account: WithId<Account>
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    return async function (req, res, next) {
 | 
	
		
			
				|  |  | -    /** @todo get ID from authentication */
 | 
	
		
			
				|  |  | -    const id = req.params.id
 | 
	
		
			
				|  |  | +    if (!req.verified) return sendUnauthorized(res, next)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const id = req.params.id || req.accountId
 | 
	
		
			
				|  |  |      if (!id) return sendBadRequest(res, next, { reason: 'no ID' })
 | 
	
		
			
				|  |  | +    if (!req.accountId?.equals(id)) return sendForbidden(res, next)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      try {
 | 
	
		
			
				|  |  |        /** @todo delete related data */
 | 
	
	
		
			
				|  | @@ -65,15 +70,17 @@ export function deleteAccount({ model }: Context): RequestHandler {
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -export function getAccount({ model }: Context): RequestHandler {
 | 
	
		
			
				|  |  | +export function getAccount({ model }: Context): AuthRequestHandler {
 | 
	
		
			
				|  |  |    interface ResponseData {
 | 
	
		
			
				|  |  |      account: WithId<Account>
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    return async function (req, res, next) {
 | 
	
		
			
				|  |  | -    /** @todo get ID from authentication */
 | 
	
		
			
				|  |  | -    const id = req.params.id
 | 
	
		
			
				|  |  | +    if (!req.verified) return sendUnauthorized(res, next)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const id = req.params.id || req.accountId
 | 
	
		
			
				|  |  |      if (!id) return sendBadRequest(res, next, { reason: 'no ID' })
 | 
	
		
			
				|  |  | +    if (!req.accountId?.equals(id)) return sendForbidden(res, next)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      try {
 | 
	
		
			
				|  |  |        const account = await model.account.collection.findOne({ _id: new ObjectId(id) })
 | 
	
	
		
			
				|  | @@ -88,9 +95,52 @@ export function getAccount({ model }: Context): RequestHandler {
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -export function updateAccount({ model }: Context): RequestHandler {
 | 
	
		
			
				|  |  | +export function loginAccount({ auth, model }: Context): RequestHandler {
 | 
	
		
			
				|  |  |    interface RequestData {
 | 
	
		
			
				|  |  | -    account: Partial<Account>
 | 
	
		
			
				|  |  | +    account: Pick<Account, 'email' | 'password'>
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  interface ResponseData {
 | 
	
		
			
				|  |  | +    token: string
 | 
	
		
			
				|  |  | +    account: WithId<Account>
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const readRequestData = v.validate<RequestData>({
 | 
	
		
			
				|  |  | +    account: {
 | 
	
		
			
				|  |  | +      email: v.email,
 | 
	
		
			
				|  |  | +      password: v.seq(v.minLength(8)),
 | 
	
		
			
				|  |  | +    },
 | 
	
		
			
				|  |  | +  })
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  return async function (req, res, next) {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const input = readRequestData(req.body)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const account = await model.account.collection.findOne({ email: input.account.email })
 | 
	
		
			
				|  |  | +      if (!account) return sendNotFound(res, next)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const password = model.account.hashPassword(input.account.password, account.passwordSalt)
 | 
	
		
			
				|  |  | +      if (password !== account.password) return sendBadRequest(res, next, { reason: 'invalid password' })
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const token = await auth.sign(account._id)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const output: ResponseData = { token, account }
 | 
	
		
			
				|  |  | +      res.send(output)
 | 
	
		
			
				|  |  | +      next()
 | 
	
		
			
				|  |  | +    } catch (err) {
 | 
	
		
			
				|  |  | +      const name = (err as Error).name
 | 
	
		
			
				|  |  | +      if (name === 'ValidateError') {
 | 
	
		
			
				|  |  | +        const ve = err as v.ValidateError
 | 
	
		
			
				|  |  | +        return sendBadRequest(res, next, { param: ve.param, reason: ve.message })
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      return next(err)
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +export function updateAccount({ model }: Context): AuthRequestHandler {
 | 
	
		
			
				|  |  | +  interface RequestData {
 | 
	
		
			
				|  |  | +    account: AccountUpdate
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    interface ResponseData {
 | 
	
	
		
			
				|  | @@ -100,25 +150,24 @@ export function updateAccount({ model }: Context): RequestHandler {
 | 
	
		
			
				|  |  |    const readRequestData = v.validate<RequestData>({
 | 
	
		
			
				|  |  |      account: {
 | 
	
		
			
				|  |  |        email: v.seq(v.optional, v.email),
 | 
	
		
			
				|  |  | +      password: v.seq(v.optional, v.minLength(8)),
 | 
	
		
			
				|  |  |      },
 | 
	
		
			
				|  |  |    })
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    return async function (req, res, next) {
 | 
	
		
			
				|  |  | -    /** @todo get ID from authentication */
 | 
	
		
			
				|  |  | -    const id = req.params.id
 | 
	
		
			
				|  |  | +    if (!req.verified) return sendUnauthorized(res, next)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const id = req.params.id || req.accountId
 | 
	
		
			
				|  |  |      if (!id) return sendBadRequest(res, next, { reason: 'no ID' })
 | 
	
		
			
				|  |  | +    if (!req.accountId?.equals(id)) return sendForbidden(res, next)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      try {
 | 
	
		
			
				|  |  |        const input = readRequestData(req.body)
 | 
	
		
			
				|  |  | -      if (!input.account.email) {
 | 
	
		
			
				|  |  | -        return sendBadRequest(res, next, { reason: 'no data' })
 | 
	
		
			
				|  |  | +      if (!input.account.email && !input.account.password) {
 | 
	
		
			
				|  |  | +        return sendBadRequest(res, next, { reason: 'no changes' })
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -      const account = await model.account.collection.findOneAndUpdate(
 | 
	
		
			
				|  |  | -        { _id: new ObjectId(id) },
 | 
	
		
			
				|  |  | -        { $set: input.account },
 | 
	
		
			
				|  |  | -        { returnDocument: 'after' },
 | 
	
		
			
				|  |  | -      )
 | 
	
		
			
				|  |  | +      const account = await model.account.update(id, input.account)
 | 
	
		
			
				|  |  |        if (!account) return sendNotFound(res, next)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        const output: ResponseData = { account }
 |