@design-edito/cli 0.0.78 → 0.0.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/assets/list.txt +1 -2
- package/cli/assets/version.txt +1 -1
- package/make-template/assets/express/esbuild.config.js +19 -0
- package/make-template/assets/express/package.json +16 -12
- package/make-template/assets/express/src/index.ts +13 -49
- package/make-template/assets/express/src/public/index.css +7 -0
- package/make-template/assets/express/src/public/index.html +15 -0
- package/make-template/assets/express/src/public/index.js +31 -0
- package/make-template/assets/express/src/tsconfig.json +7 -3
- package/make-template/assets/express/src/www/index.ts +42 -0
- package/make-template/assets/express-api/Dockerfile +18 -0
- package/make-template/assets/express-api/Dockerfile.dev +8 -0
- package/make-template/assets/express-api/env +36 -0
- package/make-template/assets/express-api/esbuild.config.js +26 -0
- package/make-template/assets/express-api/gitignore +214 -0
- package/make-template/assets/express-api/package.json +60 -0
- package/make-template/assets/express-api/src/api/auth/_utils/index.ts +47 -0
- package/make-template/assets/express-api/src/api/auth/index.ts +25 -0
- package/make-template/assets/express-api/src/api/auth/login/index.ts +101 -0
- package/make-template/assets/express-api/src/api/auth/logout/index.ts +45 -0
- package/make-template/assets/express-api/src/api/auth/refresh-token/index.ts +54 -0
- package/make-template/assets/express-api/src/api/auth/request-email-verification-token/index.ts +45 -0
- package/make-template/assets/express-api/src/api/auth/request-new-password/index.ts +62 -0
- package/make-template/assets/express-api/src/api/auth/signup/index.ts +99 -0
- package/make-template/assets/express-api/src/api/auth/submit-new-password/index.ts +61 -0
- package/make-template/assets/express-api/src/api/auth/verify-email/index.ts +79 -0
- package/make-template/assets/express-api/src/api/auth/whoami/index.ts +68 -0
- package/make-template/assets/express-api/src/api/index.ts +18 -0
- package/make-template/assets/express-api/src/api/types.ts +76 -0
- package/make-template/assets/express-api/src/api/utils.ts +146 -0
- package/make-template/assets/express-api/src/database/index.ts +173 -0
- package/make-template/assets/express-api/src/email/index.ts +47 -0
- package/make-template/assets/express-api/src/env/index.ts +77 -0
- package/make-template/assets/express-api/src/errors/index.ts +128 -0
- package/make-template/assets/express-api/src/index.ts +42 -0
- package/make-template/assets/express-api/src/init/index.ts +156 -0
- package/make-template/assets/express-api/src/jwt/index.ts +105 -0
- package/make-template/assets/express-api/src/public/index.css +7 -0
- package/make-template/assets/express-api/src/public/index.html +15 -0
- package/make-template/assets/express-api/src/public/index.js +31 -0
- package/make-template/assets/express-api/src/schema/_history/index.ts +124 -0
- package/make-template/assets/express-api/src/schema/_meta/index.ts +113 -0
- package/make-template/assets/express-api/src/schema/index.ts +17 -0
- package/make-template/assets/express-api/src/schema/user/index.ts +117 -0
- package/make-template/assets/express-api/src/schema/user-email-validation-token/index.ts +20 -0
- package/make-template/assets/express-api/src/schema/user-password-renewal-token/index.ts +20 -0
- package/make-template/assets/express-api/src/schema/user-revoked-auth-token/index.ts +26 -0
- package/make-template/assets/express-api/src/tsconfig.json +16 -0
- package/make-template/assets/express-api/src/www/index.ts +43 -0
- package/make-template/index.js +2 -3
- package/make-template/index.js.map +3 -3
- package/package.json +7 -8
- package/make-template/assets/express/src/routes/index.ts +0 -7
- package/upgrade/index.js +0 -12
- package/upgrade/index.js.map +0 -7
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Outcome } from '@design-edito/tools/agnostic/misc/outcome'
|
|
2
|
+
import * as Database from '../../../database'
|
|
3
|
+
import { ROOT_USER_ID } from '../../../env'
|
|
4
|
+
import { Codes, makeFailureOutcome } from '../../../errors'
|
|
5
|
+
import { BaseUserModel, Role, Status } from '../../../schema/user'
|
|
6
|
+
import { UserRevokedTokenModel } from '../../../schema/user-revoked-auth-token'
|
|
7
|
+
import { makeEndpoint, nullAuthenticator, nullValidator } from '../../utils'
|
|
8
|
+
|
|
9
|
+
type BaseUserData = {
|
|
10
|
+
_id: string
|
|
11
|
+
username: string
|
|
12
|
+
role: Role
|
|
13
|
+
status: Status
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type LocalUserData = BaseUserData & {
|
|
17
|
+
email: string
|
|
18
|
+
verified: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type GoogleUserData = BaseUserData & {
|
|
22
|
+
googleId: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SuccessResponse = LocalUserData | GoogleUserData
|
|
26
|
+
|
|
27
|
+
export const whoami = makeEndpoint<{}, SuccessResponse>(
|
|
28
|
+
nullAuthenticator,
|
|
29
|
+
nullValidator,
|
|
30
|
+
async (_body, _req, res) => {
|
|
31
|
+
const { accessTokenSigned, accessTokenPayload } = res.locals
|
|
32
|
+
// [WIP] probably better to do this in the authenticator ?
|
|
33
|
+
// Then assume res.locals are not undefined here ?
|
|
34
|
+
if (accessTokenPayload === undefined || accessTokenSigned === undefined) return makeFailureOutcome(Codes.USER_NOT_AUTHENTICATED, 'Access token has been revoked')
|
|
35
|
+
const tokenUserId = accessTokenPayload.userId
|
|
36
|
+
const foundRevokedToken = await Database.findOne(UserRevokedTokenModel, { value: accessTokenSigned }, { initiatorId: ROOT_USER_ID })
|
|
37
|
+
if (foundRevokedToken.success) return makeFailureOutcome(Codes.USER_NOT_AUTHENTICATED, 'Access token has been revoked')
|
|
38
|
+
const userLookup = await Database.findOne(BaseUserModel, { _id: tokenUserId }, { initiatorId: ROOT_USER_ID })
|
|
39
|
+
if (!userLookup.success) return userLookup
|
|
40
|
+
const { _id, username, role, status } = userLookup.payload
|
|
41
|
+
if (
|
|
42
|
+
'email' in userLookup.payload
|
|
43
|
+
&& 'verified' in userLookup.payload) {
|
|
44
|
+
const email = userLookup.payload.email as string
|
|
45
|
+
const verified = userLookup.payload.verified as boolean
|
|
46
|
+
return Outcome.makeSuccess({
|
|
47
|
+
_id: _id.toString(),
|
|
48
|
+
username,
|
|
49
|
+
email,
|
|
50
|
+
role,
|
|
51
|
+
status,
|
|
52
|
+
verified
|
|
53
|
+
})
|
|
54
|
+
} else if ('googleId' in userLookup.payload) {
|
|
55
|
+
const googleId = userLookup.payload.googleId as string
|
|
56
|
+
return Outcome.makeSuccess({
|
|
57
|
+
_id: _id.toString(),
|
|
58
|
+
username,
|
|
59
|
+
googleId,
|
|
60
|
+
role,
|
|
61
|
+
status
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
// [WIP] what do here ? Send an email to the admin ?
|
|
65
|
+
// probably better not to inform the end user
|
|
66
|
+
return makeFailureOutcome(Codes.USER_DOES_NOT_EXIST, _id.toString())
|
|
67
|
+
}
|
|
68
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as Auth from './auth'
|
|
2
|
+
|
|
3
|
+
export const Endpoints = {
|
|
4
|
+
|
|
5
|
+
// Auth
|
|
6
|
+
'POST:/auth/signup': Auth.signup, // ok
|
|
7
|
+
'POST:/auth/verify-email/': Auth.verifyEmail, // ok
|
|
8
|
+
'POST:/auth/request-email-verification-token': Auth.requestEmailVerificationToken, // ok
|
|
9
|
+
'POST:/auth/login': Auth.login, // ok
|
|
10
|
+
'GET:/auth/refresh-token': Auth.refreshToken,
|
|
11
|
+
'GET:/auth/logout': Auth.logout, // ok
|
|
12
|
+
'POST:/auth/request-new-password': Auth.requestNewPassword, // ok
|
|
13
|
+
'POST:/auth/submit-new-password': Auth.submitNewPassword, // ok
|
|
14
|
+
// 'GET:/auth/google': Auth.google,
|
|
15
|
+
// 'GET:/auth/google-callback': Auth.googleCallback,
|
|
16
|
+
'GET:/auth/whoami': Auth.whoami // ok
|
|
17
|
+
|
|
18
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Request, Response, RequestHandler } from 'express'
|
|
2
|
+
import { Outcome } from '@design-edito/tools/agnostic/misc/outcome'
|
|
3
|
+
import { Codes, ErrorData } from '../errors'
|
|
4
|
+
import { Endpoints } from '.'
|
|
5
|
+
|
|
6
|
+
type AsyncOrNot<T> = T | Promise<T>
|
|
7
|
+
|
|
8
|
+
export type Authenticator = (req: Request, res: Response) => AsyncOrNot<Outcome.Either<true, ErrorData<Codes.USER_NOT_AUTHENTICATED | Codes.USER_NOT_AUTHORIZED | Codes.USER_DOES_NOT_EXIST>>>
|
|
9
|
+
|
|
10
|
+
export type Validator<
|
|
11
|
+
ReqBody extends object = {},
|
|
12
|
+
Code extends Codes = Codes // [WIP] Maybe only Codes.INVALID_REQUEST_BODY here? Or split Codes into RequestValidatonErrorCodes and RequestProcessingErrorCodes
|
|
13
|
+
> = (req: Request, res: Response) => AsyncOrNot<Outcome.Either<ReqBody, ErrorData<Code>>>
|
|
14
|
+
|
|
15
|
+
export type Processor<
|
|
16
|
+
ReqBody extends object = {},
|
|
17
|
+
Output extends object = {},
|
|
18
|
+
Code extends Codes = Codes
|
|
19
|
+
> = (body: ReqBody, req: Request, res: Response) => AsyncOrNot<Outcome.Either<Output, ErrorData<Code>>>
|
|
20
|
+
|
|
21
|
+
export type ResponseMetaData = {
|
|
22
|
+
userId: string | null
|
|
23
|
+
requestId: string
|
|
24
|
+
timestamp: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SuccessResponseBody<Output extends object = {}> = Outcome.Success<Output> & {
|
|
28
|
+
meta: ResponseMetaData
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type FailureResponseBody<
|
|
32
|
+
ValidationErrCode extends Codes = Codes,
|
|
33
|
+
ProcessingErrCode extends Codes = Codes
|
|
34
|
+
> = Outcome.Failure<ErrorData<ValidationErrCode | ProcessingErrCode | Codes.USER_NOT_AUTHENTICATED | Codes.USER_NOT_AUTHORIZED | Codes.USER_DOES_NOT_EXIST | Codes.UNKNOWN_ERROR>> & {
|
|
35
|
+
meta: ResponseMetaData
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ResponseBody<
|
|
39
|
+
Output extends object = {},
|
|
40
|
+
ValidationErrCode extends Codes = Codes,
|
|
41
|
+
ProcessingErrCode extends Codes = Codes
|
|
42
|
+
> = SuccessResponseBody<Output> | FailureResponseBody<ValidationErrCode, ProcessingErrCode>
|
|
43
|
+
|
|
44
|
+
export type Handler<
|
|
45
|
+
Output extends object = {},
|
|
46
|
+
ValidationErrCode extends Codes = Codes,
|
|
47
|
+
ProcessingErrCode extends Codes = Codes
|
|
48
|
+
> = RequestHandler<{}, ResponseBody<Output, ValidationErrCode, ProcessingErrCode>, unknown>
|
|
49
|
+
|
|
50
|
+
export type Endpoint<
|
|
51
|
+
ReqBody extends object = {},
|
|
52
|
+
Output extends object = {},
|
|
53
|
+
ValidationErrCodes extends Codes = Codes,
|
|
54
|
+
ProcessingErrCodes extends Codes = Codes
|
|
55
|
+
> = {
|
|
56
|
+
authenticator: Authenticator
|
|
57
|
+
validator: Validator<ReqBody, ValidationErrCodes>
|
|
58
|
+
processor: Processor<ReqBody, Output, ProcessingErrCodes>
|
|
59
|
+
handler: Handler<Output, ValidationErrCodes, ProcessingErrCodes>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type ExtractReqBody<T extends keyof typeof Endpoints> = typeof Endpoints[T] extends Endpoint<
|
|
63
|
+
infer ReqBody,
|
|
64
|
+
any,
|
|
65
|
+
any,
|
|
66
|
+
any
|
|
67
|
+
> ? ReqBody
|
|
68
|
+
: never
|
|
69
|
+
|
|
70
|
+
export type ExtractResBody<T extends keyof typeof Endpoints> = typeof Endpoints[T] extends Endpoint<
|
|
71
|
+
any,
|
|
72
|
+
infer Output,
|
|
73
|
+
infer ValidationErrCode,
|
|
74
|
+
infer ProcessingErrCode
|
|
75
|
+
> ? ResponseBody<Output, ValidationErrCode, ProcessingErrCode>
|
|
76
|
+
: never
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { RequestHandler, Router } from 'express'
|
|
3
|
+
import { Outcome } from '@design-edito/tools/agnostic/misc/outcome'
|
|
4
|
+
import { unknownToString } from '@design-edito/tools/agnostic/errors/unknown-to-string'
|
|
5
|
+
import { Codes, register } from '../errors'
|
|
6
|
+
import {
|
|
7
|
+
Validator,
|
|
8
|
+
Processor,
|
|
9
|
+
Handler,
|
|
10
|
+
FailureResponseBody,
|
|
11
|
+
SuccessResponseBody,
|
|
12
|
+
Endpoint,
|
|
13
|
+
Authenticator
|
|
14
|
+
} from './types'
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
namespace Express {
|
|
18
|
+
interface Locals {
|
|
19
|
+
requestId?: string
|
|
20
|
+
requestTimestamp?: number
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeHandler <
|
|
26
|
+
ReqBody extends object = {},
|
|
27
|
+
Output extends object = {},
|
|
28
|
+
ValidationErrCodes extends Codes = Codes,
|
|
29
|
+
ProcessingErrCodes extends Codes = Codes
|
|
30
|
+
>(authenticator: Authenticator,
|
|
31
|
+
validator: Validator<ReqBody, ValidationErrCodes>,
|
|
32
|
+
processor: Processor<ReqBody, Output, ProcessingErrCodes>
|
|
33
|
+
): Handler<Output, ValidationErrCodes, ProcessingErrCodes> {
|
|
34
|
+
const handler: Handler<Output, ValidationErrCodes, ProcessingErrCodes> = async (req, res) => {
|
|
35
|
+
const requestMeta = {
|
|
36
|
+
// This only works because Jwt.authenticate is used before this
|
|
37
|
+
userId: res.locals.accessTokenPayload?.userId ?? null,
|
|
38
|
+
requestId: randomUUID(),
|
|
39
|
+
timestamp: Date.now()
|
|
40
|
+
}
|
|
41
|
+
res.locals.requestId = requestMeta.requestId
|
|
42
|
+
res.locals.requestTimestamp = requestMeta.timestamp
|
|
43
|
+
try {
|
|
44
|
+
const authenticated = await authenticator(req, res)
|
|
45
|
+
if (!authenticated.success) {
|
|
46
|
+
const { code } = authenticated.error
|
|
47
|
+
if (code === Codes.USER_NOT_AUTHENTICATED) res.status(401)
|
|
48
|
+
else if (code === Codes.USER_DOES_NOT_EXIST) res.status(401)
|
|
49
|
+
else res.status(403)
|
|
50
|
+
const responseCore = Outcome.makeFailure(authenticated.error)
|
|
51
|
+
const response: FailureResponseBody<never, never> = {
|
|
52
|
+
...responseCore,
|
|
53
|
+
meta: requestMeta
|
|
54
|
+
}
|
|
55
|
+
res.json(response)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const validated = await validator(req, res)
|
|
60
|
+
if (!validated.success) {
|
|
61
|
+
const errorData = validated.error
|
|
62
|
+
const responseCore = Outcome.makeFailure(errorData)
|
|
63
|
+
const response: FailureResponseBody<ValidationErrCodes, never> = {
|
|
64
|
+
...responseCore,
|
|
65
|
+
meta: requestMeta
|
|
66
|
+
}
|
|
67
|
+
res.status(400).json(response)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const processed = await processor(validated.payload, req, res)
|
|
71
|
+
if (!processed.success) {
|
|
72
|
+
const errorData = processed.error
|
|
73
|
+
const responseCore = Outcome.makeFailure(errorData)
|
|
74
|
+
const response: FailureResponseBody<never, ProcessingErrCodes> = {
|
|
75
|
+
...responseCore,
|
|
76
|
+
meta: requestMeta
|
|
77
|
+
}
|
|
78
|
+
// [WIP] maybe find a better logic err.code => res.status
|
|
79
|
+
res.status(422).json(response)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const responsePayload = processed.payload
|
|
83
|
+
const responseCore = Outcome.makeSuccess(responsePayload)
|
|
84
|
+
const response: SuccessResponseBody<Output> = {
|
|
85
|
+
...responseCore,
|
|
86
|
+
meta: requestMeta
|
|
87
|
+
}
|
|
88
|
+
res.status(200).json(response)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const errorString = unknownToString(err)
|
|
91
|
+
const errorData = register.getErrorData(Codes.UNKNOWN_ERROR, errorString)
|
|
92
|
+
const responseCore = Outcome.makeFailure(errorData)
|
|
93
|
+
const response: FailureResponseBody<never, Codes.UNKNOWN_ERROR> = {
|
|
94
|
+
...responseCore,
|
|
95
|
+
meta: requestMeta
|
|
96
|
+
}
|
|
97
|
+
res.status(500).json(response)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return handler
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function makeEndpoint<
|
|
104
|
+
ReqBody extends object = {},
|
|
105
|
+
Output extends object = {},
|
|
106
|
+
ValidationErrCodes extends Codes = Codes,
|
|
107
|
+
ProcessingErrCodes extends Codes = Codes
|
|
108
|
+
>(
|
|
109
|
+
authenticator: Authenticator,
|
|
110
|
+
validator: Validator<ReqBody, ValidationErrCodes>,
|
|
111
|
+
processor: Processor<ReqBody, Output, ProcessingErrCodes>
|
|
112
|
+
): Endpoint<ReqBody, Output, ValidationErrCodes, ProcessingErrCodes> {
|
|
113
|
+
return {
|
|
114
|
+
authenticator,
|
|
115
|
+
validator,
|
|
116
|
+
processor,
|
|
117
|
+
handler: makeHandler(authenticator, validator, processor)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const nullAuthenticator: Authenticator = () => Outcome.makeSuccess(true)
|
|
122
|
+
export const nullValidator: Validator = () => Outcome.makeSuccess({})
|
|
123
|
+
|
|
124
|
+
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] as const
|
|
125
|
+
type Method = (typeof methods)[number]
|
|
126
|
+
const isMethod = (str: string): str is Method => methods.includes(str as Method)
|
|
127
|
+
|
|
128
|
+
export function makeRouter<T extends { [key: string]: Endpoint<any, any, any, any> }> (endpoints: T) {
|
|
129
|
+
const router = Router()
|
|
130
|
+
Object.entries(endpoints).forEach(([path, endpoint]) => {
|
|
131
|
+
const [method, ...actualPathChunks] = path.split(':')
|
|
132
|
+
const isValidMethod = method !== undefined && isMethod(method)
|
|
133
|
+
if (!isValidMethod) {
|
|
134
|
+
const errorMessage = `${method} is not a valid HTTP method. @${path}.`
|
|
135
|
+
throw new Error(errorMessage)
|
|
136
|
+
}
|
|
137
|
+
const actualPath = actualPathChunks.join(':') // [WIP] check if heading slash is needed
|
|
138
|
+
const handler: RequestHandler = endpoint.handler
|
|
139
|
+
if (method === 'GET') return router.get(actualPath, handler)
|
|
140
|
+
if (method === 'POST') return router.post(actualPath, handler)
|
|
141
|
+
if (method === 'PUT') return router.put(actualPath, handler)
|
|
142
|
+
if (method === 'DELETE') return router.delete(actualPath, handler)
|
|
143
|
+
return router.options(actualPath, handler)
|
|
144
|
+
})
|
|
145
|
+
return router
|
|
146
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
connect as mongooseConnect,
|
|
3
|
+
disconnect as mongooseDisconnect,
|
|
4
|
+
Model as MongooseModel,
|
|
5
|
+
FilterQuery as MongooseFilterQuery,
|
|
6
|
+
RootFilterQuery as MongooseRootFilterQuery,
|
|
7
|
+
Document as MongooseDocument,
|
|
8
|
+
Query as MongooseQuery,
|
|
9
|
+
UpdateQuery as MongooseUpdateQuery
|
|
10
|
+
} from 'mongoose'
|
|
11
|
+
import { unknownToString } from '@design-edito/tools/agnostic/errors/unknown-to-string'
|
|
12
|
+
import { Logs } from '@design-edito/tools/agnostic/misc/logs'
|
|
13
|
+
import { Outcome } from '@design-edito/tools/agnostic/misc/outcome'
|
|
14
|
+
import { DB_USR, DB_PWD, DB_URL, DB_OPT } from '../env'
|
|
15
|
+
import { Codes, makeFailureOutcome } from '../errors'
|
|
16
|
+
|
|
17
|
+
/* * * * * * * * * * * * * * * * * *
|
|
18
|
+
*
|
|
19
|
+
* UTILITY
|
|
20
|
+
*
|
|
21
|
+
* * * * * * * * * * * * * * * * * */
|
|
22
|
+
export interface QueryWithLocals<T, R = T | null> extends MongooseQuery<T, R> {
|
|
23
|
+
getOptions(): {
|
|
24
|
+
$locals?: {
|
|
25
|
+
context?: OperationContext
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* * * * * * * * * * * * * * * * * *
|
|
31
|
+
*
|
|
32
|
+
* CONNECTION / DISCONNECTION
|
|
33
|
+
*
|
|
34
|
+
* * * * * * * * * * * * * * * * * */
|
|
35
|
+
export const connectionString = `mongodb+srv://${DB_USR}:${DB_PWD}@${DB_URL}/?${DB_OPT}`
|
|
36
|
+
export async function connect () {
|
|
37
|
+
try {
|
|
38
|
+
await mongooseConnect(connectionString)
|
|
39
|
+
console.log(Logs.styles.important('Mongoose connected'))
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.log(Logs.styles.error(unknownToString(err)))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function disconnect() {
|
|
46
|
+
try {
|
|
47
|
+
await mongooseDisconnect()
|
|
48
|
+
console.log(Logs.styles.important('Mongoose disconnected'))
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.log(Logs.styles.error(unknownToString(err)))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* * * * * * * * * * * * * * * * * *
|
|
55
|
+
*
|
|
56
|
+
* OPERATIONS
|
|
57
|
+
*
|
|
58
|
+
* * * * * * * * * * * * * * * * * */
|
|
59
|
+
export type OperationContext = {
|
|
60
|
+
initiatorId: string | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DocumentWithLocals<T> extends MongooseDocument<T> {
|
|
64
|
+
$locals: {
|
|
65
|
+
context?: OperationContext
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function insertOne<T> (
|
|
70
|
+
model: MongooseModel<T>,
|
|
71
|
+
data: Partial<T>,
|
|
72
|
+
context: OperationContext
|
|
73
|
+
) {
|
|
74
|
+
try {
|
|
75
|
+
const doc = new model(data)
|
|
76
|
+
doc.$locals = { context }
|
|
77
|
+
const saved = await doc.save()
|
|
78
|
+
return Outcome.makeSuccess(saved.toObject())
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return makeFailureOutcome(Codes.DB_ERROR, unknownToString(err))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function insertMany<T> (
|
|
85
|
+
model: MongooseModel<T>,
|
|
86
|
+
data: Partial<T>[],
|
|
87
|
+
context: OperationContext
|
|
88
|
+
) {
|
|
89
|
+
try {
|
|
90
|
+
const docs = data.map(d => {
|
|
91
|
+
const doc = new model(d)
|
|
92
|
+
doc.$locals = { context }
|
|
93
|
+
return doc
|
|
94
|
+
})
|
|
95
|
+
const saved = await model.insertMany(docs)
|
|
96
|
+
return Outcome.makeSuccess(saved.map(d => d.toObject()))
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return makeFailureOutcome(Codes.DB_ERROR, unknownToString(err))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function findOne<T> (
|
|
103
|
+
model: MongooseModel<T>,
|
|
104
|
+
filter: MongooseFilterQuery<T>,
|
|
105
|
+
context: OperationContext
|
|
106
|
+
) {
|
|
107
|
+
try {
|
|
108
|
+
const found = await model
|
|
109
|
+
.findOne(filter)
|
|
110
|
+
.setOptions({ $locals: { context } })
|
|
111
|
+
.exec()
|
|
112
|
+
return found === null
|
|
113
|
+
? makeFailureOutcome(Codes.DB_NO_DOCUMENT_MATCHES_FILTER, model, filter)
|
|
114
|
+
: Outcome.makeSuccess(found.toObject())
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return makeFailureOutcome(Codes.DB_ERROR, unknownToString(err))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function updateOne<T> (
|
|
121
|
+
model: MongooseModel<T>,
|
|
122
|
+
filter: MongooseRootFilterQuery<T>,
|
|
123
|
+
update: MongooseUpdateQuery<T>,
|
|
124
|
+
context: OperationContext
|
|
125
|
+
) {
|
|
126
|
+
try {
|
|
127
|
+
const updated = await model
|
|
128
|
+
.findOneAndUpdate(filter, update, { new: true })
|
|
129
|
+
.setOptions({ $locals: { context } })
|
|
130
|
+
.exec()
|
|
131
|
+
return updated === null
|
|
132
|
+
? makeFailureOutcome(Codes.DB_NO_DOCUMENT_MATCHES_FILTER, model, filter)
|
|
133
|
+
: Outcome.makeSuccess(updated.toObject())
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return makeFailureOutcome(Codes.DB_ERROR, unknownToString(err))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function deleteOne<T> (
|
|
140
|
+
model: MongooseModel<T>,
|
|
141
|
+
filter: MongooseRootFilterQuery<T>,
|
|
142
|
+
context: OperationContext
|
|
143
|
+
) {
|
|
144
|
+
try {
|
|
145
|
+
const deleted = await model
|
|
146
|
+
.findOneAndDelete(filter)
|
|
147
|
+
.setOptions({ $locals: { context } })
|
|
148
|
+
.exec()
|
|
149
|
+
return deleted === null
|
|
150
|
+
? makeFailureOutcome(Codes.DB_NO_DOCUMENT_MATCHES_FILTER, model, filter)
|
|
151
|
+
: Outcome.makeSuccess(deleted.toObject())
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return makeFailureOutcome(Codes.DB_ERROR, unknownToString(err))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function deleteMany<T> (
|
|
158
|
+
model: MongooseModel<T>,
|
|
159
|
+
filter: MongooseRootFilterQuery<T>,
|
|
160
|
+
context: OperationContext
|
|
161
|
+
) {
|
|
162
|
+
try {
|
|
163
|
+
const deleted = await model
|
|
164
|
+
.deleteMany(filter)
|
|
165
|
+
.setOptions({ $locals: { context } })
|
|
166
|
+
.exec()
|
|
167
|
+
return deleted.deletedCount === 0
|
|
168
|
+
? makeFailureOutcome(Codes.DB_NO_DOCUMENT_MATCHES_FILTER, model, filter)
|
|
169
|
+
: Outcome.makeSuccess({ deleted: deleted.deletedCount })
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return makeFailureOutcome(Codes.DB_ERROR, unknownToString(err))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Sender, Recipient, EmailParams, MailerSend } from 'mailersend'
|
|
2
|
+
import { APIResponse } from 'mailersend/lib/services/request.service'
|
|
3
|
+
import { Outcome } from '@design-edito/tools/agnostic/misc/outcome'
|
|
4
|
+
import { unknownToString } from '@design-edito/tools/agnostic/errors/unknown-to-string'
|
|
5
|
+
import { MAILERSEND_API_KEY } from '../env'
|
|
6
|
+
|
|
7
|
+
export type SendOptions = {
|
|
8
|
+
senderEmail: string
|
|
9
|
+
senderName: string
|
|
10
|
+
recipientEmail: string
|
|
11
|
+
recipientName: string
|
|
12
|
+
subject: string
|
|
13
|
+
htmlBody: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function send (
|
|
17
|
+
from: string,
|
|
18
|
+
fromName: string,
|
|
19
|
+
to: string,
|
|
20
|
+
toName: string,
|
|
21
|
+
subject: string,
|
|
22
|
+
htmlBody: string
|
|
23
|
+
): Promise<Outcome.Either<APIResponse, string>> {
|
|
24
|
+
const mailerSend = new MailerSend({ apiKey: MAILERSEND_API_KEY })
|
|
25
|
+
const sender = new Sender(from, fromName)
|
|
26
|
+
const recipient = new Recipient(to, toName)
|
|
27
|
+
const emailParams = new EmailParams()
|
|
28
|
+
.setFrom(sender)
|
|
29
|
+
.setTo([recipient])
|
|
30
|
+
.setReplyTo(sender)
|
|
31
|
+
.setSubject(subject)
|
|
32
|
+
.setHtml(htmlBody)
|
|
33
|
+
try {
|
|
34
|
+
const sent = await mailerSend.email.send(emailParams)
|
|
35
|
+
return Outcome.makeSuccess(sent)
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return Outcome.makeFailure(unknownToString(err))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function makeUserEmailVerificationTokenBody (username: string, token: string): string {
|
|
42
|
+
return `Hey ${username}! Here is your account verification code: ${token}.`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function makeUserPasswordRenewalTokenBody (username: string, token: string): string {
|
|
46
|
+
return `Hey ${username}! Here is your password renewal code: ${token}.`
|
|
47
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import dotenv from 'dotenv'
|
|
2
|
+
|
|
3
|
+
dotenv.config()
|
|
4
|
+
|
|
5
|
+
const MODE = process.env.MODE as 'development' | 'production'
|
|
6
|
+
const HOST = process.env.HOST as string
|
|
7
|
+
const DB_USR = process.env.DB_USR as string
|
|
8
|
+
const DB_PWD = process.env.DB_PWD as string
|
|
9
|
+
const DB_URL = process.env.DB_URL as string
|
|
10
|
+
const DB_OPT = process.env.DB_OPT as string
|
|
11
|
+
const DB_RESERVED_AGENDA_JOBS_COLLECTION_NAME = process.env.DB_RESERVED_AGENDA_JOBS_COLLECTION_NAME as string
|
|
12
|
+
const ROOT_USER_ID = process.env.ROOT_USER_ID as string
|
|
13
|
+
const ROOT_USER_NAME = process.env.ROOT_USER_NAME as string
|
|
14
|
+
const ROOT_USER_EMAIL = process.env.ROOT_USER_EMAIL as string
|
|
15
|
+
const ROOT_USER_PWD = process.env.ROOT_USER_PWD as string
|
|
16
|
+
const USER_EMAIL_VALIDATION_TOKEN_LIFETIME_MINUTES = process.env.USER_EMAIL_VALIDATION_TOKEN_LIFETIME_MINUTES as string
|
|
17
|
+
const JWT_SECRET = process.env.JWT_SECRET as string
|
|
18
|
+
const ACCESS_TOKEN_EXPIRATION_SECONDS = parseFloat(process.env.ACCESS_TOKEN_EXPIRATION_SECONDS as string)
|
|
19
|
+
const ACCESS_TOKEN_RENEWAL_THRESHOLD_SECONDS = parseFloat(process.env.ACCESS_TOKEN_RENEWAL_THRESHOLD_SECONDS as string)
|
|
20
|
+
const REFRESH_TOKEN_EXPIRATION_SECONDS = parseFloat(process.env.REFRESH_TOKEN_EXPIRATION_SECONDS as string)
|
|
21
|
+
const ACCESS_TOKEN_MAX_REFRESH_COUNT = parseFloat(process.env.ACCESS_TOKEN_MAX_REFRESH_COUNT as string)
|
|
22
|
+
const REFRESH_TOKEN_MAX_REFRESH_COUNT = parseFloat(process.env.REFRESH_TOKEN_MAX_REFRESH_COUNT as string)
|
|
23
|
+
const MAILERSEND_API_KEY = process.env.MAILERSEND_API_KEY as string
|
|
24
|
+
const EMAILER_EMAIL = process.env.EMAILER_EMAIL as string
|
|
25
|
+
const EMAILER_NAME = process.env.EMAILER_NAME as string
|
|
26
|
+
|
|
27
|
+
function kill (message: string) {
|
|
28
|
+
console.log(message) // [WIP] replace all app's console.logs with actual logger calls
|
|
29
|
+
throw process.exit(1) // It's too early in this file to use await Init.shutdown(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof MODE !== 'string') { kill('.env/MODE field must be a string') }
|
|
33
|
+
if (typeof HOST !== 'string') { kill('.env/HOST field must be a string') }
|
|
34
|
+
else if (MODE !== 'development' && MODE !== 'production') { kill('.env/MODE field must be either "development" or "production"') }
|
|
35
|
+
else if (typeof DB_USR !== 'string') { kill('.env/DB_USR field must be a string') }
|
|
36
|
+
else if (typeof DB_PWD !== 'string') { kill('.env/DB_PWD field must be a string') }
|
|
37
|
+
else if (typeof DB_URL !== 'string') { kill('.env/DB_URL field must be a string') }
|
|
38
|
+
else if (typeof DB_OPT !== 'string') { kill('.env/DB_OPT field must be a string') }
|
|
39
|
+
else if (typeof DB_RESERVED_AGENDA_JOBS_COLLECTION_NAME !== 'string') { kill('.env/DB_RESERVED_AGENDA_JOBS_COLLECTION_NAME field must be a string') }
|
|
40
|
+
else if (typeof ROOT_USER_ID !== 'string') { kill('.env/ROOT_USER_ID field must be a string') }
|
|
41
|
+
else if (typeof ROOT_USER_NAME !== 'string') { kill('.env/ROOT_USER_NAME field must be a string') }
|
|
42
|
+
else if (typeof ROOT_USER_EMAIL !== 'string') { kill('.env/ROOT_USER_EMAIL field must be a string') }
|
|
43
|
+
else if (typeof ROOT_USER_PWD !== 'string') { kill('.env/ROOT_USER_PWD field must be a string') }
|
|
44
|
+
else if (typeof USER_EMAIL_VALIDATION_TOKEN_LIFETIME_MINUTES !== 'string') { kill('.env/USER_EMAIL_VALIDATION_TOKEN_LIFETIME_MINUTES field must be a string') }
|
|
45
|
+
else if (typeof JWT_SECRET !== 'string') { kill('.env/JWT_SECRET field must be a string') }
|
|
46
|
+
else if (typeof ACCESS_TOKEN_EXPIRATION_SECONDS !== 'number' || Number.isNaN(ACCESS_TOKEN_EXPIRATION_SECONDS)) { kill('.env/ACCESS_TOKEN_EXPIRATION_SECONDS field must be a number') }
|
|
47
|
+
else if (typeof ACCESS_TOKEN_RENEWAL_THRESHOLD_SECONDS !== 'number' || Number.isNaN(ACCESS_TOKEN_RENEWAL_THRESHOLD_SECONDS)) { kill('.env/ACCESS_TOKEN_RENEWAL_THRESHOLD_SECONDS field must be a number') }
|
|
48
|
+
else if (typeof REFRESH_TOKEN_EXPIRATION_SECONDS !== 'number' || Number.isNaN(REFRESH_TOKEN_EXPIRATION_SECONDS)) { kill('.env/REFRESH_TOKEN_EXPIRATION_SECONDS field must be a number') }
|
|
49
|
+
else if (typeof ACCESS_TOKEN_MAX_REFRESH_COUNT !== 'number' || Number.isNaN(ACCESS_TOKEN_MAX_REFRESH_COUNT)) { kill('.env/ACCESS_TOKEN_MAX_REFRESH_COUNT field must be a number') }
|
|
50
|
+
else if (typeof REFRESH_TOKEN_MAX_REFRESH_COUNT !== 'number' || Number.isNaN(REFRESH_TOKEN_MAX_REFRESH_COUNT)) { kill('.env/REFRESH_TOKEN_MAX_REFRESH_COUNT field must be a number') }
|
|
51
|
+
else if (typeof MAILERSEND_API_KEY !== 'string') { kill('.env/MAILERSEND_API_KEY field must be a string') }
|
|
52
|
+
else if (typeof EMAILER_EMAIL !== 'string') { kill('.env/EMAILER_EMAIL field must be a string') }
|
|
53
|
+
else if (typeof EMAILER_NAME !== 'string') { kill('.env/EMAILER_NAME field must be a string') }
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
MODE,
|
|
57
|
+
HOST,
|
|
58
|
+
DB_USR,
|
|
59
|
+
DB_PWD,
|
|
60
|
+
DB_URL,
|
|
61
|
+
DB_OPT,
|
|
62
|
+
DB_RESERVED_AGENDA_JOBS_COLLECTION_NAME,
|
|
63
|
+
ROOT_USER_ID,
|
|
64
|
+
ROOT_USER_NAME,
|
|
65
|
+
ROOT_USER_EMAIL,
|
|
66
|
+
ROOT_USER_PWD,
|
|
67
|
+
USER_EMAIL_VALIDATION_TOKEN_LIFETIME_MINUTES,
|
|
68
|
+
JWT_SECRET,
|
|
69
|
+
ACCESS_TOKEN_EXPIRATION_SECONDS,
|
|
70
|
+
ACCESS_TOKEN_RENEWAL_THRESHOLD_SECONDS,
|
|
71
|
+
REFRESH_TOKEN_EXPIRATION_SECONDS,
|
|
72
|
+
ACCESS_TOKEN_MAX_REFRESH_COUNT,
|
|
73
|
+
REFRESH_TOKEN_MAX_REFRESH_COUNT,
|
|
74
|
+
MAILERSEND_API_KEY,
|
|
75
|
+
EMAILER_EMAIL,
|
|
76
|
+
EMAILER_NAME
|
|
77
|
+
}
|