@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.
Files changed (55) hide show
  1. package/cli/assets/list.txt +1 -2
  2. package/cli/assets/version.txt +1 -1
  3. package/make-template/assets/express/esbuild.config.js +19 -0
  4. package/make-template/assets/express/package.json +16 -12
  5. package/make-template/assets/express/src/index.ts +13 -49
  6. package/make-template/assets/express/src/public/index.css +7 -0
  7. package/make-template/assets/express/src/public/index.html +15 -0
  8. package/make-template/assets/express/src/public/index.js +31 -0
  9. package/make-template/assets/express/src/tsconfig.json +7 -3
  10. package/make-template/assets/express/src/www/index.ts +42 -0
  11. package/make-template/assets/express-api/Dockerfile +18 -0
  12. package/make-template/assets/express-api/Dockerfile.dev +8 -0
  13. package/make-template/assets/express-api/env +36 -0
  14. package/make-template/assets/express-api/esbuild.config.js +26 -0
  15. package/make-template/assets/express-api/gitignore +214 -0
  16. package/make-template/assets/express-api/package.json +60 -0
  17. package/make-template/assets/express-api/src/api/auth/_utils/index.ts +47 -0
  18. package/make-template/assets/express-api/src/api/auth/index.ts +25 -0
  19. package/make-template/assets/express-api/src/api/auth/login/index.ts +101 -0
  20. package/make-template/assets/express-api/src/api/auth/logout/index.ts +45 -0
  21. package/make-template/assets/express-api/src/api/auth/refresh-token/index.ts +54 -0
  22. package/make-template/assets/express-api/src/api/auth/request-email-verification-token/index.ts +45 -0
  23. package/make-template/assets/express-api/src/api/auth/request-new-password/index.ts +62 -0
  24. package/make-template/assets/express-api/src/api/auth/signup/index.ts +99 -0
  25. package/make-template/assets/express-api/src/api/auth/submit-new-password/index.ts +61 -0
  26. package/make-template/assets/express-api/src/api/auth/verify-email/index.ts +79 -0
  27. package/make-template/assets/express-api/src/api/auth/whoami/index.ts +68 -0
  28. package/make-template/assets/express-api/src/api/index.ts +18 -0
  29. package/make-template/assets/express-api/src/api/types.ts +76 -0
  30. package/make-template/assets/express-api/src/api/utils.ts +146 -0
  31. package/make-template/assets/express-api/src/database/index.ts +173 -0
  32. package/make-template/assets/express-api/src/email/index.ts +47 -0
  33. package/make-template/assets/express-api/src/env/index.ts +77 -0
  34. package/make-template/assets/express-api/src/errors/index.ts +128 -0
  35. package/make-template/assets/express-api/src/index.ts +42 -0
  36. package/make-template/assets/express-api/src/init/index.ts +156 -0
  37. package/make-template/assets/express-api/src/jwt/index.ts +105 -0
  38. package/make-template/assets/express-api/src/public/index.css +7 -0
  39. package/make-template/assets/express-api/src/public/index.html +15 -0
  40. package/make-template/assets/express-api/src/public/index.js +31 -0
  41. package/make-template/assets/express-api/src/schema/_history/index.ts +124 -0
  42. package/make-template/assets/express-api/src/schema/_meta/index.ts +113 -0
  43. package/make-template/assets/express-api/src/schema/index.ts +17 -0
  44. package/make-template/assets/express-api/src/schema/user/index.ts +117 -0
  45. package/make-template/assets/express-api/src/schema/user-email-validation-token/index.ts +20 -0
  46. package/make-template/assets/express-api/src/schema/user-password-renewal-token/index.ts +20 -0
  47. package/make-template/assets/express-api/src/schema/user-revoked-auth-token/index.ts +26 -0
  48. package/make-template/assets/express-api/src/tsconfig.json +16 -0
  49. package/make-template/assets/express-api/src/www/index.ts +43 -0
  50. package/make-template/index.js +2 -3
  51. package/make-template/index.js.map +3 -3
  52. package/package.json +7 -8
  53. package/make-template/assets/express/src/routes/index.ts +0 -7
  54. package/upgrade/index.js +0 -12
  55. 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
+ }