aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared
diff options
context:
space:
mode:
Diffstat (limited to 'shared')
-rw-r--r--shared/core-utils/common/version.ts17
-rw-r--r--shared/core-utils/plugins/hooks.ts8
-rw-r--r--shared/core-utils/renderer/html.ts6
-rw-r--r--shared/core-utils/users/user-role.ts3
-rw-r--r--shared/models/plugins/server/server-hook.model.ts7
-rw-r--r--shared/models/server/custom-config.model.ts1
-rw-r--r--shared/models/server/server-config.model.ts1
-rw-r--r--shared/models/server/server-error-code.enum.ts10
-rw-r--r--shared/models/users/index.ts2
-rw-r--r--shared/models/users/registration/index.ts5
-rw-r--r--shared/models/users/registration/user-register.model.ts (renamed from shared/models/users/user-register.model.ts)0
-rw-r--r--shared/models/users/registration/user-registration-request.model.ts5
-rw-r--r--shared/models/users/registration/user-registration-state.model.ts5
-rw-r--r--shared/models/users/registration/user-registration-update-state.model.ts4
-rw-r--r--shared/models/users/registration/user-registration.model.ts29
-rw-r--r--shared/models/users/user-notification.model.ts9
-rw-r--r--shared/models/users/user-right.enum.ts4
-rw-r--r--shared/server-commands/miscs/sql-command.ts102
-rw-r--r--shared/server-commands/requests/requests.ts2
-rw-r--r--shared/server-commands/server/config-command.ts50
-rw-r--r--shared/server-commands/server/server.ts3
-rw-r--r--shared/server-commands/users/index.ts1
-rw-r--r--shared/server-commands/users/registrations-command.ts151
-rw-r--r--shared/server-commands/users/users-command.ts29
24 files changed, 338 insertions, 116 deletions
diff --git a/shared/core-utils/common/version.ts b/shared/core-utils/common/version.ts
index 8a64f8c4d..305287233 100644
--- a/shared/core-utils/common/version.ts
+++ b/shared/core-utils/common/version.ts
@@ -1,18 +1,9 @@
1// Thanks https://stackoverflow.com/a/16187766 1// Thanks https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb
2function compareSemVer (a: string, b: string) { 2function compareSemVer (a: string, b: string) {
3 const regExStrip0 = /(\.0+)+$/ 3 if (a.startsWith(b + '-')) return -1
4 const segmentsA = a.replace(regExStrip0, '').split('.') 4 if (b.startsWith(a + '-')) return 1
5 const segmentsB = b.replace(regExStrip0, '').split('.')
6 5
7 const l = Math.min(segmentsA.length, segmentsB.length) 6 return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' })
8
9 for (let i = 0; i < l; i++) {
10 const diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10)
11
12 if (diff) return diff
13 }
14
15 return segmentsA.length - segmentsB.length
16} 7}
17 8
18export { 9export {
diff --git a/shared/core-utils/plugins/hooks.ts b/shared/core-utils/plugins/hooks.ts
index 3784969b5..96bcc945e 100644
--- a/shared/core-utils/plugins/hooks.ts
+++ b/shared/core-utils/plugins/hooks.ts
@@ -1,3 +1,4 @@
1import { RegisteredExternalAuthConfig } from '@shared/models'
1import { HookType } from '../../models/plugins/hook-type.enum' 2import { HookType } from '../../models/plugins/hook-type.enum'
2import { isCatchable, isPromise } from '../common/promises' 3import { isCatchable, isPromise } from '../common/promises'
3 4
@@ -49,7 +50,12 @@ async function internalRunHook <T> (options: {
49 return result 50 return result
50} 51}
51 52
53function getExternalAuthHref (apiUrl: string, auth: RegisteredExternalAuthConfig) {
54 return apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
55}
56
52export { 57export {
53 getHookType, 58 getHookType,
54 internalRunHook 59 internalRunHook,
60 getExternalAuthHref
55} 61}
diff --git a/shared/core-utils/renderer/html.ts b/shared/core-utils/renderer/html.ts
index 502308979..877f2ec55 100644
--- a/shared/core-utils/renderer/html.ts
+++ b/shared/core-utils/renderer/html.ts
@@ -38,7 +38,11 @@ export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[]
38 ...additionalAllowedTags, 38 ...additionalAllowedTags,
39 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img' 39 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img'
40 ], 40 ],
41 allowedSchemes: base.allowedSchemes, 41 allowedSchemes: [
42 ...base.allowedSchemes,
43
44 'mailto'
45 ],
42 allowedAttributes: { 46 allowedAttributes: {
43 ...base.allowedAttributes, 47 ...base.allowedAttributes,
44 48
diff --git a/shared/core-utils/users/user-role.ts b/shared/core-utils/users/user-role.ts
index cc757d779..5f3b9a10f 100644
--- a/shared/core-utils/users/user-role.ts
+++ b/shared/core-utils/users/user-role.ts
@@ -23,7 +23,8 @@ const userRoleRights: { [ id in UserRole ]: UserRight[] } = {
23 UserRight.MANAGE_ACCOUNTS_BLOCKLIST, 23 UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
24 UserRight.MANAGE_SERVERS_BLOCKLIST, 24 UserRight.MANAGE_SERVERS_BLOCKLIST,
25 UserRight.MANAGE_USERS, 25 UserRight.MANAGE_USERS,
26 UserRight.SEE_ALL_COMMENTS 26 UserRight.SEE_ALL_COMMENTS,
27 UserRight.MANAGE_REGISTRATIONS
27 ], 28 ],
28 29
29 [UserRole.USER]: [] 30 [UserRole.USER]: []
diff --git a/shared/models/plugins/server/server-hook.model.ts b/shared/models/plugins/server/server-hook.model.ts
index f11d2050b..dd9cc3ad6 100644
--- a/shared/models/plugins/server/server-hook.model.ts
+++ b/shared/models/plugins/server/server-hook.model.ts
@@ -91,6 +91,10 @@ export const serverFilterHookObject = {
91 // Filter result used to check if a user can register on the instance 91 // Filter result used to check if a user can register on the instance
92 'filter:api.user.signup.allowed.result': true, 92 'filter:api.user.signup.allowed.result': true,
93 93
94 // Filter result used to check if a user can send a registration request on the instance
95 // PeerTube >= 5.1
96 'filter:api.user.request-signup.allowed.result': true,
97
94 // Filter result used to check if video/torrent download is allowed 98 // Filter result used to check if video/torrent download is allowed
95 'filter:api.download.video.allowed.result': true, 99 'filter:api.download.video.allowed.result': true,
96 'filter:api.download.torrent.allowed.result': true, 100 'filter:api.download.torrent.allowed.result': true,
@@ -156,6 +160,9 @@ export const serverActionHookObject = {
156 'action:api.user.unblocked': true, 160 'action:api.user.unblocked': true,
157 // Fired when a user registered on the instance 161 // Fired when a user registered on the instance
158 'action:api.user.registered': true, 162 'action:api.user.registered': true,
163 // Fired when a user requested registration on the instance
164 // PeerTube >= 5.1
165 'action:api.user.requested-registration': true,
159 // Fired when an admin/moderator created a user 166 // Fired when an admin/moderator created a user
160 'action:api.user.created': true, 167 'action:api.user.created': true,
161 // Fired when a user is removed by an admin/moderator 168 // Fired when a user is removed by an admin/moderator
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 7d9d570b1..846bf6159 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -83,6 +83,7 @@ export interface CustomConfig {
83 signup: { 83 signup: {
84 enabled: boolean 84 enabled: boolean
85 limit: number 85 limit: number
86 requiresApproval: boolean
86 requiresEmailVerification: boolean 87 requiresEmailVerification: boolean
87 minimumAge: number 88 minimumAge: number
88 } 89 }
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index 3b6d0597c..d0bd9a00f 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -131,6 +131,7 @@ export interface ServerConfig {
131 allowed: boolean 131 allowed: boolean
132 allowedForCurrentIP: boolean 132 allowedForCurrentIP: boolean
133 requiresEmailVerification: boolean 133 requiresEmailVerification: boolean
134 requiresApproval: boolean
134 minimumAge: number 135 minimumAge: number
135 } 136 }
136 137
diff --git a/shared/models/server/server-error-code.enum.ts b/shared/models/server/server-error-code.enum.ts
index 0e70ea0a7..a39cde1b3 100644
--- a/shared/models/server/server-error-code.enum.ts
+++ b/shared/models/server/server-error-code.enum.ts
@@ -39,7 +39,13 @@ export const enum ServerErrorCode {
39 */ 39 */
40 INCORRECT_FILES_IN_TORRENT = 'incorrect_files_in_torrent', 40 INCORRECT_FILES_IN_TORRENT = 'incorrect_files_in_torrent',
41 41
42 COMMENT_NOT_ASSOCIATED_TO_VIDEO = 'comment_not_associated_to_video' 42 COMMENT_NOT_ASSOCIATED_TO_VIDEO = 'comment_not_associated_to_video',
43
44 MISSING_TWO_FACTOR = 'missing_two_factor',
45 INVALID_TWO_FACTOR = 'invalid_two_factor',
46
47 ACCOUNT_WAITING_FOR_APPROVAL = 'account_waiting_for_approval',
48 ACCOUNT_APPROVAL_REJECTED = 'account_approval_rejected'
43} 49}
44 50
45/** 51/**
@@ -70,5 +76,5 @@ export const enum OAuth2ErrorCode {
70 * 76 *
71 * @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-token-error.js 77 * @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-token-error.js
72 */ 78 */
73 INVALID_TOKEN = 'invalid_token', 79 INVALID_TOKEN = 'invalid_token'
74} 80}
diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts
index 32f7a441c..4a050c870 100644
--- a/shared/models/users/index.ts
+++ b/shared/models/users/index.ts
@@ -1,3 +1,4 @@
1export * from './registration'
1export * from './two-factor-enable-result.model' 2export * from './two-factor-enable-result.model'
2export * from './user-create-result.model' 3export * from './user-create-result.model'
3export * from './user-create.model' 4export * from './user-create.model'
@@ -6,7 +7,6 @@ export * from './user-login.model'
6export * from './user-notification-setting.model' 7export * from './user-notification-setting.model'
7export * from './user-notification.model' 8export * from './user-notification.model'
8export * from './user-refresh-token.model' 9export * from './user-refresh-token.model'
9export * from './user-register.model'
10export * from './user-right.enum' 10export * from './user-right.enum'
11export * from './user-role' 11export * from './user-role'
12export * from './user-scoped-token' 12export * from './user-scoped-token'
diff --git a/shared/models/users/registration/index.ts b/shared/models/users/registration/index.ts
new file mode 100644
index 000000000..593740c4f
--- /dev/null
+++ b/shared/models/users/registration/index.ts
@@ -0,0 +1,5 @@
1export * from './user-register.model'
2export * from './user-registration-request.model'
3export * from './user-registration-state.model'
4export * from './user-registration-update-state.model'
5export * from './user-registration.model'
diff --git a/shared/models/users/user-register.model.ts b/shared/models/users/registration/user-register.model.ts
index cf9a43a67..cf9a43a67 100644
--- a/shared/models/users/user-register.model.ts
+++ b/shared/models/users/registration/user-register.model.ts
diff --git a/shared/models/users/registration/user-registration-request.model.ts b/shared/models/users/registration/user-registration-request.model.ts
new file mode 100644
index 000000000..6c38817e0
--- /dev/null
+++ b/shared/models/users/registration/user-registration-request.model.ts
@@ -0,0 +1,5 @@
1import { UserRegister } from './user-register.model'
2
3export interface UserRegistrationRequest extends UserRegister {
4 registrationReason: string
5}
diff --git a/shared/models/users/registration/user-registration-state.model.ts b/shared/models/users/registration/user-registration-state.model.ts
new file mode 100644
index 000000000..e4c835f78
--- /dev/null
+++ b/shared/models/users/registration/user-registration-state.model.ts
@@ -0,0 +1,5 @@
1export const enum UserRegistrationState {
2 PENDING = 1,
3 REJECTED = 2,
4 ACCEPTED = 3
5}
diff --git a/shared/models/users/registration/user-registration-update-state.model.ts b/shared/models/users/registration/user-registration-update-state.model.ts
new file mode 100644
index 000000000..a1740dcca
--- /dev/null
+++ b/shared/models/users/registration/user-registration-update-state.model.ts
@@ -0,0 +1,4 @@
1export interface UserRegistrationUpdateState {
2 moderationResponse: string
3 preventEmailDelivery?: boolean
4}
diff --git a/shared/models/users/registration/user-registration.model.ts b/shared/models/users/registration/user-registration.model.ts
new file mode 100644
index 000000000..0d74dc28b
--- /dev/null
+++ b/shared/models/users/registration/user-registration.model.ts
@@ -0,0 +1,29 @@
1import { UserRegistrationState } from './user-registration-state.model'
2
3export interface UserRegistration {
4 id: number
5
6 state: {
7 id: UserRegistrationState
8 label: string
9 }
10
11 registrationReason: string
12 moderationResponse: string
13
14 username: string
15 email: string
16 emailVerified: boolean
17
18 accountDisplayName: string
19
20 channelHandle: string
21 channelDisplayName: string
22
23 createdAt: Date
24 updatedAt: Date
25
26 user?: {
27 id: number
28 }
29}
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts
index 0fd7a7181..294c921bd 100644
--- a/shared/models/users/user-notification.model.ts
+++ b/shared/models/users/user-notification.model.ts
@@ -32,7 +32,9 @@ export const enum UserNotificationType {
32 NEW_PLUGIN_VERSION = 17, 32 NEW_PLUGIN_VERSION = 17,
33 NEW_PEERTUBE_VERSION = 18, 33 NEW_PEERTUBE_VERSION = 18,
34 34
35 MY_VIDEO_STUDIO_EDITION_FINISHED = 19 35 MY_VIDEO_STUDIO_EDITION_FINISHED = 19,
36
37 NEW_USER_REGISTRATION_REQUEST = 20
36} 38}
37 39
38export interface VideoInfo { 40export interface VideoInfo {
@@ -126,6 +128,11 @@ export interface UserNotification {
126 latestVersion: string 128 latestVersion: string
127 } 129 }
128 130
131 registration?: {
132 id: number
133 username: string
134 }
135
129 createdAt: string 136 createdAt: string
130 updatedAt: string 137 updatedAt: string
131} 138}
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
index 9c6828aa5..42e5c8cd6 100644
--- a/shared/models/users/user-right.enum.ts
+++ b/shared/models/users/user-right.enum.ts
@@ -43,5 +43,7 @@ export const enum UserRight {
43 MANAGE_VIDEO_FILES = 25, 43 MANAGE_VIDEO_FILES = 25,
44 RUN_VIDEO_TRANSCODING = 26, 44 RUN_VIDEO_TRANSCODING = 26,
45 45
46 MANAGE_VIDEO_IMPORTS = 27 46 MANAGE_VIDEO_IMPORTS = 27,
47
48 MANAGE_REGISTRATIONS = 28
47} 49}
diff --git a/shared/server-commands/miscs/sql-command.ts b/shared/server-commands/miscs/sql-command.ts
index 823fc9e38..35cc2253f 100644
--- a/shared/server-commands/miscs/sql-command.ts
+++ b/shared/server-commands/miscs/sql-command.ts
@@ -13,101 +13,87 @@ export class SQLCommand extends AbstractCommand {
13 return seq.query(`DELETE FROM "${table}"`, options) 13 return seq.query(`DELETE FROM "${table}"`, options)
14 } 14 }
15 15
16 async getCount (table: string) { 16 async getVideoShareCount () {
17 const seq = this.getSequelize() 17 const [ { total } ] = await this.selectQuery<{ total: string }>(`SELECT COUNT(*) as total FROM "videoShare"`)
18
19 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
20
21 const [ { total } ] = await seq.query<{ total: string }>(`SELECT COUNT(*) as total FROM "${table}"`, options)
22 if (total === null) return 0 18 if (total === null) return 0
23 19
24 return parseInt(total, 10) 20 return parseInt(total, 10)
25 } 21 }
26 22
27 async getInternalFileUrl (fileId: number) { 23 async getInternalFileUrl (fileId: number) {
28 return this.selectQuery(`SELECT "fileUrl" FROM "videoFile" WHERE id = ${fileId}`) 24 return this.selectQuery<{ fileUrl: string }>(`SELECT "fileUrl" FROM "videoFile" WHERE id = :fileId`, { fileId })
29 .then(rows => rows[0].fileUrl as string) 25 .then(rows => rows[0].fileUrl)
30 } 26 }
31 27
32 setActorField (to: string, field: string, value: string) { 28 setActorField (to: string, field: string, value: string) {
33 const seq = this.getSequelize() 29 return this.updateQuery(`UPDATE actor SET ${this.escapeColumnName(field)} = :value WHERE url = :to`, { value, to })
34
35 const options = { type: QueryTypes.UPDATE }
36
37 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
38 } 30 }
39 31
40 setVideoField (uuid: string, field: string, value: string) { 32 setVideoField (uuid: string, field: string, value: string) {
41 const seq = this.getSequelize() 33 return this.updateQuery(`UPDATE video SET ${this.escapeColumnName(field)} = :value WHERE uuid = :uuid`, { value, uuid })
42
43 const options = { type: QueryTypes.UPDATE }
44
45 return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
46 } 34 }
47 35
48 setPlaylistField (uuid: string, field: string, value: string) { 36 setPlaylistField (uuid: string, field: string, value: string) {
49 const seq = this.getSequelize() 37 return this.updateQuery(`UPDATE "videoPlaylist" SET ${this.escapeColumnName(field)} = :value WHERE uuid = :uuid`, { value, uuid })
50
51 const options = { type: QueryTypes.UPDATE }
52
53 return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
54 } 38 }
55 39
56 async countVideoViewsOf (uuid: string) { 40 async countVideoViewsOf (uuid: string) {
57 const seq = this.getSequelize()
58
59 const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' + 41 const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
60 `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'` 42 `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = :uuid`
61
62 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
63 const [ { total } ] = await seq.query<{ total: number }>(query, options)
64 43
44 const [ { total } ] = await this.selectQuery<{ total: number }>(query, { uuid })
65 if (!total) return 0 45 if (!total) return 0
66 46
67 return forceNumber(total) 47 return forceNumber(total)
68 } 48 }
69 49
70 getActorImage (filename: string) { 50 getActorImage (filename: string) {
71 return this.selectQuery(`SELECT * FROM "actorImage" WHERE filename = '${filename}'`) 51 return this.selectQuery<{ width: number, height: number }>(`SELECT * FROM "actorImage" WHERE filename = :filename`, { filename })
72 .then(rows => rows[0]) 52 .then(rows => rows[0])
73 } 53 }
74 54
75 selectQuery (query: string) { 55 // ---------------------------------------------------------------------------
76 const seq = this.getSequelize()
77 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
78 56
79 return seq.query<any>(query, options) 57 setPluginVersion (pluginName: string, newVersion: string) {
58 return this.setPluginField(pluginName, 'version', newVersion)
80 } 59 }
81 60
82 updateQuery (query: string) { 61 setPluginLatestVersion (pluginName: string, newVersion: string) {
83 const seq = this.getSequelize() 62 return this.setPluginField(pluginName, 'latestVersion', newVersion)
84 const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE } 63 }
85 64
86 return seq.query(query, options) 65 setPluginField (pluginName: string, field: string, value: string) {
66 return this.updateQuery(
67 `UPDATE "plugin" SET ${this.escapeColumnName(field)} = :value WHERE "name" = :pluginName`,
68 { pluginName, value }
69 )
87 } 70 }
88 71
89 // --------------------------------------------------------------------------- 72 // ---------------------------------------------------------------------------
90 73
91 setPluginField (pluginName: string, field: string, value: string) { 74 selectQuery <T extends object> (query: string, replacements: { [id: string]: string | number } = {}) {
92 const seq = this.getSequelize() 75 const seq = this.getSequelize()
76 const options = {
77 type: QueryTypes.SELECT as QueryTypes.SELECT,
78 replacements
79 }
93 80
94 const options = { type: QueryTypes.UPDATE } 81 return seq.query<T>(query, options)
95
96 return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
97 } 82 }
98 83
99 setPluginVersion (pluginName: string, newVersion: string) { 84 updateQuery (query: string, replacements: { [id: string]: string | number } = {}) {
100 return this.setPluginField(pluginName, 'version', newVersion) 85 const seq = this.getSequelize()
101 } 86 const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE, replacements }
102 87
103 setPluginLatestVersion (pluginName: string, newVersion: string) { 88 return seq.query(query, options)
104 return this.setPluginField(pluginName, 'latestVersion', newVersion)
105 } 89 }
106 90
107 // --------------------------------------------------------------------------- 91 // ---------------------------------------------------------------------------
108 92
109 async getPlaylistInfohash (playlistId: number) { 93 async getPlaylistInfohash (playlistId: number) {
110 const result = await this.selectQuery('SELECT "p2pMediaLoaderInfohashes" FROM "videoStreamingPlaylist" WHERE id = ' + playlistId) 94 const query = 'SELECT "p2pMediaLoaderInfohashes" FROM "videoStreamingPlaylist" WHERE id = :playlistId'
95
96 const result = await this.selectQuery<{ p2pMediaLoaderInfohashes: string }>(query, { playlistId })
111 if (!result || result.length === 0) return [] 97 if (!result || result.length === 0) return []
112 98
113 return result[0].p2pMediaLoaderInfohashes 99 return result[0].p2pMediaLoaderInfohashes
@@ -116,19 +102,14 @@ export class SQLCommand extends AbstractCommand {
116 // --------------------------------------------------------------------------- 102 // ---------------------------------------------------------------------------
117 103
118 setActorFollowScores (newScore: number) { 104 setActorFollowScores (newScore: number) {
119 const seq = this.getSequelize() 105 return this.updateQuery(`UPDATE "actorFollow" SET "score" = :newScore`, { newScore })
120
121 const options = { type: QueryTypes.UPDATE }
122
123 return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options)
124 } 106 }
125 107
126 setTokenField (accessToken: string, field: string, value: string) { 108 setTokenField (accessToken: string, field: string, value: string) {
127 const seq = this.getSequelize() 109 return this.updateQuery(
128 110 `UPDATE "oAuthToken" SET ${this.escapeColumnName(field)} = :value WHERE "accessToken" = :accessToken`,
129 const options = { type: QueryTypes.UPDATE } 111 { value, accessToken }
130 112 )
131 return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options)
132 } 113 }
133 114
134 async cleanup () { 115 async cleanup () {
@@ -157,4 +138,9 @@ export class SQLCommand extends AbstractCommand {
157 return this.sequelize 138 return this.sequelize
158 } 139 }
159 140
141 private escapeColumnName (columnName: string) {
142 return this.getSequelize().escape(columnName)
143 .replace(/^'/, '"')
144 .replace(/'$/, '"')
145 }
160} 146}
diff --git a/shared/server-commands/requests/requests.ts b/shared/server-commands/requests/requests.ts
index dc9cf4e01..cb0e1a5fb 100644
--- a/shared/server-commands/requests/requests.ts
+++ b/shared/server-commands/requests/requests.ts
@@ -199,7 +199,7 @@ function buildRequest (req: request.Test, options: CommonRequestParams) {
199 return req.expect((res) => { 199 return req.expect((res) => {
200 if (options.expectedStatus && res.status !== options.expectedStatus) { 200 if (options.expectedStatus && res.status !== options.expectedStatus) {
201 throw new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` + 201 throw new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` +
202 `\nThe server responded this error: "${res.body?.error ?? res.text}".\n` + 202 `\nThe server responded: "${res.body?.error ?? res.text}".\n` +
203 'You may take a closer look at the logs. To see how to do so, check out this page: ' + 203 'You may take a closer look at the logs. To see how to do so, check out this page: ' +
204 'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs') 204 'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs')
205 } 205 }
diff --git a/shared/server-commands/server/config-command.ts b/shared/server-commands/server/config-command.ts
index 1c2315ed1..eb6bb95a5 100644
--- a/shared/server-commands/server/config-command.ts
+++ b/shared/server-commands/server/config-command.ts
@@ -18,6 +18,33 @@ export class ConfigCommand extends AbstractCommand {
18 } 18 }
19 } 19 }
20 20
21 // ---------------------------------------------------------------------------
22
23 static getEmailOverrideConfig (emailPort: number) {
24 return {
25 smtp: {
26 hostname: '127.0.0.1',
27 port: emailPort
28 }
29 }
30 }
31
32 // ---------------------------------------------------------------------------
33
34 enableSignup (requiresApproval: boolean, limit = -1) {
35 return this.updateExistingSubConfig({
36 newConfig: {
37 signup: {
38 enabled: true,
39 requiresApproval,
40 limit
41 }
42 }
43 })
44 }
45
46 // ---------------------------------------------------------------------------
47
21 disableImports () { 48 disableImports () {
22 return this.setImportsEnabled(false) 49 return this.setImportsEnabled(false)
23 } 50 }
@@ -44,6 +71,16 @@ export class ConfigCommand extends AbstractCommand {
44 }) 71 })
45 } 72 }
46 73
74 // ---------------------------------------------------------------------------
75
76 enableChannelSync () {
77 return this.setChannelSyncEnabled(true)
78 }
79
80 disableChannelSync () {
81 return this.setChannelSyncEnabled(false)
82 }
83
47 private setChannelSyncEnabled (enabled: boolean) { 84 private setChannelSyncEnabled (enabled: boolean) {
48 return this.updateExistingSubConfig({ 85 return this.updateExistingSubConfig({
49 newConfig: { 86 newConfig: {
@@ -56,13 +93,7 @@ export class ConfigCommand extends AbstractCommand {
56 }) 93 })
57 } 94 }
58 95
59 enableChannelSync () { 96 // ---------------------------------------------------------------------------
60 return this.setChannelSyncEnabled(true)
61 }
62
63 disableChannelSync () {
64 return this.setChannelSyncEnabled(false)
65 }
66 97
67 enableLive (options: { 98 enableLive (options: {
68 allowReplay?: boolean 99 allowReplay?: boolean
@@ -142,6 +173,8 @@ export class ConfigCommand extends AbstractCommand {
142 }) 173 })
143 } 174 }
144 175
176 // ---------------------------------------------------------------------------
177
145 enableStudio () { 178 enableStudio () {
146 return this.updateExistingSubConfig({ 179 return this.updateExistingSubConfig({
147 newConfig: { 180 newConfig: {
@@ -152,6 +185,8 @@ export class ConfigCommand extends AbstractCommand {
152 }) 185 })
153 } 186 }
154 187
188 // ---------------------------------------------------------------------------
189
155 getConfig (options: OverrideCommandOptions = {}) { 190 getConfig (options: OverrideCommandOptions = {}) {
156 const path = '/api/v1/config' 191 const path = '/api/v1/config'
157 192
@@ -304,6 +339,7 @@ export class ConfigCommand extends AbstractCommand {
304 signup: { 339 signup: {
305 enabled: false, 340 enabled: false,
306 limit: 5, 341 limit: 5,
342 requiresApproval: true,
307 requiresEmailVerification: false, 343 requiresEmailVerification: false,
308 minimumAge: 16 344 minimumAge: 16
309 }, 345 },
diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts
index ae1395a74..793fae3a8 100644
--- a/shared/server-commands/server/server.ts
+++ b/shared/server-commands/server/server.ts
@@ -18,6 +18,7 @@ import {
18 BlocklistCommand, 18 BlocklistCommand,
19 LoginCommand, 19 LoginCommand,
20 NotificationsCommand, 20 NotificationsCommand,
21 RegistrationsCommand,
21 SubscriptionsCommand, 22 SubscriptionsCommand,
22 TwoFactorCommand, 23 TwoFactorCommand,
23 UsersCommand 24 UsersCommand
@@ -147,6 +148,7 @@ export class PeerTubeServer {
147 views?: ViewsCommand 148 views?: ViewsCommand
148 twoFactor?: TwoFactorCommand 149 twoFactor?: TwoFactorCommand
149 videoToken?: VideoTokenCommand 150 videoToken?: VideoTokenCommand
151 registrations?: RegistrationsCommand
150 152
151 constructor (options: { serverNumber: number } | { url: string }) { 153 constructor (options: { serverNumber: number } | { url: string }) {
152 if ((options as any).url) { 154 if ((options as any).url) {
@@ -430,5 +432,6 @@ export class PeerTubeServer {
430 this.views = new ViewsCommand(this) 432 this.views = new ViewsCommand(this)
431 this.twoFactor = new TwoFactorCommand(this) 433 this.twoFactor = new TwoFactorCommand(this)
432 this.videoToken = new VideoTokenCommand(this) 434 this.videoToken = new VideoTokenCommand(this)
435 this.registrations = new RegistrationsCommand(this)
433 } 436 }
434} 437}
diff --git a/shared/server-commands/users/index.ts b/shared/server-commands/users/index.ts
index 1afc02dc1..404756539 100644
--- a/shared/server-commands/users/index.ts
+++ b/shared/server-commands/users/index.ts
@@ -4,6 +4,7 @@ export * from './blocklist-command'
4export * from './login' 4export * from './login'
5export * from './login-command' 5export * from './login-command'
6export * from './notifications-command' 6export * from './notifications-command'
7export * from './registrations-command'
7export * from './subscriptions-command' 8export * from './subscriptions-command'
8export * from './two-factor-command' 9export * from './two-factor-command'
9export * from './users-command' 10export * from './users-command'
diff --git a/shared/server-commands/users/registrations-command.ts b/shared/server-commands/users/registrations-command.ts
new file mode 100644
index 000000000..f57f54b34
--- /dev/null
+++ b/shared/server-commands/users/registrations-command.ts
@@ -0,0 +1,151 @@
1import { pick } from '@shared/core-utils'
2import { HttpStatusCode, ResultList, UserRegistration, UserRegistrationRequest, UserRegistrationUpdateState } from '@shared/models'
3import { unwrapBody } from '../requests'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class RegistrationsCommand extends AbstractCommand {
7
8 register (options: OverrideCommandOptions & Partial<UserRegistrationRequest> & Pick<UserRegistrationRequest, 'username'>) {
9 const { password = 'password', email = options.username + '@example.com' } = options
10 const path = '/api/v1/users/register'
11
12 return this.postBodyRequest({
13 ...options,
14
15 path,
16 fields: {
17 ...pick(options, [ 'username', 'displayName', 'channel' ]),
18
19 password,
20 email
21 },
22 implicitToken: false,
23 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
24 })
25 }
26
27 requestRegistration (
28 options: OverrideCommandOptions & Partial<UserRegistrationRequest> & Pick<UserRegistrationRequest, 'username' | 'registrationReason'>
29 ) {
30 const { password = 'password', email = options.username + '@example.com' } = options
31 const path = '/api/v1/users/registrations/request'
32
33 return unwrapBody<UserRegistration>(this.postBodyRequest({
34 ...options,
35
36 path,
37 fields: {
38 ...pick(options, [ 'username', 'displayName', 'channel', 'registrationReason' ]),
39
40 password,
41 email
42 },
43 implicitToken: false,
44 defaultExpectedStatus: HttpStatusCode.OK_200
45 }))
46 }
47
48 // ---------------------------------------------------------------------------
49
50 accept (options: OverrideCommandOptions & { id: number } & UserRegistrationUpdateState) {
51 const { id } = options
52 const path = '/api/v1/users/registrations/' + id + '/accept'
53
54 return this.postBodyRequest({
55 ...options,
56
57 path,
58 fields: pick(options, [ 'moderationResponse', 'preventEmailDelivery' ]),
59 implicitToken: true,
60 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
61 })
62 }
63
64 reject (options: OverrideCommandOptions & { id: number } & UserRegistrationUpdateState) {
65 const { id } = options
66 const path = '/api/v1/users/registrations/' + id + '/reject'
67
68 return this.postBodyRequest({
69 ...options,
70
71 path,
72 fields: pick(options, [ 'moderationResponse', 'preventEmailDelivery' ]),
73 implicitToken: true,
74 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
75 })
76 }
77
78 // ---------------------------------------------------------------------------
79
80 delete (options: OverrideCommandOptions & {
81 id: number
82 }) {
83 const { id } = options
84 const path = '/api/v1/users/registrations/' + id
85
86 return this.deleteRequest({
87 ...options,
88
89 path,
90 implicitToken: true,
91 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
92 })
93 }
94
95 // ---------------------------------------------------------------------------
96
97 list (options: OverrideCommandOptions & {
98 start?: number
99 count?: number
100 sort?: string
101 search?: string
102 } = {}) {
103 const path = '/api/v1/users/registrations'
104
105 return this.getRequestBody<ResultList<UserRegistration>>({
106 ...options,
107
108 path,
109 query: pick(options, [ 'start', 'count', 'sort', 'search' ]),
110 implicitToken: true,
111 defaultExpectedStatus: HttpStatusCode.OK_200
112 })
113 }
114
115 // ---------------------------------------------------------------------------
116
117 askSendVerifyEmail (options: OverrideCommandOptions & {
118 email: string
119 }) {
120 const { email } = options
121 const path = '/api/v1/users/registrations/ask-send-verify-email'
122
123 return this.postBodyRequest({
124 ...options,
125
126 path,
127 fields: { email },
128 implicitToken: false,
129 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
130 })
131 }
132
133 verifyEmail (options: OverrideCommandOptions & {
134 registrationId: number
135 verificationString: string
136 }) {
137 const { registrationId, verificationString } = options
138 const path = '/api/v1/users/registrations/' + registrationId + '/verify-email'
139
140 return this.postBodyRequest({
141 ...options,
142
143 path,
144 fields: {
145 verificationString
146 },
147 implicitToken: false,
148 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
149 })
150 }
151}
diff --git a/shared/server-commands/users/users-command.ts b/shared/server-commands/users/users-command.ts
index 811b9685b..8a42fafc8 100644
--- a/shared/server-commands/users/users-command.ts
+++ b/shared/server-commands/users/users-command.ts
@@ -214,35 +214,6 @@ export class UsersCommand extends AbstractCommand {
214 return this.server.login.getAccessToken({ username, password }) 214 return this.server.login.getAccessToken({ username, password })
215 } 215 }
216 216
217 register (options: OverrideCommandOptions & {
218 username: string
219 password?: string
220 displayName?: string
221 email?: string
222 channel?: {
223 name: string
224 displayName: string
225 }
226 }) {
227 const { username, password = 'password', displayName, channel, email = username + '@example.com' } = options
228 const path = '/api/v1/users/register'
229
230 return this.postBodyRequest({
231 ...options,
232
233 path,
234 fields: {
235 username,
236 password,
237 email,
238 displayName,
239 channel
240 },
241 implicitToken: false,
242 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
243 })
244 }
245
246 // --------------------------------------------------------------------------- 217 // ---------------------------------------------------------------------------
247 218
248 getMyInfo (options: OverrideCommandOptions = {}) { 219 getMyInfo (options: OverrideCommandOptions = {}) {