diff options
Diffstat (limited to 'shared')
24 files changed, 343 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 |
2 | function compareSemVer (a: string, b: string) { | 2 | function 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 | ||
18 | export { | 9 | export { |
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 @@ | |||
1 | import { RegisteredExternalAuthConfig } from '@shared/models' | ||
1 | import { HookType } from '../../models/plugins/hook-type.enum' | 2 | import { HookType } from '../../models/plugins/hook-type.enum' |
2 | import { isCatchable, isPromise } from '../common/promises' | 3 | import { 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 | ||
53 | function getExternalAuthHref (apiUrl: string, auth: RegisteredExternalAuthConfig) { | ||
54 | return apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` | ||
55 | } | ||
56 | |||
52 | export { | 57 | export { |
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 @@ | |||
1 | export * from './registration' | ||
1 | export * from './two-factor-enable-result.model' | 2 | export * from './two-factor-enable-result.model' |
2 | export * from './user-create-result.model' | 3 | export * from './user-create-result.model' |
3 | export * from './user-create.model' | 4 | export * from './user-create.model' |
@@ -6,7 +7,6 @@ export * from './user-login.model' | |||
6 | export * from './user-notification-setting.model' | 7 | export * from './user-notification-setting.model' |
7 | export * from './user-notification.model' | 8 | export * from './user-notification.model' |
8 | export * from './user-refresh-token.model' | 9 | export * from './user-refresh-token.model' |
9 | export * from './user-register.model' | ||
10 | export * from './user-right.enum' | 10 | export * from './user-right.enum' |
11 | export * from './user-role' | 11 | export * from './user-role' |
12 | export * from './user-scoped-token' | 12 | export * 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 @@ | |||
1 | export * from './user-register.model' | ||
2 | export * from './user-registration-request.model' | ||
3 | export * from './user-registration-state.model' | ||
4 | export * from './user-registration-update-state.model' | ||
5 | export * 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 @@ | |||
1 | import { UserRegister } from './user-register.model' | ||
2 | |||
3 | export 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 @@ | |||
1 | export 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..636e22c32 --- /dev/null +++ b/shared/models/users/registration/user-registration-update-state.model.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export interface UserRegistrationUpdateState { | ||
2 | moderationResponse: string | ||
3 | } | ||
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 @@ | |||
1 | import { UserRegistrationState } from './user-registration-state.model' | ||
2 | |||
3 | export 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 | ||
38 | export interface VideoInfo { | 40 | export 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..51267b85b 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) { | ||
35 | return this.updateExistingSubConfig({ | ||
36 | newConfig: { | ||
37 | signup: { | ||
38 | enabled: true, | ||
39 | requiresApproval, | ||
40 | limit: -1 | ||
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' | |||
4 | export * from './login' | 4 | export * from './login' |
5 | export * from './login-command' | 5 | export * from './login-command' |
6 | export * from './notifications-command' | 6 | export * from './notifications-command' |
7 | export * from './registrations-command' | ||
7 | export * from './subscriptions-command' | 8 | export * from './subscriptions-command' |
8 | export * from './two-factor-command' | 9 | export * from './two-factor-command' |
9 | export * from './users-command' | 10 | export * 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..4e97571f4 --- /dev/null +++ b/shared/server-commands/users/registrations-command.ts | |||
@@ -0,0 +1,157 @@ | |||
1 | import { pick } from '@shared/core-utils' | ||
2 | import { HttpStatusCode, ResultList, UserRegistration, UserRegistrationRequest } from '@shared/models' | ||
3 | import { unwrapBody } from '../requests' | ||
4 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
5 | |||
6 | export 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 | |||
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 | |||
42 | }, | ||
43 | implicitToken: false, | ||
44 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
45 | })) | ||
46 | } | ||
47 | |||
48 | // --------------------------------------------------------------------------- | ||
49 | |||
50 | accept (options: OverrideCommandOptions & { | ||
51 | id: number | ||
52 | moderationResponse: string | ||
53 | }) { | ||
54 | const { id, moderationResponse } = options | ||
55 | const path = '/api/v1/users/registrations/' + id + '/accept' | ||
56 | |||
57 | return this.postBodyRequest({ | ||
58 | ...options, | ||
59 | |||
60 | path, | ||
61 | fields: { moderationResponse }, | ||
62 | implicitToken: true, | ||
63 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
64 | }) | ||
65 | } | ||
66 | |||
67 | reject (options: OverrideCommandOptions & { | ||
68 | id: number | ||
69 | moderationResponse: string | ||
70 | }) { | ||
71 | const { id, moderationResponse } = options | ||
72 | const path = '/api/v1/users/registrations/' + id + '/reject' | ||
73 | |||
74 | return this.postBodyRequest({ | ||
75 | ...options, | ||
76 | |||
77 | path, | ||
78 | fields: { moderationResponse }, | ||
79 | implicitToken: true, | ||
80 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | // --------------------------------------------------------------------------- | ||
85 | |||
86 | delete (options: OverrideCommandOptions & { | ||
87 | id: number | ||
88 | }) { | ||
89 | const { id } = options | ||
90 | const path = '/api/v1/users/registrations/' + id | ||
91 | |||
92 | return this.deleteRequest({ | ||
93 | ...options, | ||
94 | |||
95 | path, | ||
96 | implicitToken: true, | ||
97 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
98 | }) | ||
99 | } | ||
100 | |||
101 | // --------------------------------------------------------------------------- | ||
102 | |||
103 | list (options: OverrideCommandOptions & { | ||
104 | start?: number | ||
105 | count?: number | ||
106 | sort?: string | ||
107 | search?: string | ||
108 | } = {}) { | ||
109 | const path = '/api/v1/users/registrations' | ||
110 | |||
111 | return this.getRequestBody<ResultList<UserRegistration>>({ | ||
112 | ...options, | ||
113 | |||
114 | path, | ||
115 | query: pick(options, [ 'start', 'count', 'sort', 'search' ]), | ||
116 | implicitToken: true, | ||
117 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
118 | }) | ||
119 | } | ||
120 | |||
121 | // --------------------------------------------------------------------------- | ||
122 | |||
123 | askSendVerifyEmail (options: OverrideCommandOptions & { | ||
124 | email: string | ||
125 | }) { | ||
126 | const { email } = options | ||
127 | const path = '/api/v1/users/registrations/ask-send-verify-email' | ||
128 | |||
129 | return this.postBodyRequest({ | ||
130 | ...options, | ||
131 | |||
132 | path, | ||
133 | fields: { email }, | ||
134 | implicitToken: false, | ||
135 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
136 | }) | ||
137 | } | ||
138 | |||
139 | verifyEmail (options: OverrideCommandOptions & { | ||
140 | registrationId: number | ||
141 | verificationString: string | ||
142 | }) { | ||
143 | const { registrationId, verificationString } = options | ||
144 | const path = '/api/v1/users/registrations/' + registrationId + '/verify-email' | ||
145 | |||
146 | return this.postBodyRequest({ | ||
147 | ...options, | ||
148 | |||
149 | path, | ||
150 | fields: { | ||
151 | verificationString | ||
152 | }, | ||
153 | implicitToken: false, | ||
154 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
155 | }) | ||
156 | } | ||
157 | } | ||
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 = {}) { |