@radio-garden/ditojs-server 2.85.2-0.5067ad799
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/README.md +6 -0
- package/package.json +95 -0
- package/src/app/Application.js +1186 -0
- package/src/app/Validator.js +405 -0
- package/src/app/index.js +2 -0
- package/src/cli/console.js +152 -0
- package/src/cli/db/createMigration.js +241 -0
- package/src/cli/db/index.js +7 -0
- package/src/cli/db/listAssetConfig.js +10 -0
- package/src/cli/db/migrate.js +12 -0
- package/src/cli/db/reset.js +23 -0
- package/src/cli/db/rollback.js +12 -0
- package/src/cli/db/seed.js +80 -0
- package/src/cli/db/unlock.js +9 -0
- package/src/cli/index.js +72 -0
- package/src/controllers/AdminController.js +322 -0
- package/src/controllers/CollectionController.js +274 -0
- package/src/controllers/Controller.js +657 -0
- package/src/controllers/ControllerAction.js +370 -0
- package/src/controllers/MemberAction.js +27 -0
- package/src/controllers/ModelController.js +63 -0
- package/src/controllers/RelationController.js +93 -0
- package/src/controllers/UsersController.js +64 -0
- package/src/controllers/index.js +5 -0
- package/src/errors/AssetError.js +7 -0
- package/src/errors/AuthenticationError.js +7 -0
- package/src/errors/AuthorizationError.js +7 -0
- package/src/errors/ControllerError.js +14 -0
- package/src/errors/DatabaseError.js +37 -0
- package/src/errors/GraphError.js +7 -0
- package/src/errors/ModelError.js +12 -0
- package/src/errors/NotFoundError.js +7 -0
- package/src/errors/NotImplementedError.js +7 -0
- package/src/errors/QueryBuilderError.js +7 -0
- package/src/errors/RelationError.js +21 -0
- package/src/errors/ResponseError.js +56 -0
- package/src/errors/ValidationError.js +7 -0
- package/src/errors/index.js +13 -0
- package/src/graph/DitoGraphProcessor.js +213 -0
- package/src/graph/expression.js +53 -0
- package/src/graph/graph.js +258 -0
- package/src/graph/index.js +3 -0
- package/src/index.js +9 -0
- package/src/lib/EventEmitter.js +66 -0
- package/src/lib/KnexHelper.js +30 -0
- package/src/lib/index.js +2 -0
- package/src/middleware/attachLogger.js +8 -0
- package/src/middleware/createTransaction.js +33 -0
- package/src/middleware/extendContext.js +10 -0
- package/src/middleware/findRoute.js +20 -0
- package/src/middleware/handleConnectMiddleware.js +99 -0
- package/src/middleware/handleError.js +29 -0
- package/src/middleware/handleRoute.js +23 -0
- package/src/middleware/handleSession.js +77 -0
- package/src/middleware/handleUser.js +31 -0
- package/src/middleware/index.js +11 -0
- package/src/middleware/logRequests.js +125 -0
- package/src/middleware/setupRequestStorage.js +14 -0
- package/src/mixins/AssetMixin.js +78 -0
- package/src/mixins/SessionMixin.js +17 -0
- package/src/mixins/TimeStampedMixin.js +41 -0
- package/src/mixins/UserMixin.js +171 -0
- package/src/mixins/index.js +4 -0
- package/src/models/AssetModel.js +4 -0
- package/src/models/Model.js +1205 -0
- package/src/models/RelationAccessor.js +41 -0
- package/src/models/SessionModel.js +4 -0
- package/src/models/TimeStampedModel.js +4 -0
- package/src/models/UserModel.js +4 -0
- package/src/models/definitions/assets.js +5 -0
- package/src/models/definitions/filters.js +121 -0
- package/src/models/definitions/hooks.js +8 -0
- package/src/models/definitions/index.js +22 -0
- package/src/models/definitions/modifiers.js +5 -0
- package/src/models/definitions/options.js +5 -0
- package/src/models/definitions/properties.js +73 -0
- package/src/models/definitions/relations.js +5 -0
- package/src/models/definitions/schema.js +5 -0
- package/src/models/definitions/scopes.js +36 -0
- package/src/models/index.js +5 -0
- package/src/query/QueryBuilder.js +1077 -0
- package/src/query/QueryFilters.js +66 -0
- package/src/query/QueryParameters.js +79 -0
- package/src/query/Registry.js +29 -0
- package/src/query/index.js +3 -0
- package/src/schema/formats/_empty.js +4 -0
- package/src/schema/formats/_required.js +4 -0
- package/src/schema/formats/index.js +2 -0
- package/src/schema/index.js +5 -0
- package/src/schema/keywords/_computed.js +7 -0
- package/src/schema/keywords/_foreign.js +7 -0
- package/src/schema/keywords/_hidden.js +7 -0
- package/src/schema/keywords/_index.js +7 -0
- package/src/schema/keywords/_instanceof.js +45 -0
- package/src/schema/keywords/_primary.js +7 -0
- package/src/schema/keywords/_range.js +18 -0
- package/src/schema/keywords/_relate.js +13 -0
- package/src/schema/keywords/_specificType.js +7 -0
- package/src/schema/keywords/_unique.js +7 -0
- package/src/schema/keywords/_unsigned.js +7 -0
- package/src/schema/keywords/_validate.js +73 -0
- package/src/schema/keywords/index.js +12 -0
- package/src/schema/relations.js +324 -0
- package/src/schema/relations.test.js +177 -0
- package/src/schema/schema.js +289 -0
- package/src/schema/schema.test.js +720 -0
- package/src/schema/types/_asset.js +31 -0
- package/src/schema/types/_color.js +4 -0
- package/src/schema/types/index.js +2 -0
- package/src/services/Service.js +35 -0
- package/src/services/index.js +1 -0
- package/src/storage/AssetFile.js +81 -0
- package/src/storage/DiskStorage.js +114 -0
- package/src/storage/S3Storage.js +169 -0
- package/src/storage/Storage.js +231 -0
- package/src/storage/index.js +9 -0
- package/src/utils/duration.js +15 -0
- package/src/utils/emitter.js +8 -0
- package/src/utils/fs.js +10 -0
- package/src/utils/function.js +17 -0
- package/src/utils/function.test.js +77 -0
- package/src/utils/handler.js +17 -0
- package/src/utils/json.js +3 -0
- package/src/utils/model.js +35 -0
- package/src/utils/net.js +17 -0
- package/src/utils/object.js +82 -0
- package/src/utils/object.test.js +86 -0
- package/src/utils/scope.js +7 -0
- package/types/index.d.ts +3547 -0
- package/types/tests/application.test-d.ts +26 -0
- package/types/tests/controller.test-d.ts +113 -0
- package/types/tests/errors.test-d.ts +53 -0
- package/types/tests/fixtures.ts +19 -0
- package/types/tests/model.test-d.ts +193 -0
- package/types/tests/query-builder.test-d.ts +106 -0
- package/types/tests/relation.test-d.ts +83 -0
- package/types/tests/storage.test-d.ts +113 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function handleUser() {
|
|
2
|
+
return (ctx, next) => {
|
|
3
|
+
// Attach a logger instance with the user to the context.
|
|
4
|
+
ctx.logger = ctx.logger?.child({ user: ctx.state.user }) || null
|
|
5
|
+
|
|
6
|
+
// Override `ctx.login()` and `ctx.logout()` with versions that emit events
|
|
7
|
+
// on the user model:
|
|
8
|
+
const { login, logout } = ctx
|
|
9
|
+
|
|
10
|
+
ctx.login = ctx.logIn = async function (user, options = {}) {
|
|
11
|
+
await user.$emit('before:login', options)
|
|
12
|
+
await login.call(this, user, options)
|
|
13
|
+
await user.$emit('after:login', options)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
ctx.logout = ctx.logOut = async function (options = {}) {
|
|
17
|
+
const { user } = ctx.state
|
|
18
|
+
await user?.$emit('before:logout', options)
|
|
19
|
+
await logout.call(this, options)
|
|
20
|
+
// Clear the session after logout, apparently koa-passport doesn't take
|
|
21
|
+
// care of this itself:
|
|
22
|
+
// https://stackoverflow.com/questions/55818887/koa-passport-logout-is-not-clearing-session
|
|
23
|
+
ctx.session = null
|
|
24
|
+
await user?.$emit('after:logout', options)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return next()
|
|
28
|
+
// Don't set back `ctx.logger` because the context is still valid. It is
|
|
29
|
+
// used in `logResponse()` of the `logRequests()` middleware, among others.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './attachLogger.js'
|
|
2
|
+
export * from './createTransaction.js'
|
|
3
|
+
export * from './extendContext.js'
|
|
4
|
+
export * from './findRoute.js'
|
|
5
|
+
export * from './handleConnectMiddleware.js'
|
|
6
|
+
export * from './handleError.js'
|
|
7
|
+
export * from './handleRoute.js'
|
|
8
|
+
export * from './handleSession.js'
|
|
9
|
+
export * from './handleUser.js'
|
|
10
|
+
export * from './logRequests.js'
|
|
11
|
+
export * from './setupRequestStorage.js'
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Middleware inspired by 'koa-logger'. Adapted and extended to our needs.
|
|
3
|
+
*/
|
|
4
|
+
import bytes from 'bytes'
|
|
5
|
+
import pico from 'picocolors'
|
|
6
|
+
import Counter from 'passthrough-counter'
|
|
7
|
+
|
|
8
|
+
export function logRequests({ ignoreUrlPattern } = {}) {
|
|
9
|
+
return async (ctx, next) => {
|
|
10
|
+
if (ignoreUrlPattern && ctx.req.url.match(ignoreUrlPattern)) {
|
|
11
|
+
return next()
|
|
12
|
+
}
|
|
13
|
+
// request
|
|
14
|
+
const start = performance.now()
|
|
15
|
+
|
|
16
|
+
logRequest(ctx)
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await next()
|
|
20
|
+
} catch (err) {
|
|
21
|
+
logResponse({ ctx, start, err })
|
|
22
|
+
throw err
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Calculate the length of a streaming response by intercepting the stream
|
|
26
|
+
// with a counter. Only necessary if a content-length header is currently
|
|
27
|
+
// not set.
|
|
28
|
+
const {
|
|
29
|
+
body,
|
|
30
|
+
response: { length }
|
|
31
|
+
} = ctx
|
|
32
|
+
|
|
33
|
+
let counter
|
|
34
|
+
if (length === null && body?.readable) {
|
|
35
|
+
ctx.body = body.pipe((counter = new Counter())).on('error', ctx.onerror)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Log when the response is finished or closed, whichever happens first.
|
|
39
|
+
const { res } = ctx
|
|
40
|
+
|
|
41
|
+
const onfinish = done.bind(null, 'finish')
|
|
42
|
+
const onclose = done.bind(null, 'close')
|
|
43
|
+
|
|
44
|
+
res.once('finish', onfinish)
|
|
45
|
+
res.once('close', onclose)
|
|
46
|
+
|
|
47
|
+
function done() {
|
|
48
|
+
res.removeListener('finish', onfinish)
|
|
49
|
+
res.removeListener('close', onclose)
|
|
50
|
+
logResponse({ ctx, start, length: counter ? counter.length : length })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function logRequest(ctx) {
|
|
56
|
+
const logger = ctx.logger?.child({ name: 'http' })
|
|
57
|
+
if (logger?.isLevelEnabled('trace')) {
|
|
58
|
+
logger.trace(
|
|
59
|
+
{ req: ctx.req },
|
|
60
|
+
`${
|
|
61
|
+
pico.gray('<--')
|
|
62
|
+
} ${
|
|
63
|
+
pico.bold(ctx.method)
|
|
64
|
+
} ${
|
|
65
|
+
pico.gray(ctx.originalUrl)
|
|
66
|
+
}`
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function logResponse({ ctx, start, length, err }) {
|
|
72
|
+
const logger = ctx.logger?.child({ name: 'http' })
|
|
73
|
+
const level = err ? 'warn' : 'info'
|
|
74
|
+
if (logger?.isLevelEnabled(level)) {
|
|
75
|
+
// Get the status code of the response
|
|
76
|
+
const status = err
|
|
77
|
+
? err.status || 500
|
|
78
|
+
: ctx.status || 404
|
|
79
|
+
|
|
80
|
+
// Set the color of the status code;
|
|
81
|
+
const statusRange = (status / 100) | 0
|
|
82
|
+
const statusColor = colorCodes[statusRange] || colorCodes[0]
|
|
83
|
+
|
|
84
|
+
// Get the human readable response length
|
|
85
|
+
const formattedLength = [204, 205, 304].includes(status)
|
|
86
|
+
? ''
|
|
87
|
+
: length == null
|
|
88
|
+
? '-'
|
|
89
|
+
: bytes(length).toLowerCase()
|
|
90
|
+
|
|
91
|
+
const formattedTime = formatTime(start)
|
|
92
|
+
|
|
93
|
+
logger[level](
|
|
94
|
+
{ req: ctx.req, res: ctx.res },
|
|
95
|
+
`${
|
|
96
|
+
pico.bold(ctx.method)
|
|
97
|
+
} ${
|
|
98
|
+
pico.gray(ctx.originalUrl)
|
|
99
|
+
} ${
|
|
100
|
+
pico[statusColor](status)
|
|
101
|
+
} ${
|
|
102
|
+
pico.gray(formattedTime)
|
|
103
|
+
} ${
|
|
104
|
+
pico.gray(formattedLength)
|
|
105
|
+
}`
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatTime(start) {
|
|
111
|
+
const delta = performance.now() - start
|
|
112
|
+
return delta < 10000
|
|
113
|
+
? +delta.toFixed(2) + 'ms'
|
|
114
|
+
: +(delta / 1000).toFixed(2) + 's'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const colorCodes = {
|
|
118
|
+
7: 'magenta',
|
|
119
|
+
5: 'red',
|
|
120
|
+
4: 'yellow',
|
|
121
|
+
3: 'cyan',
|
|
122
|
+
2: 'green',
|
|
123
|
+
1: 'green',
|
|
124
|
+
0: 'yellow'
|
|
125
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { mixin } from '@ditojs/utils'
|
|
2
|
+
import { TimeStampedMixin } from './TimeStampedMixin.js'
|
|
3
|
+
|
|
4
|
+
// Asset models are always to be time-stamped:
|
|
5
|
+
export const AssetMixin = mixin(
|
|
6
|
+
Model =>
|
|
7
|
+
class extends TimeStampedMixin(Model) {
|
|
8
|
+
static properties = {
|
|
9
|
+
key: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
required: true,
|
|
12
|
+
unique: true,
|
|
13
|
+
index: true
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
file: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
// TODO: Support this on 'object':
|
|
19
|
+
// required: true
|
|
20
|
+
properties: {
|
|
21
|
+
// The unique key within the storage (uuid/v4 + file extension)
|
|
22
|
+
key: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
required: true
|
|
25
|
+
},
|
|
26
|
+
// The original filename, and display name when file is shown
|
|
27
|
+
name: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
required: true
|
|
30
|
+
},
|
|
31
|
+
// The file's mime-type
|
|
32
|
+
type: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
required: true
|
|
35
|
+
},
|
|
36
|
+
// The amount of bytes consumed by the file
|
|
37
|
+
size: {
|
|
38
|
+
type: 'integer',
|
|
39
|
+
required: true
|
|
40
|
+
},
|
|
41
|
+
// Use for storages configured for files to be publicly accessible:
|
|
42
|
+
url: {
|
|
43
|
+
type: 'string'
|
|
44
|
+
},
|
|
45
|
+
// These are only used when the storage defines
|
|
46
|
+
// `config.readDimensions`:
|
|
47
|
+
width: {
|
|
48
|
+
type: 'integer'
|
|
49
|
+
},
|
|
50
|
+
height: {
|
|
51
|
+
type: 'integer'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
storage: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
required: true
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
count: {
|
|
62
|
+
type: 'integer',
|
|
63
|
+
unsigned: true,
|
|
64
|
+
default: 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// @override
|
|
69
|
+
$parseJson(json) {
|
|
70
|
+
const { file, storage } = json
|
|
71
|
+
// Convert `AssetMixin#file` to an `AssetFile` instance:
|
|
72
|
+
if (file && storage) {
|
|
73
|
+
this.constructor.app.getStorage(storage)?.convertAssetFile(file)
|
|
74
|
+
}
|
|
75
|
+
return json
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mixin } from '@ditojs/utils'
|
|
2
|
+
|
|
3
|
+
export const TimeStampedMixin = mixin(
|
|
4
|
+
Model =>
|
|
5
|
+
class extends Model {
|
|
6
|
+
static properties = {
|
|
7
|
+
createdAt: {
|
|
8
|
+
type: 'timestamp',
|
|
9
|
+
default: 'now()'
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
updatedAt: {
|
|
13
|
+
type: 'timestamp',
|
|
14
|
+
default: 'now()'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static scopes = {
|
|
19
|
+
timeStamped: query =>
|
|
20
|
+
query
|
|
21
|
+
.select('createdAt', 'updatedAt')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static hooks = {
|
|
25
|
+
'before:insert'({ inputItems }) {
|
|
26
|
+
const now = new Date()
|
|
27
|
+
for (const item of inputItems) {
|
|
28
|
+
item.createdAt = now
|
|
29
|
+
item.updatedAt = now
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
'before:update'({ inputItems }) {
|
|
34
|
+
const now = new Date()
|
|
35
|
+
for (const item of inputItems) {
|
|
36
|
+
item.updatedAt = now
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import bcrypt from 'bcryptjs'
|
|
2
|
+
import passport from 'koa-passport'
|
|
3
|
+
import { Strategy as LocalStrategy } from 'passport-local'
|
|
4
|
+
import { mixin, asArray } from '@ditojs/utils'
|
|
5
|
+
import { AuthenticationError } from '../errors/index.js'
|
|
6
|
+
|
|
7
|
+
export const UserMixin = mixin(
|
|
8
|
+
Model =>
|
|
9
|
+
class extends Model {
|
|
10
|
+
static options = {
|
|
11
|
+
usernameProperty: 'username',
|
|
12
|
+
passwordProperty: 'password',
|
|
13
|
+
// This option can be used to specify (eager) scopes to be applied when
|
|
14
|
+
// the user is deserialized from the session.
|
|
15
|
+
sessionScope: undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static get properties() {
|
|
19
|
+
const {
|
|
20
|
+
usernameProperty,
|
|
21
|
+
passwordProperty
|
|
22
|
+
} = this.definition.options
|
|
23
|
+
return {
|
|
24
|
+
[usernameProperty]: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
required: true
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// `password` isn't stored, but this is required for validation:
|
|
30
|
+
[passwordProperty]: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
computed: true
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
hash: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
hidden: true
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
lastLogin: {
|
|
41
|
+
type: 'timestamp',
|
|
42
|
+
nullable: true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get password() {
|
|
48
|
+
// Nice try ;)
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
set password(password) {
|
|
53
|
+
this.hash = bcrypt.hashSync(password, bcrypt.genSaltSync(10))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async $verifyPassword(password) {
|
|
57
|
+
return bcrypt.compare(password, this.hash)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$hasRole(...roles) {
|
|
61
|
+
// Support an optional `roles` array on the model that can contain roles
|
|
62
|
+
return this.roles?.find(role => roles.includes(role)) || false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$hasOwner(owner) {
|
|
66
|
+
return this.$is(owner)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$isLoggedIn(ctx) {
|
|
70
|
+
return this.$is(ctx.state.user)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static setup() {
|
|
74
|
+
userClasses[this.name] = this
|
|
75
|
+
const {
|
|
76
|
+
usernameProperty,
|
|
77
|
+
passwordProperty
|
|
78
|
+
} = this.definition.options
|
|
79
|
+
passport.use(
|
|
80
|
+
this.name,
|
|
81
|
+
new LocalStrategy(
|
|
82
|
+
{
|
|
83
|
+
usernameField: usernameProperty,
|
|
84
|
+
passwordField: passwordProperty,
|
|
85
|
+
// Wee need the `req` object, so we can get the active database
|
|
86
|
+
// transaction through `req.ctx.transaction`:
|
|
87
|
+
passReqToCallback: true
|
|
88
|
+
},
|
|
89
|
+
async (req, username, password, done) => {
|
|
90
|
+
try {
|
|
91
|
+
const user = await this.sessionQuery(
|
|
92
|
+
req.ctx.transaction
|
|
93
|
+
).findOne(usernameProperty, username)
|
|
94
|
+
const res =
|
|
95
|
+
user && (await user.$verifyPassword(password))
|
|
96
|
+
? user
|
|
97
|
+
: null
|
|
98
|
+
done(null, res)
|
|
99
|
+
} catch (err) {
|
|
100
|
+
done(err)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static async login(ctx, options) {
|
|
108
|
+
// Unfortunately koa-passport isn't promisified yet so do some wrapping:
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
// Use a custom callback to handle authentication, see:
|
|
111
|
+
// http://www.passportjs.org/docs/downloads/html/#custom-callback
|
|
112
|
+
passport.authenticate(
|
|
113
|
+
this.name,
|
|
114
|
+
async (err, user, message, status) => {
|
|
115
|
+
if (err) {
|
|
116
|
+
reject(err)
|
|
117
|
+
} else if (user) {
|
|
118
|
+
try {
|
|
119
|
+
await ctx.login(user, options)
|
|
120
|
+
resolve(user)
|
|
121
|
+
} catch (err) {
|
|
122
|
+
reject(err)
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
reject(
|
|
126
|
+
new AuthenticationError(
|
|
127
|
+
message || 'Password or username is incorrect',
|
|
128
|
+
status
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
)(ctx)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static sessionQuery(trx) {
|
|
138
|
+
return this.query(trx).withScope(
|
|
139
|
+
...asArray(this.definition.options.sessionScope)
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const userClasses = {}
|
|
146
|
+
|
|
147
|
+
// NOTE: We can't use toCallback() here since passport checks function arity to
|
|
148
|
+
// determine sequence of arguments received, and `req` would not be included:
|
|
149
|
+
passport.serializeUser((req, user, done) => {
|
|
150
|
+
// To support multiple user model classes, use both the model class name and
|
|
151
|
+
// id as identifier.
|
|
152
|
+
const modelName = user?.constructor.name
|
|
153
|
+
const identifier =
|
|
154
|
+
modelName && userClasses[modelName]
|
|
155
|
+
? `${modelName}-${user.id}`
|
|
156
|
+
: null
|
|
157
|
+
done(null, identifier)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
passport.deserializeUser(async (req, identifier, done) => {
|
|
161
|
+
const [modelName, userId] = identifier.split('-')
|
|
162
|
+
const userClass = userClasses[modelName]
|
|
163
|
+
try {
|
|
164
|
+
const user = userClass
|
|
165
|
+
? await userClass.sessionQuery(req.ctx.transaction).findById(userId)
|
|
166
|
+
: null
|
|
167
|
+
done(null, user)
|
|
168
|
+
} catch (err) {
|
|
169
|
+
done(err)
|
|
170
|
+
}
|
|
171
|
+
})
|