diff options
347 files changed, 8009 insertions, 2986 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65e1acec6..678b0674b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml | |||
@@ -48,6 +48,7 @@ jobs: | |||
48 | ENABLE_OBJECT_STORAGE_TESTS: true | 48 | ENABLE_OBJECT_STORAGE_TESTS: true |
49 | OBJECT_STORAGE_SCALEWAY_KEY_ID: ${{ secrets.OBJECT_STORAGE_SCALEWAY_KEY_ID }} | 49 | OBJECT_STORAGE_SCALEWAY_KEY_ID: ${{ secrets.OBJECT_STORAGE_SCALEWAY_KEY_ID }} |
50 | OBJECT_STORAGE_SCALEWAY_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY }} | 50 | OBJECT_STORAGE_SCALEWAY_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY }} |
51 | YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
51 | 52 | ||
52 | steps: | 53 | steps: |
53 | - uses: actions/checkout@v3 | 54 | - uses: actions/checkout@v3 |
diff --git a/client/.gitignore b/client/.gitignore index 3241b09ed..ca68413c8 100644 --- a/client/.gitignore +++ b/client/.gitignore | |||
@@ -11,5 +11,6 @@ | |||
11 | /src/locale/target/server_*.xml | 11 | /src/locale/target/server_*.xml |
12 | /e2e/local.log | 12 | /e2e/local.log |
13 | /e2e/browserstack.err | 13 | /e2e/browserstack.err |
14 | /e2e/screenshots | ||
14 | /src/standalone/player/build | 15 | /src/standalone/player/build |
15 | /src/standalone/player/dist | 16 | /src/standalone/player/dist |
diff --git a/client/e2e/src/po/admin-config.po.ts b/client/e2e/src/po/admin-config.po.ts index 27957a71f..510037ddd 100644 --- a/client/e2e/src/po/admin-config.po.ts +++ b/client/e2e/src/po/admin-config.po.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { getCheckbox, go } from '../utils' | 1 | import { browserSleep, getCheckbox, go, isCheckboxSelected } from '../utils' |
2 | 2 | ||
3 | export class AdminConfigPage { | 3 | export class AdminConfigPage { |
4 | 4 | ||
@@ -8,7 +8,6 @@ export class AdminConfigPage { | |||
8 | 'basic-configuration': 'APPEARANCE', | 8 | 'basic-configuration': 'APPEARANCE', |
9 | 'instance-information': 'INSTANCE' | 9 | 'instance-information': 'INSTANCE' |
10 | } | 10 | } |
11 | |||
12 | await go('/admin/config/edit-custom#' + tab) | 11 | await go('/admin/config/edit-custom#' + tab) |
13 | 12 | ||
14 | await $('.inner-form-title=' + waitTitles[tab]).waitForDisplayed() | 13 | await $('.inner-form-title=' + waitTitles[tab]).waitForDisplayed() |
@@ -28,17 +27,39 @@ export class AdminConfigPage { | |||
28 | return $('#instanceCustomHomepageContent').setValue(newValue) | 27 | return $('#instanceCustomHomepageContent').setValue(newValue) |
29 | } | 28 | } |
30 | 29 | ||
31 | async toggleSignup () { | 30 | async toggleSignup (enabled: boolean) { |
31 | if (await isCheckboxSelected('signupEnabled') === enabled) return | ||
32 | |||
32 | const checkbox = await getCheckbox('signupEnabled') | 33 | const checkbox = await getCheckbox('signupEnabled') |
33 | 34 | ||
34 | await checkbox.waitForClickable() | 35 | await checkbox.waitForClickable() |
35 | await checkbox.click() | 36 | await checkbox.click() |
36 | } | 37 | } |
37 | 38 | ||
39 | async toggleSignupApproval (required: boolean) { | ||
40 | if (await isCheckboxSelected('signupRequiresApproval') === required) return | ||
41 | |||
42 | const checkbox = await getCheckbox('signupRequiresApproval') | ||
43 | |||
44 | await checkbox.waitForClickable() | ||
45 | await checkbox.click() | ||
46 | } | ||
47 | |||
48 | async toggleSignupEmailVerification (required: boolean) { | ||
49 | if (await isCheckboxSelected('signupRequiresEmailVerification') === required) return | ||
50 | |||
51 | const checkbox = await getCheckbox('signupRequiresEmailVerification') | ||
52 | |||
53 | await checkbox.waitForClickable() | ||
54 | await checkbox.click() | ||
55 | } | ||
56 | |||
38 | async save () { | 57 | async save () { |
39 | const button = $('input[type=submit]') | 58 | const button = $('input[type=submit]') |
40 | 59 | ||
41 | await button.waitForClickable() | 60 | await button.waitForClickable() |
42 | await button.click() | 61 | await button.click() |
62 | |||
63 | await browserSleep(1000) | ||
43 | } | 64 | } |
44 | } | 65 | } |
diff --git a/client/e2e/src/po/admin-registration.po.ts b/client/e2e/src/po/admin-registration.po.ts new file mode 100644 index 000000000..85234654d --- /dev/null +++ b/client/e2e/src/po/admin-registration.po.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import { browserSleep, findParentElement, go } from '../utils' | ||
2 | |||
3 | export class AdminRegistrationPage { | ||
4 | |||
5 | async navigateToRegistratonsList () { | ||
6 | await go('/admin/moderation/registrations/list') | ||
7 | |||
8 | await $('my-registration-list').waitForDisplayed() | ||
9 | } | ||
10 | |||
11 | async accept (username: string, moderationResponse: string) { | ||
12 | const usernameEl = await $('*=' + username) | ||
13 | await usernameEl.waitForDisplayed() | ||
14 | |||
15 | const tr = await findParentElement(usernameEl, async el => await el.getTagName() === 'tr') | ||
16 | |||
17 | await tr.$('.action-cell .dropdown-root').click() | ||
18 | |||
19 | const accept = await $('span*=Accept this registration') | ||
20 | await accept.waitForClickable() | ||
21 | await accept.click() | ||
22 | |||
23 | const moderationResponseTextarea = await $('#moderationResponse') | ||
24 | await moderationResponseTextarea.waitForDisplayed() | ||
25 | |||
26 | await moderationResponseTextarea.setValue(moderationResponse) | ||
27 | |||
28 | const submitButton = $('.modal-footer input[type=submit]') | ||
29 | await submitButton.waitForClickable() | ||
30 | await submitButton.click() | ||
31 | |||
32 | await browserSleep(1000) | ||
33 | } | ||
34 | |||
35 | } | ||
diff --git a/client/e2e/src/po/login.po.ts b/client/e2e/src/po/login.po.ts index bc1854dbc..f1d13a2b0 100644 --- a/client/e2e/src/po/login.po.ts +++ b/client/e2e/src/po/login.po.ts | |||
@@ -6,7 +6,14 @@ export class LoginPage { | |||
6 | 6 | ||
7 | } | 7 | } |
8 | 8 | ||
9 | async login (username: string, password: string, url = '/login') { | 9 | async login (options: { |
10 | username: string | ||
11 | password: string | ||
12 | displayName?: string | ||
13 | url?: string | ||
14 | }) { | ||
15 | const { username, password, url = '/login', displayName = username } = options | ||
16 | |||
10 | await go(url) | 17 | await go(url) |
11 | 18 | ||
12 | await browser.execute(`window.localStorage.setItem('no_account_setup_warning_modal', 'true')`) | 19 | await browser.execute(`window.localStorage.setItem('no_account_setup_warning_modal', 'true')`) |
@@ -27,27 +34,40 @@ export class LoginPage { | |||
27 | 34 | ||
28 | await menuToggle.click() | 35 | await menuToggle.click() |
29 | 36 | ||
30 | await this.ensureIsLoggedInAs(username) | 37 | await this.ensureIsLoggedInAs(displayName) |
31 | 38 | ||
32 | await menuToggle.click() | 39 | await menuToggle.click() |
33 | } else { | 40 | } else { |
34 | await this.ensureIsLoggedInAs(username) | 41 | await this.ensureIsLoggedInAs(displayName) |
35 | } | 42 | } |
36 | } | 43 | } |
37 | 44 | ||
45 | async getLoginError (username: string, password: string) { | ||
46 | await go('/login') | ||
47 | |||
48 | await $('input#username').setValue(username) | ||
49 | await $('input#password').setValue(password) | ||
50 | |||
51 | await browser.pause(1000) | ||
52 | |||
53 | await $('form input[type=submit]').click() | ||
54 | |||
55 | return $('.alert-danger').getText() | ||
56 | } | ||
57 | |||
38 | async loginAsRootUser () { | 58 | async loginAsRootUser () { |
39 | return this.login('root', 'test' + this.getSuffix()) | 59 | return this.login({ username: 'root', password: 'test' + this.getSuffix() }) |
40 | } | 60 | } |
41 | 61 | ||
42 | loginOnPeerTube2 () { | 62 | loginOnPeerTube2 () { |
43 | return this.login('e2e', process.env.PEERTUBE2_E2E_PASSWORD, 'https://peertube2.cpy.re/login') | 63 | return this.login({ username: 'e2e', password: process.env.PEERTUBE2_E2E_PASSWORD, url: 'https://peertube2.cpy.re/login' }) |
44 | } | 64 | } |
45 | 65 | ||
46 | async logout () { | 66 | async logout () { |
47 | const loggedInMore = $('.logged-in-more') | 67 | const loggedInDropdown = $('.logged-in-more .logged-in-info') |
48 | 68 | ||
49 | await loggedInMore.waitForClickable() | 69 | await loggedInDropdown.waitForClickable() |
50 | await loggedInMore.click() | 70 | await loggedInDropdown.click() |
51 | 71 | ||
52 | const logout = $('.dropdown-item*=Log out') | 72 | const logout = $('.dropdown-item*=Log out') |
53 | 73 | ||
diff --git a/client/e2e/src/po/signup.po.ts b/client/e2e/src/po/signup.po.ts index cc2ed7c5f..7917cdda7 100644 --- a/client/e2e/src/po/signup.po.ts +++ b/client/e2e/src/po/signup.po.ts | |||
@@ -27,42 +27,39 @@ export class SignupPage { | |||
27 | return terms.click() | 27 | return terms.click() |
28 | } | 28 | } |
29 | 29 | ||
30 | async getEndMessage () { | ||
31 | const alert = $('.pt-alert-primary') | ||
32 | await alert.waitForDisplayed() | ||
33 | |||
34 | return alert.getText() | ||
35 | } | ||
36 | |||
37 | async fillRegistrationReason (reason: string) { | ||
38 | await $('#registrationReason').setValue(reason) | ||
39 | } | ||
40 | |||
30 | async fillAccountStep (options: { | 41 | async fillAccountStep (options: { |
31 | displayName: string | ||
32 | username: string | 42 | username: string |
33 | email: string | 43 | password?: string |
34 | password: string | 44 | displayName?: string |
45 | email?: string | ||
35 | }) { | 46 | }) { |
36 | if (options.displayName) { | 47 | await $('#displayName').setValue(options.displayName || `${options.username} display name`) |
37 | await $('#displayName').setValue(options.displayName) | ||
38 | } | ||
39 | |||
40 | if (options.username) { | ||
41 | await $('#username').setValue(options.username) | ||
42 | } | ||
43 | 48 | ||
44 | if (options.email) { | 49 | await $('#username').setValue(options.username) |
45 | // Fix weird bug on firefox that "cannot scroll into view" when using just `setValue` | 50 | await $('#password').setValue(options.password || 'password') |
46 | await $('#email').scrollIntoView(false) | ||
47 | await $('#email').waitForClickable() | ||
48 | await $('#email').setValue(options.email) | ||
49 | } | ||
50 | 51 | ||
51 | if (options.password) { | 52 | // Fix weird bug on firefox that "cannot scroll into view" when using just `setValue` |
52 | await $('#password').setValue(options.password) | 53 | await $('#email').scrollIntoView(false) |
53 | } | 54 | await $('#email').waitForClickable() |
55 | await $('#email').setValue(options.email || `${options.username}@example.com`) | ||
54 | } | 56 | } |
55 | 57 | ||
56 | async fillChannelStep (options: { | 58 | async fillChannelStep (options: { |
57 | displayName: string | ||
58 | name: string | 59 | name: string |
60 | displayName?: string | ||
59 | }) { | 61 | }) { |
60 | if (options.displayName) { | 62 | await $('#displayName').setValue(options.displayName || `${options.name} channel display name`) |
61 | await $('#displayName').setValue(options.displayName) | 63 | await $('#name').setValue(options.name) |
62 | } | ||
63 | |||
64 | if (options.name) { | ||
65 | await $('#name').setValue(options.name) | ||
66 | } | ||
67 | } | 64 | } |
68 | } | 65 | } |
diff --git a/client/e2e/src/suites-local/signup.e2e-spec.ts b/client/e2e/src/suites-local/signup.e2e-spec.ts index 4eed3eefe..b6f7ad1a7 100644 --- a/client/e2e/src/suites-local/signup.e2e-spec.ts +++ b/client/e2e/src/suites-local/signup.e2e-spec.ts | |||
@@ -1,12 +1,89 @@ | |||
1 | import { AdminConfigPage } from '../po/admin-config.po' | 1 | import { AdminConfigPage } from '../po/admin-config.po' |
2 | import { AdminRegistrationPage } from '../po/admin-registration.po' | ||
2 | import { LoginPage } from '../po/login.po' | 3 | import { LoginPage } from '../po/login.po' |
3 | import { SignupPage } from '../po/signup.po' | 4 | import { SignupPage } from '../po/signup.po' |
4 | import { isMobileDevice, waitServerUp } from '../utils' | 5 | import { browserSleep, getVerificationLink, go, findEmailTo, isMobileDevice, MockSMTPServer, waitServerUp } from '../utils' |
6 | |||
7 | function checkEndMessage (options: { | ||
8 | message: string | ||
9 | requiresEmailVerification: boolean | ||
10 | requiresApproval: boolean | ||
11 | afterEmailVerification: boolean | ||
12 | }) { | ||
13 | const { message, requiresApproval, requiresEmailVerification, afterEmailVerification } = options | ||
14 | |||
15 | { | ||
16 | const created = 'account has been created' | ||
17 | const request = 'account request has been sent' | ||
18 | |||
19 | if (requiresApproval) { | ||
20 | expect(message).toContain(request) | ||
21 | expect(message).not.toContain(created) | ||
22 | } else { | ||
23 | expect(message).not.toContain(request) | ||
24 | expect(message).toContain(created) | ||
25 | } | ||
26 | } | ||
27 | |||
28 | { | ||
29 | const checkEmail = 'Check your emails' | ||
30 | |||
31 | if (requiresEmailVerification) { | ||
32 | expect(message).toContain(checkEmail) | ||
33 | } else { | ||
34 | expect(message).not.toContain(checkEmail) | ||
35 | |||
36 | const moderatorsApproval = 'moderator will check your registration request' | ||
37 | if (requiresApproval) { | ||
38 | expect(message).toContain(moderatorsApproval) | ||
39 | } else { | ||
40 | expect(message).not.toContain(moderatorsApproval) | ||
41 | } | ||
42 | } | ||
43 | } | ||
44 | |||
45 | { | ||
46 | const emailVerified = 'email has been verified' | ||
47 | |||
48 | if (afterEmailVerification) { | ||
49 | expect(message).toContain(emailVerified) | ||
50 | } else { | ||
51 | expect(message).not.toContain(emailVerified) | ||
52 | } | ||
53 | } | ||
54 | } | ||
5 | 55 | ||
6 | describe('Signup', () => { | 56 | describe('Signup', () => { |
7 | let loginPage: LoginPage | 57 | let loginPage: LoginPage |
8 | let adminConfigPage: AdminConfigPage | 58 | let adminConfigPage: AdminConfigPage |
9 | let signupPage: SignupPage | 59 | let signupPage: SignupPage |
60 | let adminRegistrationPage: AdminRegistrationPage | ||
61 | |||
62 | async function prepareSignup (options: { | ||
63 | enabled: boolean | ||
64 | requiresApproval?: boolean | ||
65 | requiresEmailVerification?: boolean | ||
66 | }) { | ||
67 | await loginPage.loginAsRootUser() | ||
68 | |||
69 | await adminConfigPage.navigateTo('basic-configuration') | ||
70 | await adminConfigPage.toggleSignup(options.enabled) | ||
71 | |||
72 | if (options.enabled) { | ||
73 | if (options.requiresApproval !== undefined) { | ||
74 | await adminConfigPage.toggleSignupApproval(options.requiresApproval) | ||
75 | } | ||
76 | |||
77 | if (options.requiresEmailVerification !== undefined) { | ||
78 | await adminConfigPage.toggleSignupEmailVerification(options.requiresEmailVerification) | ||
79 | } | ||
80 | } | ||
81 | |||
82 | await adminConfigPage.save() | ||
83 | |||
84 | await loginPage.logout() | ||
85 | await browser.refresh() | ||
86 | } | ||
10 | 87 | ||
11 | before(async () => { | 88 | before(async () => { |
12 | await waitServerUp() | 89 | await waitServerUp() |
@@ -16,72 +93,310 @@ describe('Signup', () => { | |||
16 | loginPage = new LoginPage(isMobileDevice()) | 93 | loginPage = new LoginPage(isMobileDevice()) |
17 | adminConfigPage = new AdminConfigPage() | 94 | adminConfigPage = new AdminConfigPage() |
18 | signupPage = new SignupPage() | 95 | signupPage = new SignupPage() |
96 | adminRegistrationPage = new AdminRegistrationPage() | ||
19 | 97 | ||
20 | await browser.maximizeWindow() | 98 | await browser.maximizeWindow() |
21 | }) | 99 | }) |
22 | 100 | ||
23 | it('Should disable signup', async () => { | 101 | describe('Signup disabled', function () { |
24 | await loginPage.loginAsRootUser() | 102 | it('Should disable signup', async () => { |
103 | await prepareSignup({ enabled: false }) | ||
25 | 104 | ||
26 | await adminConfigPage.navigateTo('basic-configuration') | 105 | await expect(signupPage.getRegisterMenuButton()).not.toBeDisplayed() |
27 | await adminConfigPage.toggleSignup() | 106 | }) |
107 | }) | ||
28 | 108 | ||
29 | await adminConfigPage.save() | 109 | describe('Email verification disabled', function () { |
30 | 110 | ||
31 | await loginPage.logout() | 111 | describe('Direct registration', function () { |
32 | await browser.refresh() | ||
33 | 112 | ||
34 | expect(signupPage.getRegisterMenuButton()).not.toBeDisplayed() | 113 | it('Should enable signup without approval', async () => { |
35 | }) | 114 | await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: false }) |
36 | 115 | ||
37 | it('Should enable signup', async () => { | 116 | await signupPage.getRegisterMenuButton().waitForDisplayed() |
38 | await loginPage.loginAsRootUser() | 117 | }) |
39 | 118 | ||
40 | await adminConfigPage.navigateTo('basic-configuration') | 119 | it('Should go on signup page', async function () { |
41 | await adminConfigPage.toggleSignup() | 120 | await signupPage.clickOnRegisterInMenu() |
121 | }) | ||
42 | 122 | ||
43 | await adminConfigPage.save() | 123 | it('Should validate the first step (about page)', async function () { |
124 | await signupPage.validateStep() | ||
125 | }) | ||
44 | 126 | ||
45 | await loginPage.logout() | 127 | it('Should validate the second step (terms)', async function () { |
46 | await browser.refresh() | 128 | await signupPage.checkTerms() |
129 | await signupPage.validateStep() | ||
130 | }) | ||
47 | 131 | ||
48 | expect(signupPage.getRegisterMenuButton()).toBeDisplayed() | 132 | it('Should validate the third step (account)', async function () { |
49 | }) | 133 | await signupPage.fillAccountStep({ username: 'user_1', displayName: 'user_1_dn' }) |
50 | 134 | ||
51 | it('Should go on signup page', async function () { | 135 | await signupPage.validateStep() |
52 | await signupPage.clickOnRegisterInMenu() | 136 | }) |
53 | }) | ||
54 | 137 | ||
55 | it('Should validate the first step (about page)', async function () { | 138 | it('Should validate the third step (channel)', async function () { |
56 | await signupPage.validateStep() | 139 | await signupPage.fillChannelStep({ name: 'user_1_channel' }) |
57 | }) | ||
58 | 140 | ||
59 | it('Should validate the second step (terms)', async function () { | 141 | await signupPage.validateStep() |
60 | await signupPage.checkTerms() | 142 | }) |
61 | await signupPage.validateStep() | 143 | |
62 | }) | 144 | it('Should be logged in', async function () { |
145 | await loginPage.ensureIsLoggedInAs('user_1_dn') | ||
146 | }) | ||
147 | |||
148 | it('Should have a valid end message', async function () { | ||
149 | const message = await signupPage.getEndMessage() | ||
150 | |||
151 | checkEndMessage({ | ||
152 | message, | ||
153 | requiresEmailVerification: false, | ||
154 | requiresApproval: false, | ||
155 | afterEmailVerification: false | ||
156 | }) | ||
63 | 157 | ||
64 | it('Should validate the third step (account)', async function () { | 158 | await browser.saveScreenshot('./screenshots/direct-without-email.png') |
65 | await signupPage.fillAccountStep({ | 159 | |
66 | displayName: 'user 1', | 160 | await loginPage.logout() |
67 | username: 'user_1', | 161 | }) |
68 | email: 'user_1@example.com', | ||
69 | password: 'my_super_password' | ||
70 | }) | 162 | }) |
71 | 163 | ||
72 | await signupPage.validateStep() | 164 | describe('Registration with approval', function () { |
165 | |||
166 | it('Should enable signup with approval', async () => { | ||
167 | await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: false }) | ||
168 | |||
169 | await signupPage.getRegisterMenuButton().waitForDisplayed() | ||
170 | }) | ||
171 | |||
172 | it('Should go on signup page', async function () { | ||
173 | await signupPage.clickOnRegisterInMenu() | ||
174 | }) | ||
175 | |||
176 | it('Should validate the first step (about page)', async function () { | ||
177 | await signupPage.validateStep() | ||
178 | }) | ||
179 | |||
180 | it('Should validate the second step (terms)', async function () { | ||
181 | await signupPage.checkTerms() | ||
182 | await signupPage.fillRegistrationReason('my super reason') | ||
183 | await signupPage.validateStep() | ||
184 | }) | ||
185 | |||
186 | it('Should validate the third step (account)', async function () { | ||
187 | await signupPage.fillAccountStep({ username: 'user_2', displayName: 'user_2 display name', password: 'password' }) | ||
188 | await signupPage.validateStep() | ||
189 | }) | ||
190 | |||
191 | it('Should validate the third step (channel)', async function () { | ||
192 | await signupPage.fillChannelStep({ name: 'user_2_channel' }) | ||
193 | await signupPage.validateStep() | ||
194 | }) | ||
195 | |||
196 | it('Should have a valid end message', async function () { | ||
197 | const message = await signupPage.getEndMessage() | ||
198 | |||
199 | checkEndMessage({ | ||
200 | message, | ||
201 | requiresEmailVerification: false, | ||
202 | requiresApproval: true, | ||
203 | afterEmailVerification: false | ||
204 | }) | ||
205 | |||
206 | await browser.saveScreenshot('./screenshots/request-without-email.png') | ||
207 | }) | ||
208 | |||
209 | it('Should display a message when trying to login with this account', async function () { | ||
210 | const error = await loginPage.getLoginError('user_2', 'password') | ||
211 | |||
212 | expect(error).toContain('awaiting approval') | ||
213 | }) | ||
214 | |||
215 | it('Should accept the registration', async function () { | ||
216 | await loginPage.loginAsRootUser() | ||
217 | |||
218 | await adminRegistrationPage.navigateToRegistratonsList() | ||
219 | await adminRegistrationPage.accept('user_2', 'moderation response') | ||
220 | |||
221 | await loginPage.logout() | ||
222 | }) | ||
223 | |||
224 | it('Should be able to login with this new account', async function () { | ||
225 | await loginPage.login({ username: 'user_2', password: 'password', displayName: 'user_2 display name' }) | ||
226 | |||
227 | await loginPage.logout() | ||
228 | }) | ||
229 | }) | ||
73 | }) | 230 | }) |
74 | 231 | ||
75 | it('Should validate the third step (channel)', async function () { | 232 | describe('Email verification enabled', function () { |
76 | await signupPage.fillChannelStep({ | 233 | const emails: any[] = [] |
77 | displayName: 'user 1 channel', | 234 | let emailPort: number |
78 | name: 'user_1_channel' | 235 | |
236 | before(async () => { | ||
237 | // FIXME: typings are wrong, get returns a promise | ||
238 | emailPort = await browser.sharedStore.get('emailPort') as unknown as number | ||
239 | |||
240 | MockSMTPServer.Instance.collectEmails(emailPort, emails) | ||
79 | }) | 241 | }) |
80 | 242 | ||
81 | await signupPage.validateStep() | 243 | describe('Direct registration', function () { |
82 | }) | 244 | |
245 | it('Should enable signup without approval', async () => { | ||
246 | await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: true }) | ||
247 | |||
248 | await signupPage.getRegisterMenuButton().waitForDisplayed() | ||
249 | }) | ||
250 | |||
251 | it('Should go on signup page', async function () { | ||
252 | await signupPage.clickOnRegisterInMenu() | ||
253 | }) | ||
254 | |||
255 | it('Should validate the first step (about page)', async function () { | ||
256 | await signupPage.validateStep() | ||
257 | }) | ||
258 | |||
259 | it('Should validate the second step (terms)', async function () { | ||
260 | await signupPage.checkTerms() | ||
261 | await signupPage.validateStep() | ||
262 | }) | ||
263 | |||
264 | it('Should validate the third step (account)', async function () { | ||
265 | await signupPage.fillAccountStep({ username: 'user_3', displayName: 'user_3 display name', email: 'user_3@example.com' }) | ||
266 | |||
267 | await signupPage.validateStep() | ||
268 | }) | ||
269 | |||
270 | it('Should validate the third step (channel)', async function () { | ||
271 | await signupPage.fillChannelStep({ name: 'user_3_channel' }) | ||
272 | |||
273 | await signupPage.validateStep() | ||
274 | }) | ||
275 | |||
276 | it('Should have a valid end message', async function () { | ||
277 | const message = await signupPage.getEndMessage() | ||
278 | |||
279 | checkEndMessage({ | ||
280 | message, | ||
281 | requiresEmailVerification: true, | ||
282 | requiresApproval: false, | ||
283 | afterEmailVerification: false | ||
284 | }) | ||
285 | |||
286 | await browser.saveScreenshot('./screenshots/direct-with-email.png') | ||
287 | }) | ||
288 | |||
289 | it('Should validate the email', async function () { | ||
290 | let email: { text: string } | ||
291 | |||
292 | while (!(email = findEmailTo(emails, 'user_3@example.com'))) { | ||
293 | await browserSleep(100) | ||
294 | } | ||
295 | |||
296 | await go(getVerificationLink(email)) | ||
297 | |||
298 | const message = await signupPage.getEndMessage() | ||
299 | |||
300 | checkEndMessage({ | ||
301 | message, | ||
302 | requiresEmailVerification: false, | ||
303 | requiresApproval: false, | ||
304 | afterEmailVerification: true | ||
305 | }) | ||
83 | 306 | ||
84 | it('Should be logged in', async function () { | 307 | await browser.saveScreenshot('./screenshots/direct-after-email.png') |
85 | await loginPage.ensureIsLoggedInAs('user 1') | 308 | }) |
309 | }) | ||
310 | |||
311 | describe('Registration with approval', function () { | ||
312 | |||
313 | it('Should enable signup without approval', async () => { | ||
314 | await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: true }) | ||
315 | |||
316 | await signupPage.getRegisterMenuButton().waitForDisplayed() | ||
317 | }) | ||
318 | |||
319 | it('Should go on signup page', async function () { | ||
320 | await signupPage.clickOnRegisterInMenu() | ||
321 | }) | ||
322 | |||
323 | it('Should validate the first step (about page)', async function () { | ||
324 | await signupPage.validateStep() | ||
325 | }) | ||
326 | |||
327 | it('Should validate the second step (terms)', async function () { | ||
328 | await signupPage.checkTerms() | ||
329 | await signupPage.fillRegistrationReason('my super reason 2') | ||
330 | await signupPage.validateStep() | ||
331 | }) | ||
332 | |||
333 | it('Should validate the third step (account)', async function () { | ||
334 | await signupPage.fillAccountStep({ | ||
335 | username: 'user_4', | ||
336 | displayName: 'user_4 display name', | ||
337 | email: 'user_4@example.com', | ||
338 | password: 'password' | ||
339 | }) | ||
340 | await signupPage.validateStep() | ||
341 | }) | ||
342 | |||
343 | it('Should validate the third step (channel)', async function () { | ||
344 | await signupPage.fillChannelStep({ name: 'user_4_channel' }) | ||
345 | await signupPage.validateStep() | ||
346 | }) | ||
347 | |||
348 | it('Should have a valid end message', async function () { | ||
349 | const message = await signupPage.getEndMessage() | ||
350 | |||
351 | checkEndMessage({ | ||
352 | message, | ||
353 | requiresEmailVerification: true, | ||
354 | requiresApproval: true, | ||
355 | afterEmailVerification: false | ||
356 | }) | ||
357 | |||
358 | await browser.saveScreenshot('./screenshots/request-with-email.png') | ||
359 | }) | ||
360 | |||
361 | it('Should display a message when trying to login with this account', async function () { | ||
362 | const error = await loginPage.getLoginError('user_4', 'password') | ||
363 | |||
364 | expect(error).toContain('awaiting approval') | ||
365 | }) | ||
366 | |||
367 | it('Should accept the registration', async function () { | ||
368 | await loginPage.loginAsRootUser() | ||
369 | |||
370 | await adminRegistrationPage.navigateToRegistratonsList() | ||
371 | await adminRegistrationPage.accept('user_4', 'moderation response 2') | ||
372 | |||
373 | await loginPage.logout() | ||
374 | }) | ||
375 | |||
376 | it('Should validate the email', async function () { | ||
377 | let email: { text: string } | ||
378 | |||
379 | while (!(email = findEmailTo(emails, 'user_4@example.com'))) { | ||
380 | await browserSleep(100) | ||
381 | } | ||
382 | |||
383 | await go(getVerificationLink(email)) | ||
384 | |||
385 | const message = await signupPage.getEndMessage() | ||
386 | |||
387 | checkEndMessage({ | ||
388 | message, | ||
389 | requiresEmailVerification: false, | ||
390 | requiresApproval: true, | ||
391 | afterEmailVerification: true | ||
392 | }) | ||
393 | |||
394 | await browser.saveScreenshot('./screenshots/request-after-email.png') | ||
395 | }) | ||
396 | }) | ||
397 | |||
398 | before(() => { | ||
399 | MockSMTPServer.Instance.kill() | ||
400 | }) | ||
86 | }) | 401 | }) |
87 | }) | 402 | }) |
diff --git a/client/e2e/src/utils/elements.ts b/client/e2e/src/utils/elements.ts index b0ddd5a65..d9435e520 100644 --- a/client/e2e/src/utils/elements.ts +++ b/client/e2e/src/utils/elements.ts | |||
@@ -5,6 +5,10 @@ async function getCheckbox (name: string) { | |||
5 | return input.parentElement() | 5 | return input.parentElement() |
6 | } | 6 | } |
7 | 7 | ||
8 | function isCheckboxSelected (name: string) { | ||
9 | return $(`input[id=${name}]`).isSelected() | ||
10 | } | ||
11 | |||
8 | async function selectCustomSelect (id: string, valueLabel: string) { | 12 | async function selectCustomSelect (id: string, valueLabel: string) { |
9 | const wrapper = $(`[formcontrolname=${id}] .ng-arrow-wrapper`) | 13 | const wrapper = $(`[formcontrolname=${id}] .ng-arrow-wrapper`) |
10 | 14 | ||
@@ -22,7 +26,18 @@ async function selectCustomSelect (id: string, valueLabel: string) { | |||
22 | return option.click() | 26 | return option.click() |
23 | } | 27 | } |
24 | 28 | ||
29 | async function findParentElement ( | ||
30 | el: WebdriverIO.Element, | ||
31 | finder: (el: WebdriverIO.Element) => Promise<boolean> | ||
32 | ) { | ||
33 | if (await finder(el) === true) return el | ||
34 | |||
35 | return findParentElement(await el.parentElement(), finder) | ||
36 | } | ||
37 | |||
25 | export { | 38 | export { |
26 | getCheckbox, | 39 | getCheckbox, |
27 | selectCustomSelect | 40 | isCheckboxSelected, |
41 | selectCustomSelect, | ||
42 | findParentElement | ||
28 | } | 43 | } |
diff --git a/client/e2e/src/utils/email.ts b/client/e2e/src/utils/email.ts new file mode 100644 index 000000000..2ad120333 --- /dev/null +++ b/client/e2e/src/utils/email.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | function getVerificationLink (email: { text: string }) { | ||
2 | const { text } = email | ||
3 | |||
4 | const regexp = /\[(?<link>http:\/\/[^\]]+)\]/g | ||
5 | const matched = text.matchAll(regexp) | ||
6 | |||
7 | if (!matched) throw new Error('Could not find verification link in email') | ||
8 | |||
9 | for (const match of matched) { | ||
10 | const link = match.groups.link | ||
11 | |||
12 | if (link.includes('/verify-account/')) return link | ||
13 | } | ||
14 | |||
15 | throw new Error('Could not find /verify-account/ link') | ||
16 | } | ||
17 | |||
18 | function findEmailTo (emails: { text: string, to: { address: string }[] }[], to: string) { | ||
19 | for (const email of emails) { | ||
20 | for (const { address } of email.to) { | ||
21 | if (address === to) return email | ||
22 | } | ||
23 | } | ||
24 | |||
25 | return undefined | ||
26 | } | ||
27 | |||
28 | export { | ||
29 | getVerificationLink, | ||
30 | findEmailTo | ||
31 | } | ||
diff --git a/client/e2e/src/utils/hooks.ts b/client/e2e/src/utils/hooks.ts index 889cf1d86..7fe247681 100644 --- a/client/e2e/src/utils/hooks.ts +++ b/client/e2e/src/utils/hooks.ts | |||
@@ -1,10 +1,13 @@ | |||
1 | import { ChildProcessWithoutNullStreams } from 'child_process' | 1 | import { ChildProcessWithoutNullStreams } from 'child_process' |
2 | import { basename } from 'path' | 2 | import { basename } from 'path' |
3 | import { runCommand, runServer } from './server' | 3 | import { runCommand, runServer } from './server' |
4 | import { setValue } from '@wdio/shared-store-service' | ||
4 | 5 | ||
5 | let appInstance: string | 6 | let appInstance: number |
6 | let app: ChildProcessWithoutNullStreams | 7 | let app: ChildProcessWithoutNullStreams |
7 | 8 | ||
9 | let emailPort: number | ||
10 | |||
8 | async function beforeLocalSuite (suite: any) { | 11 | async function beforeLocalSuite (suite: any) { |
9 | const config = buildConfig(suite.file) | 12 | const config = buildConfig(suite.file) |
10 | 13 | ||
@@ -17,13 +20,20 @@ function afterLocalSuite () { | |||
17 | app = undefined | 20 | app = undefined |
18 | } | 21 | } |
19 | 22 | ||
20 | function beforeLocalSession (config: { baseUrl: string }, capabilities: { browserName: string }) { | 23 | async function beforeLocalSession (config: { baseUrl: string }, capabilities: { browserName: string }) { |
21 | appInstance = capabilities['browserName'] === 'chrome' ? '1' : '2' | 24 | appInstance = capabilities['browserName'] === 'chrome' |
25 | ? 1 | ||
26 | : 2 | ||
27 | |||
28 | emailPort = 1025 + appInstance | ||
29 | |||
22 | config.baseUrl = 'http://localhost:900' + appInstance | 30 | config.baseUrl = 'http://localhost:900' + appInstance |
31 | |||
32 | await setValue('emailPort', emailPort) | ||
23 | } | 33 | } |
24 | 34 | ||
25 | async function onBrowserStackPrepare () { | 35 | async function onBrowserStackPrepare () { |
26 | const appInstance = '1' | 36 | const appInstance = 1 |
27 | 37 | ||
28 | await runCommand('npm run clean:server:test -- ' + appInstance) | 38 | await runCommand('npm run clean:server:test -- ' + appInstance) |
29 | app = runServer(appInstance) | 39 | app = runServer(appInstance) |
@@ -71,7 +81,11 @@ function buildConfig (suiteFile: string = undefined) { | |||
71 | if (filename === 'signup.e2e-spec.ts') { | 81 | if (filename === 'signup.e2e-spec.ts') { |
72 | return { | 82 | return { |
73 | signup: { | 83 | signup: { |
74 | enabled: true | 84 | limit: -1 |
85 | }, | ||
86 | smtp: { | ||
87 | hostname: '127.0.0.1', | ||
88 | port: emailPort | ||
75 | } | 89 | } |
76 | } | 90 | } |
77 | } | 91 | } |
diff --git a/client/e2e/src/utils/index.ts b/client/e2e/src/utils/index.ts index 354352ee2..420fd239e 100644 --- a/client/e2e/src/utils/index.ts +++ b/client/e2e/src/utils/index.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | export * from './common' | 1 | export * from './common' |
2 | export * from './elements' | 2 | export * from './elements' |
3 | export * from './email' | ||
3 | export * from './hooks' | 4 | export * from './hooks' |
5 | export * from './mock-smtp' | ||
4 | export * from './server' | 6 | export * from './server' |
5 | export * from './urls' | 7 | export * from './urls' |
diff --git a/client/e2e/src/utils/mock-smtp.ts b/client/e2e/src/utils/mock-smtp.ts new file mode 100644 index 000000000..614477d7d --- /dev/null +++ b/client/e2e/src/utils/mock-smtp.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | import { ChildProcess } from 'child_process' | ||
2 | import MailDev from '@peertube/maildev' | ||
3 | |||
4 | class MockSMTPServer { | ||
5 | |||
6 | private static instance: MockSMTPServer | ||
7 | private started = false | ||
8 | private emailChildProcess: ChildProcess | ||
9 | private emails: object[] | ||
10 | |||
11 | collectEmails (port: number, emailsCollection: object[]) { | ||
12 | return new Promise<number>((res, rej) => { | ||
13 | this.emails = emailsCollection | ||
14 | |||
15 | if (this.started) { | ||
16 | return res(undefined) | ||
17 | } | ||
18 | |||
19 | const maildev = new MailDev({ | ||
20 | ip: '127.0.0.1', | ||
21 | smtp: port, | ||
22 | disableWeb: true, | ||
23 | silent: true | ||
24 | }) | ||
25 | |||
26 | maildev.on('new', email => { | ||
27 | this.emails.push(email) | ||
28 | }) | ||
29 | |||
30 | maildev.listen(err => { | ||
31 | if (err) return rej(err) | ||
32 | |||
33 | this.started = true | ||
34 | |||
35 | return res(port) | ||
36 | }) | ||
37 | }) | ||
38 | } | ||
39 | |||
40 | kill () { | ||
41 | if (!this.emailChildProcess) return | ||
42 | |||
43 | process.kill(this.emailChildProcess.pid) | ||
44 | |||
45 | this.emailChildProcess = null | ||
46 | MockSMTPServer.instance = null | ||
47 | } | ||
48 | |||
49 | static get Instance () { | ||
50 | return this.instance || (this.instance = new this()) | ||
51 | } | ||
52 | } | ||
53 | |||
54 | // --------------------------------------------------------------------------- | ||
55 | |||
56 | export { | ||
57 | MockSMTPServer | ||
58 | } | ||
diff --git a/client/e2e/src/utils/server.ts b/client/e2e/src/utils/server.ts index 140054794..227f4aea6 100644 --- a/client/e2e/src/utils/server.ts +++ b/client/e2e/src/utils/server.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { exec, spawn } from 'child_process' | 1 | import { exec, spawn } from 'child_process' |
2 | import { join, resolve } from 'path' | 2 | import { join, resolve } from 'path' |
3 | 3 | ||
4 | function runServer (appInstance: string, config: any = {}) { | 4 | function runServer (appInstance: number, config: any = {}) { |
5 | const env = Object.create(process.env) | 5 | const env = Object.create(process.env) |
6 | env['NODE_ENV'] = 'test' | 6 | env['NODE_ENV'] = 'test' |
7 | env['NODE_APP_INSTANCE'] = appInstance | 7 | env['NODE_APP_INSTANCE'] = appInstance + '' |
8 | 8 | ||
9 | env['NODE_CONFIG'] = JSON.stringify({ | 9 | env['NODE_CONFIG'] = JSON.stringify({ |
10 | rates_limit: { | 10 | rates_limit: { |
diff --git a/client/e2e/wdio.local-test.conf.ts b/client/e2e/wdio.local-test.conf.ts index ca0bb5bfe..bc15123a0 100644 --- a/client/e2e/wdio.local-test.conf.ts +++ b/client/e2e/wdio.local-test.conf.ts | |||
@@ -37,7 +37,7 @@ module.exports = { | |||
37 | // } | 37 | // } |
38 | ], | 38 | ], |
39 | 39 | ||
40 | services: [ 'chromedriver', 'geckodriver' ], | 40 | services: [ 'chromedriver', 'geckodriver', 'shared-store' ], |
41 | 41 | ||
42 | beforeSession: beforeLocalSession, | 42 | beforeSession: beforeLocalSession, |
43 | beforeSuite: beforeLocalSuite, | 43 | beforeSuite: beforeLocalSuite, |
diff --git a/client/e2e/wdio.local.conf.ts b/client/e2e/wdio.local.conf.ts index d02679e06..27c6e867b 100644 --- a/client/e2e/wdio.local.conf.ts +++ b/client/e2e/wdio.local.conf.ts | |||
@@ -33,7 +33,7 @@ module.exports = { | |||
33 | } | 33 | } |
34 | ], | 34 | ], |
35 | 35 | ||
36 | services: [ 'chromedriver', 'geckodriver' ], | 36 | services: [ 'chromedriver', 'geckodriver', 'shared-store' ], |
37 | 37 | ||
38 | beforeSession: beforeLocalSession, | 38 | beforeSession: beforeLocalSession, |
39 | beforeSuite: beforeLocalSuite, | 39 | beforeSuite: beforeLocalSuite, |
diff --git a/client/package.json b/client/package.json index 115a4a199..31d9b1e7c 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -52,8 +52,9 @@ | |||
52 | "@ngx-loading-bar/core": "^6.0.0", | 52 | "@ngx-loading-bar/core": "^6.0.0", |
53 | "@ngx-loading-bar/http-client": "^6.0.0", | 53 | "@ngx-loading-bar/http-client": "^6.0.0", |
54 | "@ngx-loading-bar/router": "^6.0.0", | 54 | "@ngx-loading-bar/router": "^6.0.0", |
55 | "@peertube/p2p-media-loader-core": "^1.0.13", | 55 | "@peertube/maildev": "^1.2.0", |
56 | "@peertube/p2p-media-loader-hlsjs": "^1.0.13", | 56 | "@peertube/p2p-media-loader-core": "^1.0.14", |
57 | "@peertube/p2p-media-loader-hlsjs": "^1.0.14", | ||
57 | "@peertube/videojs-contextmenu": "^5.5.0", | 58 | "@peertube/videojs-contextmenu": "^5.5.0", |
58 | "@peertube/xliffmerge": "^2.0.3", | 59 | "@peertube/xliffmerge": "^2.0.3", |
59 | "@popperjs/core": "^2.11.5", | 60 | "@popperjs/core": "^2.11.5", |
@@ -75,6 +76,7 @@ | |||
75 | "@wdio/cli": "^7.25.2", | 76 | "@wdio/cli": "^7.25.2", |
76 | "@wdio/local-runner": "^7.25.2", | 77 | "@wdio/local-runner": "^7.25.2", |
77 | "@wdio/mocha-framework": "^7.25.2", | 78 | "@wdio/mocha-framework": "^7.25.2", |
79 | "@wdio/shared-store-service": "^7.25.2", | ||
78 | "@wdio/spec-reporter": "^7.25.1", | 80 | "@wdio/spec-reporter": "^7.25.1", |
79 | "angular2-hotkeys": "^13.1.0", | 81 | "angular2-hotkeys": "^13.1.0", |
80 | "angularx-qrcode": "14.0.0", | 82 | "angularx-qrcode": "14.0.0", |
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html index b113df82f..fdd6157e5 100644 --- a/client/src/app/+about/about-instance/about-instance.component.html +++ b/client/src/app/+about/about-instance/about-instance.component.html | |||
@@ -21,7 +21,7 @@ | |||
21 | 21 | ||
22 | <div class="anchor" id="administrators-and-sustainability"></div> | 22 | <div class="anchor" id="administrators-and-sustainability"></div> |
23 | <a | 23 | <a |
24 | *ngIf="html.administrator || html.maintenanceLifetime || html.businessModel" | 24 | *ngIf="aboutHTML.administrator || aboutHTML.maintenanceLifetime || aboutHTML.businessModel" |
25 | class="anchor-link" | 25 | class="anchor-link" |
26 | routerLink="/about/instance" | 26 | routerLink="/about/instance" |
27 | fragment="administrators-and-sustainability" | 27 | fragment="administrators-and-sustainability" |
@@ -33,7 +33,7 @@ | |||
33 | </h2> | 33 | </h2> |
34 | </a> | 34 | </a> |
35 | 35 | ||
36 | <div class="block administrator" *ngIf="html.administrator"> | 36 | <div class="block administrator" *ngIf="aboutHTML.administrator"> |
37 | <div class="anchor" id="administrators"></div> | 37 | <div class="anchor" id="administrators"></div> |
38 | <a | 38 | <a |
39 | class="anchor-link" | 39 | class="anchor-link" |
@@ -44,10 +44,10 @@ | |||
44 | <h3 i18n class="section-title">Who we are</h3> | 44 | <h3 i18n class="section-title">Who we are</h3> |
45 | </a> | 45 | </a> |
46 | 46 | ||
47 | <div [innerHTML]="html.administrator"></div> | 47 | <div [innerHTML]="aboutHTML.administrator"></div> |
48 | </div> | 48 | </div> |
49 | 49 | ||
50 | <div class="block creation-reason" *ngIf="html.creationReason"> | 50 | <div class="block creation-reason" *ngIf="aboutHTML.creationReason"> |
51 | <div class="anchor" id="creation-reason"></div> | 51 | <div class="anchor" id="creation-reason"></div> |
52 | <a | 52 | <a |
53 | class="anchor-link" | 53 | class="anchor-link" |
@@ -58,10 +58,10 @@ | |||
58 | <h3 i18n class="section-title">Why we created this instance</h3> | 58 | <h3 i18n class="section-title">Why we created this instance</h3> |
59 | </a> | 59 | </a> |
60 | 60 | ||
61 | <div [innerHTML]="html.creationReason"></div> | 61 | <div [innerHTML]="aboutHTML.creationReason"></div> |
62 | </div> | 62 | </div> |
63 | 63 | ||
64 | <div class="block maintenance-lifetime" *ngIf="html.maintenanceLifetime"> | 64 | <div class="block maintenance-lifetime" *ngIf="aboutHTML.maintenanceLifetime"> |
65 | <div class="anchor" id="maintenance-lifetime"></div> | 65 | <div class="anchor" id="maintenance-lifetime"></div> |
66 | <a | 66 | <a |
67 | class="anchor-link" | 67 | class="anchor-link" |
@@ -72,10 +72,10 @@ | |||
72 | <h3 i18n class="section-title">How long we plan to maintain this instance</h3> | 72 | <h3 i18n class="section-title">How long we plan to maintain this instance</h3> |
73 | </a> | 73 | </a> |
74 | 74 | ||
75 | <div [innerHTML]="html.maintenanceLifetime"></div> | 75 | <div [innerHTML]="aboutHTML.maintenanceLifetime"></div> |
76 | </div> | 76 | </div> |
77 | 77 | ||
78 | <div class="block business-model" *ngIf="html.businessModel"> | 78 | <div class="block business-model" *ngIf="aboutHTML.businessModel"> |
79 | <div class="anchor" id="business-model"></div> | 79 | <div class="anchor" id="business-model"></div> |
80 | <a | 80 | <a |
81 | class="anchor-link" | 81 | class="anchor-link" |
@@ -86,12 +86,12 @@ | |||
86 | <h3 i18n class="section-title">How we will pay for keeping our instance running</h3> | 86 | <h3 i18n class="section-title">How we will pay for keeping our instance running</h3> |
87 | </a> | 87 | </a> |
88 | 88 | ||
89 | <div [innerHTML]="html.businessModel"></div> | 89 | <div [innerHTML]="aboutHTML.businessModel"></div> |
90 | </div> | 90 | </div> |
91 | 91 | ||
92 | <div class="anchor" id="information"></div> | 92 | <div class="anchor" id="information"></div> |
93 | <a | 93 | <a |
94 | *ngIf="descriptionContent" | 94 | *ngIf="descriptionElement" |
95 | class="anchor-link" | 95 | class="anchor-link" |
96 | routerLink="/about/instance" | 96 | routerLink="/about/instance" |
97 | fragment="information" | 97 | fragment="information" |
@@ -113,13 +113,13 @@ | |||
113 | <h3 i18n class="section-title">Description</h3> | 113 | <h3 i18n class="section-title">Description</h3> |
114 | </a> | 114 | </a> |
115 | 115 | ||
116 | <my-custom-markup-container [content]="descriptionContent"></my-custom-markup-container> | 116 | <my-custom-markup-container [content]="descriptionElement"></my-custom-markup-container> |
117 | </div> | 117 | </div> |
118 | 118 | ||
119 | <div myPluginSelector pluginSelectorId="about-instance-moderation"> | 119 | <div myPluginSelector pluginSelectorId="about-instance-moderation"> |
120 | <div class="anchor" id="moderation"></div> | 120 | <div class="anchor" id="moderation"></div> |
121 | <a | 121 | <a |
122 | *ngIf="html.moderationInformation || html.codeOfConduct || html.terms" | 122 | *ngIf="aboutHTML.moderationInformation || aboutHTML.codeOfConduct || aboutHTML.terms" |
123 | class="anchor-link" | 123 | class="anchor-link" |
124 | routerLink="/about/instance" | 124 | routerLink="/about/instance" |
125 | fragment="moderation" | 125 | fragment="moderation" |
@@ -130,7 +130,7 @@ | |||
130 | </h2> | 130 | </h2> |
131 | </a> | 131 | </a> |
132 | 132 | ||
133 | <div class="block moderation-information" *ngIf="html.moderationInformation"> | 133 | <div class="block moderation-information" *ngIf="aboutHTML.moderationInformation"> |
134 | <div class="anchor" id="moderation-information"></div> | 134 | <div class="anchor" id="moderation-information"></div> |
135 | <a | 135 | <a |
136 | class="anchor-link" | 136 | class="anchor-link" |
@@ -141,10 +141,10 @@ | |||
141 | <h3 i18n class="section-title">Moderation information</h3> | 141 | <h3 i18n class="section-title">Moderation information</h3> |
142 | </a> | 142 | </a> |
143 | 143 | ||
144 | <div [innerHTML]="html.moderationInformation"></div> | 144 | <div [innerHTML]="aboutHTML.moderationInformation"></div> |
145 | </div> | 145 | </div> |
146 | 146 | ||
147 | <div class="block code-of-conduct" *ngIf="html.codeOfConduct"> | 147 | <div class="block code-of-conduct" *ngIf="aboutHTML.codeOfConduct"> |
148 | <div class="anchor" id="code-of-conduct"></div> | 148 | <div class="anchor" id="code-of-conduct"></div> |
149 | <a | 149 | <a |
150 | class="anchor-link" | 150 | class="anchor-link" |
@@ -155,7 +155,7 @@ | |||
155 | <h3 i18n class="section-title">Code of conduct</h3> | 155 | <h3 i18n class="section-title">Code of conduct</h3> |
156 | </a> | 156 | </a> |
157 | 157 | ||
158 | <div [innerHTML]="html.codeOfConduct"></div> | 158 | <div [innerHTML]="aboutHTML.codeOfConduct"></div> |
159 | </div> | 159 | </div> |
160 | 160 | ||
161 | <div class="block terms"> | 161 | <div class="block terms"> |
@@ -169,14 +169,14 @@ | |||
169 | <h3 i18n class="section-title">Terms</h3> | 169 | <h3 i18n class="section-title">Terms</h3> |
170 | </a> | 170 | </a> |
171 | 171 | ||
172 | <div [innerHTML]="html.terms"></div> | 172 | <div [innerHTML]="aboutHTML.terms"></div> |
173 | </div> | 173 | </div> |
174 | </div> | 174 | </div> |
175 | 175 | ||
176 | <div myPluginSelector pluginSelectorId="about-instance-other-information"> | 176 | <div myPluginSelector pluginSelectorId="about-instance-other-information"> |
177 | <div class="anchor" id="other-information"></div> | 177 | <div class="anchor" id="other-information"></div> |
178 | <a | 178 | <a |
179 | *ngIf="html.hardwareInformation" | 179 | *ngIf="aboutHTML.hardwareInformation" |
180 | class="anchor-link" | 180 | class="anchor-link" |
181 | routerLink="/about/instance" | 181 | routerLink="/about/instance" |
182 | fragment="other-information" | 182 | fragment="other-information" |
@@ -187,7 +187,7 @@ | |||
187 | </h2> | 187 | </h2> |
188 | </a> | 188 | </a> |
189 | 189 | ||
190 | <div class="block hardware-information" *ngIf="html.hardwareInformation"> | 190 | <div class="block hardware-information" *ngIf="aboutHTML.hardwareInformation"> |
191 | <div class="anchor" id="hardware-information"></div> | 191 | <div class="anchor" id="hardware-information"></div> |
192 | <a | 192 | <a |
193 | class="anchor-link" | 193 | class="anchor-link" |
@@ -198,7 +198,7 @@ | |||
198 | <h3 i18n class="section-title">Hardware information</h3> | 198 | <h3 i18n class="section-title">Hardware information</h3> |
199 | </a> | 199 | </a> |
200 | 200 | ||
201 | <div [innerHTML]="html.hardwareInformation"></div> | 201 | <div [innerHTML]="aboutHTML.hardwareInformation"></div> |
202 | </div> | 202 | </div> |
203 | </div> | 203 | </div> |
204 | </div> | 204 | </div> |
diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts index 0826bbc5a..e1501d7de 100644 --- a/client/src/app/+about/about-instance/about-instance.component.ts +++ b/client/src/app/+about/about-instance/about-instance.component.ts | |||
@@ -2,7 +2,7 @@ import { ViewportScroller } from '@angular/common' | |||
2 | import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 2 | import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute } from '@angular/router' | 3 | import { ActivatedRoute } from '@angular/router' |
4 | import { Notifier, ServerService } from '@app/core' | 4 | import { Notifier, ServerService } from '@app/core' |
5 | import { InstanceService } from '@app/shared/shared-instance' | 5 | import { AboutHTML } from '@app/shared/shared-instance' |
6 | import { copyToClipboard } from '@root-helpers/utils' | 6 | import { copyToClipboard } from '@root-helpers/utils' |
7 | import { HTMLServerConfig } from '@shared/models/server' | 7 | import { HTMLServerConfig } from '@shared/models/server' |
8 | import { ResolverData } from './about-instance.resolver' | 8 | import { ResolverData } from './about-instance.resolver' |
@@ -17,22 +17,12 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
17 | @ViewChild('descriptionWrapper') descriptionWrapper: ElementRef<HTMLInputElement> | 17 | @ViewChild('descriptionWrapper') descriptionWrapper: ElementRef<HTMLInputElement> |
18 | @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent | 18 | @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent |
19 | 19 | ||
20 | shortDescription = '' | 20 | aboutHTML: AboutHTML |
21 | descriptionContent: string | 21 | descriptionElement: HTMLDivElement |
22 | |||
23 | html = { | ||
24 | terms: '', | ||
25 | codeOfConduct: '', | ||
26 | moderationInformation: '', | ||
27 | administrator: '', | ||
28 | creationReason: '', | ||
29 | maintenanceLifetime: '', | ||
30 | businessModel: '', | ||
31 | hardwareInformation: '' | ||
32 | } | ||
33 | 22 | ||
34 | languages: string[] = [] | 23 | languages: string[] = [] |
35 | categories: string[] = [] | 24 | categories: string[] = [] |
25 | shortDescription = '' | ||
36 | 26 | ||
37 | initialized = false | 27 | initialized = false |
38 | 28 | ||
@@ -44,8 +34,7 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
44 | private viewportScroller: ViewportScroller, | 34 | private viewportScroller: ViewportScroller, |
45 | private route: ActivatedRoute, | 35 | private route: ActivatedRoute, |
46 | private notifier: Notifier, | 36 | private notifier: Notifier, |
47 | private serverService: ServerService, | 37 | private serverService: ServerService |
48 | private instanceService: InstanceService | ||
49 | ) {} | 38 | ) {} |
50 | 39 | ||
51 | get instanceName () { | 40 | get instanceName () { |
@@ -60,8 +49,16 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
60 | return this.serverConfig.instance.isNSFW | 49 | return this.serverConfig.instance.isNSFW |
61 | } | 50 | } |
62 | 51 | ||
63 | async ngOnInit () { | 52 | ngOnInit () { |
64 | const { about, languages, categories }: ResolverData = this.route.snapshot.data.instanceData | 53 | const { about, languages, categories, aboutHTML, descriptionElement }: ResolverData = this.route.snapshot.data.instanceData |
54 | |||
55 | this.aboutHTML = aboutHTML | ||
56 | this.descriptionElement = descriptionElement | ||
57 | |||
58 | this.languages = languages | ||
59 | this.categories = categories | ||
60 | |||
61 | this.shortDescription = about.instance.shortDescription | ||
65 | 62 | ||
66 | this.serverConfig = this.serverService.getHTMLConfig() | 63 | this.serverConfig = this.serverService.getHTMLConfig() |
67 | 64 | ||
@@ -73,14 +70,6 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
73 | this.contactAdminModal.show(prefill) | 70 | this.contactAdminModal.show(prefill) |
74 | }) | 71 | }) |
75 | 72 | ||
76 | this.languages = languages | ||
77 | this.categories = categories | ||
78 | |||
79 | this.shortDescription = about.instance.shortDescription | ||
80 | this.descriptionContent = about.instance.description | ||
81 | |||
82 | this.html = await this.instanceService.buildHtml(about) | ||
83 | |||
84 | this.initialized = true | 73 | this.initialized = true |
85 | } | 74 | } |
86 | 75 | ||
diff --git a/client/src/app/+about/about-instance/about-instance.resolver.ts b/client/src/app/+about/about-instance/about-instance.resolver.ts index ee0219df0..8818fc582 100644 --- a/client/src/app/+about/about-instance/about-instance.resolver.ts +++ b/client/src/app/+about/about-instance/about-instance.resolver.ts | |||
@@ -2,16 +2,25 @@ import { forkJoin } from 'rxjs' | |||
2 | import { map, switchMap } from 'rxjs/operators' | 2 | import { map, switchMap } from 'rxjs/operators' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { Resolve } from '@angular/router' | 4 | import { Resolve } from '@angular/router' |
5 | import { InstanceService } from '@app/shared/shared-instance' | 5 | import { CustomMarkupService } from '@app/shared/shared-custom-markup' |
6 | import { AboutHTML, InstanceService } from '@app/shared/shared-instance' | ||
6 | import { About } from '@shared/models/server' | 7 | import { About } from '@shared/models/server' |
7 | 8 | ||
8 | export type ResolverData = { about: About, languages: string[], categories: string[] } | 9 | export type ResolverData = { |
10 | about: About | ||
11 | languages: string[] | ||
12 | categories: string[] | ||
13 | aboutHTML: AboutHTML | ||
14 | descriptionElement: HTMLDivElement | ||
15 | } | ||
9 | 16 | ||
10 | @Injectable() | 17 | @Injectable() |
11 | export class AboutInstanceResolver implements Resolve<any> { | 18 | export class AboutInstanceResolver implements Resolve<any> { |
12 | 19 | ||
13 | constructor ( | 20 | constructor ( |
14 | private instanceService: InstanceService | 21 | private instanceService: InstanceService, |
22 | private customMarkupService: CustomMarkupService | ||
23 | |||
15 | ) {} | 24 | ) {} |
16 | 25 | ||
17 | resolve () { | 26 | resolve () { |
@@ -19,9 +28,15 @@ export class AboutInstanceResolver implements Resolve<any> { | |||
19 | .pipe( | 28 | .pipe( |
20 | switchMap(about => { | 29 | switchMap(about => { |
21 | return forkJoin([ | 30 | return forkJoin([ |
31 | Promise.resolve(about), | ||
22 | this.instanceService.buildTranslatedLanguages(about), | 32 | this.instanceService.buildTranslatedLanguages(about), |
23 | this.instanceService.buildTranslatedCategories(about) | 33 | this.instanceService.buildTranslatedCategories(about), |
24 | ]).pipe(map(([ languages, categories ]) => ({ about, languages, categories }) as ResolverData)) | 34 | this.instanceService.buildHtml(about), |
35 | this.customMarkupService.buildElement(about.instance.description) | ||
36 | ]) | ||
37 | }), | ||
38 | map(([ about, languages, categories, aboutHTML, { rootElement } ]) => { | ||
39 | return { about, languages, categories, aboutHTML, descriptionElement: rootElement } as ResolverData | ||
25 | }) | 40 | }) |
26 | ) | 41 | ) |
27 | } | 42 | } |
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 746549555..630bfe253 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts | |||
@@ -96,6 +96,14 @@ export class AdminComponent implements OnInit { | |||
96 | children: [] | 96 | children: [] |
97 | } | 97 | } |
98 | 98 | ||
99 | if (this.hasRegistrationsRight()) { | ||
100 | moderationItems.children.push({ | ||
101 | label: $localize`Registrations`, | ||
102 | routerLink: '/admin/moderation/registrations/list', | ||
103 | iconName: 'user' | ||
104 | }) | ||
105 | } | ||
106 | |||
99 | if (this.hasAbusesRight()) { | 107 | if (this.hasAbusesRight()) { |
100 | moderationItems.children.push({ | 108 | moderationItems.children.push({ |
101 | label: $localize`Reports`, | 109 | label: $localize`Reports`, |
@@ -229,4 +237,8 @@ export class AdminComponent implements OnInit { | |||
229 | private hasVideosRight () { | 237 | private hasVideosRight () { |
230 | return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS) | 238 | return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS) |
231 | } | 239 | } |
240 | |||
241 | private hasRegistrationsRight () { | ||
242 | return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS) | ||
243 | } | ||
232 | } | 244 | } |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index f01967ea6..891ff4ed1 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -30,7 +30,13 @@ import { FollowersListComponent, FollowModalComponent, VideoRedundanciesListComp | |||
30 | import { FollowingListComponent } from './follows/following-list/following-list.component' | 30 | import { FollowingListComponent } from './follows/following-list/following-list.component' |
31 | import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' | 31 | import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' |
32 | import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' | 32 | import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' |
33 | import { AbuseListComponent, VideoBlockListComponent } from './moderation' | 33 | import { |
34 | AbuseListComponent, | ||
35 | AdminRegistrationService, | ||
36 | ProcessRegistrationModalComponent, | ||
37 | RegistrationListComponent, | ||
38 | VideoBlockListComponent | ||
39 | } from './moderation' | ||
34 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' | 40 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' |
35 | import { | 41 | import { |
36 | UserCreateComponent, | 42 | UserCreateComponent, |
@@ -116,7 +122,10 @@ import { JobsComponent } from './system/jobs/jobs.component' | |||
116 | EditLiveConfigurationComponent, | 122 | EditLiveConfigurationComponent, |
117 | EditAdvancedConfigurationComponent, | 123 | EditAdvancedConfigurationComponent, |
118 | EditInstanceInformationComponent, | 124 | EditInstanceInformationComponent, |
119 | EditHomepageComponent | 125 | EditHomepageComponent, |
126 | |||
127 | RegistrationListComponent, | ||
128 | ProcessRegistrationModalComponent | ||
120 | ], | 129 | ], |
121 | 130 | ||
122 | exports: [ | 131 | exports: [ |
@@ -130,7 +139,8 @@ import { JobsComponent } from './system/jobs/jobs.component' | |||
130 | ConfigService, | 139 | ConfigService, |
131 | PluginApiService, | 140 | PluginApiService, |
132 | EditConfigurationService, | 141 | EditConfigurationService, |
133 | VideoAdminService | 142 | VideoAdminService, |
143 | AdminRegistrationService | ||
134 | ] | 144 | ] |
135 | }) | 145 | }) |
136 | export class AdminModule { } | 146 | export class AdminModule { } |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html index 43f1438e0..0f3803f97 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html | |||
@@ -44,9 +44,13 @@ | |||
44 | 44 | ||
45 | <div class="peertube-select-container"> | 45 | <div class="peertube-select-container"> |
46 | <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control"> | 46 | <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control"> |
47 | <option i18n value="publishedAt">Recently added videos</option> | ||
48 | <option i18n value="originallyPublishedAt">Original publication date</option> | ||
49 | <option i18n value="name">Name</option> | ||
47 | <option i18n value="hot">Hot videos</option> | 50 | <option i18n value="hot">Hot videos</option> |
48 | <option i18n value="most-viewed">Most viewed videos</option> | 51 | <option i18n value="most-viewed">Recent views</option> |
49 | <option i18n value="most-liked">Most liked videos</option> | 52 | <option i18n value="most-liked">Most liked videos</option> |
53 | <option i18n value="views">Global views</option> | ||
50 | </select> | 54 | </select> |
51 | </div> | 55 | </div> |
52 | 56 | ||
@@ -167,12 +171,21 @@ | |||
167 | </ng-container> | 171 | </ng-container> |
168 | 172 | ||
169 | <ng-container ngProjectAs="extra"> | 173 | <ng-container ngProjectAs="extra"> |
170 | <my-peertube-checkbox [ngClass]="getDisabledSignupClass()" | 174 | <div class="form-group"> |
171 | inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification" | 175 | <my-peertube-checkbox [ngClass]="getDisabledSignupClass()" |
172 | i18n-labelText labelText="Signup requires email verification" | 176 | inputName="signupRequiresApproval" formControlName="requiresApproval" |
173 | ></my-peertube-checkbox> | 177 | i18n-labelText labelText="Signup requires approval by moderators" |
178 | ></my-peertube-checkbox> | ||
179 | </div> | ||
174 | 180 | ||
175 | <div [ngClass]="getDisabledSignupClass()" class="mt-3"> | 181 | <div class="form-group"> |
182 | <my-peertube-checkbox [ngClass]="getDisabledSignupClass()" | ||
183 | inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification" | ||
184 | i18n-labelText labelText="Signup requires email verification" | ||
185 | ></my-peertube-checkbox> | ||
186 | </div> | ||
187 | |||
188 | <div [ngClass]="getDisabledSignupClass()"> | ||
176 | <label i18n for="signupLimit">Signup limit</label> | 189 | <label i18n for="signupLimit">Signup limit</label> |
177 | 190 | ||
178 | <div class="number-with-unit"> | 191 | <div class="number-with-unit"> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 168f4702c..2afe80a03 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | |||
@@ -132,6 +132,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
132 | signup: { | 132 | signup: { |
133 | enabled: null, | 133 | enabled: null, |
134 | limit: SIGNUP_LIMIT_VALIDATOR, | 134 | limit: SIGNUP_LIMIT_VALIDATOR, |
135 | requiresApproval: null, | ||
135 | requiresEmailVerification: null, | 136 | requiresEmailVerification: null, |
136 | minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR | 137 | minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR |
137 | }, | 138 | }, |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html index 5339240bb..3d8414f5c 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html | |||
@@ -17,7 +17,7 @@ | |||
17 | 17 | ||
18 | <my-markdown-textarea | 18 | <my-markdown-textarea |
19 | name="instanceCustomHomepageContent" formControlName="content" | 19 | name="instanceCustomHomepageContent" formControlName="content" |
20 | [customMarkdownRenderer]="getCustomMarkdownRenderer()" | 20 | [customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500" |
21 | [formError]="formErrors['instanceCustomHomepage.content']" | 21 | [formError]="formErrors['instanceCustomHomepage.content']" |
22 | ></my-markdown-textarea> | 22 | ></my-markdown-textarea> |
23 | 23 | ||
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html index b54733327..504afa189 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html | |||
@@ -38,7 +38,7 @@ | |||
38 | 38 | ||
39 | <my-markdown-textarea | 39 | <my-markdown-textarea |
40 | name="instanceDescription" formControlName="description" | 40 | name="instanceDescription" formControlName="description" |
41 | [customMarkdownRenderer]="getCustomMarkdownRenderer()" | 41 | [customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500" |
42 | [formError]="formErrors['instance.description']" | 42 | [formError]="formErrors['instance.description']" |
43 | ></my-markdown-textarea> | 43 | ></my-markdown-textarea> |
44 | </div> | 44 | </div> |
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html index 8fe0d2348..14c62f1af 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.html +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html | |||
@@ -9,14 +9,14 @@ | |||
9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" | 9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" |
10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers" | 11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers" |
12 | [(selection)]="selectedFollows" | 12 | [(selection)]="selectedRows" |
13 | > | 13 | > |
14 | <ng-template pTemplate="caption"> | 14 | <ng-template pTemplate="caption"> |
15 | <div class="caption"> | 15 | <div class="caption"> |
16 | <div class="left-buttons"> | 16 | <div class="left-buttons"> |
17 | <my-action-dropdown | 17 | <my-action-dropdown |
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | 18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" |
19 | [actions]="bulkFollowsActions" [entry]="selectedFollows" | 19 | [actions]="bulkActions" [entry]="selectedRows" |
20 | > | 20 | > |
21 | </my-action-dropdown> | 21 | </my-action-dropdown> |
22 | </div> | 22 | </div> |
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts index b2d333e83..cebb2e1a2 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts | |||
@@ -12,7 +12,7 @@ import { ActorFollow } from '@shared/models' | |||
12 | templateUrl: './followers-list.component.html', | 12 | templateUrl: './followers-list.component.html', |
13 | styleUrls: [ './followers-list.component.scss' ] | 13 | styleUrls: [ './followers-list.component.scss' ] |
14 | }) | 14 | }) |
15 | export class FollowersListComponent extends RestTable implements OnInit { | 15 | export class FollowersListComponent extends RestTable <ActorFollow> implements OnInit { |
16 | followers: ActorFollow[] = [] | 16 | followers: ActorFollow[] = [] |
17 | totalRecords = 0 | 17 | totalRecords = 0 |
18 | sort: SortMeta = { field: 'createdAt', order: -1 } | 18 | sort: SortMeta = { field: 'createdAt', order: -1 } |
@@ -20,8 +20,7 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
20 | 20 | ||
21 | searchFilters: AdvancedInputFilter[] = [] | 21 | searchFilters: AdvancedInputFilter[] = [] |
22 | 22 | ||
23 | selectedFollows: ActorFollow[] = [] | 23 | bulkActions: DropdownAction<ActorFollow[]>[] = [] |
24 | bulkFollowsActions: DropdownAction<ActorFollow[]>[] = [] | ||
25 | 24 | ||
26 | constructor ( | 25 | constructor ( |
27 | private confirmService: ConfirmService, | 26 | private confirmService: ConfirmService, |
@@ -36,7 +35,7 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
36 | 35 | ||
37 | this.searchFilters = this.followService.buildFollowsListFilters() | 36 | this.searchFilters = this.followService.buildFollowsListFilters() |
38 | 37 | ||
39 | this.bulkFollowsActions = [ | 38 | this.bulkActions = [ |
40 | { | 39 | { |
41 | label: $localize`Reject`, | 40 | label: $localize`Reject`, |
42 | handler: follows => this.rejectFollower(follows), | 41 | handler: follows => this.rejectFollower(follows), |
@@ -105,12 +104,14 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
105 | } | 104 | } |
106 | 105 | ||
107 | async deleteFollowers (follows: ActorFollow[]) { | 106 | async deleteFollowers (follows: ActorFollow[]) { |
107 | const icuParams = { count: follows.length, followerName: this.buildFollowerName(follows[0]) } | ||
108 | |||
108 | let message = $localize`Deleted followers will be able to send again a follow request.` | 109 | let message = $localize`Deleted followers will be able to send again a follow request.` |
109 | message += '<br /><br />' | 110 | message += '<br /><br />' |
110 | 111 | ||
111 | // eslint-disable-next-line max-len | 112 | // eslint-disable-next-line max-len |
112 | message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( | 113 | message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( |
113 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, | 114 | icuParams, |
114 | $localize`Do you really want to delete these follow requests?` | 115 | $localize`Do you really want to delete these follow requests?` |
115 | ) | 116 | ) |
116 | 117 | ||
@@ -122,7 +123,7 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
122 | next: () => { | 123 | next: () => { |
123 | // eslint-disable-next-line max-len | 124 | // eslint-disable-next-line max-len |
124 | const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( | 125 | const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( |
125 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, | 126 | icuParams, |
126 | $localize`Follow requests removed` | 127 | $localize`Follow requests removed` |
127 | ) | 128 | ) |
128 | 129 | ||
@@ -139,11 +140,7 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
139 | return follow.follower.name + '@' + follow.follower.host | 140 | return follow.follower.name + '@' + follow.follower.host |
140 | } | 141 | } |
141 | 142 | ||
142 | isInSelectionMode () { | 143 | protected reloadDataInternal () { |
143 | return this.selectedFollows.length !== 0 | ||
144 | } | ||
145 | |||
146 | protected reloadData () { | ||
147 | this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search }) | 144 | this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search }) |
148 | .subscribe({ | 145 | .subscribe({ |
149 | next: resultList => { | 146 | next: resultList => { |
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html index f7abb7ede..eca79be71 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.html +++ b/client/src/app/+admin/follows/following-list/following-list.component.html | |||
@@ -9,14 +9,14 @@ | |||
9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" | 9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" |
10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts" | 11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts" |
12 | [(selection)]="selectedFollows" | 12 | [(selection)]="selectedRows" |
13 | > | 13 | > |
14 | <ng-template pTemplate="caption"> | 14 | <ng-template pTemplate="caption"> |
15 | <div class="caption"> | 15 | <div class="caption"> |
16 | <div class="left-buttons"> | 16 | <div class="left-buttons"> |
17 | <my-action-dropdown | 17 | <my-action-dropdown |
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | 18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" |
19 | [actions]="bulkFollowsActions" [entry]="selectedFollows" | 19 | [actions]="bulkActions" [entry]="selectedRows" |
20 | > | 20 | > |
21 | </my-action-dropdown> | 21 | </my-action-dropdown> |
22 | 22 | ||
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts index e3a56651a..71f2fbe66 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.ts +++ b/client/src/app/+admin/follows/following-list/following-list.component.ts | |||
@@ -12,7 +12,7 @@ import { prepareIcu } from '@app/helpers' | |||
12 | templateUrl: './following-list.component.html', | 12 | templateUrl: './following-list.component.html', |
13 | styleUrls: [ './following-list.component.scss' ] | 13 | styleUrls: [ './following-list.component.scss' ] |
14 | }) | 14 | }) |
15 | export class FollowingListComponent extends RestTable implements OnInit { | 15 | export class FollowingListComponent extends RestTable <ActorFollow> implements OnInit { |
16 | @ViewChild('followModal') followModal: FollowModalComponent | 16 | @ViewChild('followModal') followModal: FollowModalComponent |
17 | 17 | ||
18 | following: ActorFollow[] = [] | 18 | following: ActorFollow[] = [] |
@@ -22,8 +22,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
22 | 22 | ||
23 | searchFilters: AdvancedInputFilter[] = [] | 23 | searchFilters: AdvancedInputFilter[] = [] |
24 | 24 | ||
25 | selectedFollows: ActorFollow[] = [] | 25 | bulkActions: DropdownAction<ActorFollow[]>[] = [] |
26 | bulkFollowsActions: DropdownAction<ActorFollow[]>[] = [] | ||
27 | 26 | ||
28 | constructor ( | 27 | constructor ( |
29 | private notifier: Notifier, | 28 | private notifier: Notifier, |
@@ -38,7 +37,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
38 | 37 | ||
39 | this.searchFilters = this.followService.buildFollowsListFilters() | 38 | this.searchFilters = this.followService.buildFollowsListFilters() |
40 | 39 | ||
41 | this.bulkFollowsActions = [ | 40 | this.bulkActions = [ |
42 | { | 41 | { |
43 | label: $localize`Delete`, | 42 | label: $localize`Delete`, |
44 | handler: follows => this.removeFollowing(follows) | 43 | handler: follows => this.removeFollowing(follows) |
@@ -58,17 +57,15 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
58 | return follow.following.name === 'peertube' | 57 | return follow.following.name === 'peertube' |
59 | } | 58 | } |
60 | 59 | ||
61 | isInSelectionMode () { | ||
62 | return this.selectedFollows.length !== 0 | ||
63 | } | ||
64 | |||
65 | buildFollowingName (follow: ActorFollow) { | 60 | buildFollowingName (follow: ActorFollow) { |
66 | return follow.following.name + '@' + follow.following.host | 61 | return follow.following.name + '@' + follow.following.host |
67 | } | 62 | } |
68 | 63 | ||
69 | async removeFollowing (follows: ActorFollow[]) { | 64 | async removeFollowing (follows: ActorFollow[]) { |
65 | const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) } | ||
66 | |||
70 | const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)( | 67 | const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)( |
71 | { count: follows.length, entryName: this.buildFollowingName(follows[0]) }, | 68 | icuParams, |
72 | $localize`Do you really want to unfollow these entries?` | 69 | $localize`Do you really want to unfollow these entries?` |
73 | ) | 70 | ) |
74 | 71 | ||
@@ -80,7 +77,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
80 | next: () => { | 77 | next: () => { |
81 | // eslint-disable-next-line max-len | 78 | // eslint-disable-next-line max-len |
82 | const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)( | 79 | const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)( |
83 | { count: follows.length, entryName: this.buildFollowingName(follows[0]) }, | 80 | icuParams, |
84 | $localize`You are not following them anymore.` | 81 | $localize`You are not following them anymore.` |
85 | ) | 82 | ) |
86 | 83 | ||
@@ -92,7 +89,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
92 | }) | 89 | }) |
93 | } | 90 | } |
94 | 91 | ||
95 | protected reloadData () { | 92 | protected reloadDataInternal () { |
96 | this.followService.getFollowing({ pagination: this.pagination, sort: this.sort, search: this.search }) | 93 | this.followService.getFollowing({ pagination: this.pagination, sort: this.sort, search: this.search }) |
97 | .subscribe({ | 94 | .subscribe({ |
98 | next: resultList => { | 95 | next: resultList => { |
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts index a89603048..b31c5b35e 100644 --- a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts | |||
@@ -162,7 +162,7 @@ export class VideoRedundanciesListComponent extends RestTable implements OnInit | |||
162 | 162 | ||
163 | } | 163 | } |
164 | 164 | ||
165 | protected reloadData () { | 165 | protected reloadDataInternal () { |
166 | const options = { | 166 | const options = { |
167 | pagination: this.pagination, | 167 | pagination: this.pagination, |
168 | sort: this.sort, | 168 | sort: this.sort, |
diff --git a/client/src/app/+admin/moderation/index.ts b/client/src/app/+admin/moderation/index.ts index 9dab270cc..135b4b408 100644 --- a/client/src/app/+admin/moderation/index.ts +++ b/client/src/app/+admin/moderation/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './abuse-list' | 1 | export * from './abuse-list' |
2 | export * from './instance-blocklist' | 2 | export * from './instance-blocklist' |
3 | export * from './video-block-list' | 3 | export * from './video-block-list' |
4 | export * from './registration-list' | ||
4 | export * from './moderation.routes' | 5 | export * from './moderation.routes' |
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index 1ad301039..378d2bed7 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts | |||
@@ -4,6 +4,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f | |||
4 | import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' | 4 | import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' |
5 | import { UserRightGuard } from '@app/core' | 5 | import { UserRightGuard } from '@app/core' |
6 | import { UserRight } from '@shared/models' | 6 | import { UserRight } from '@shared/models' |
7 | import { RegistrationListComponent } from './registration-list' | ||
7 | 8 | ||
8 | export const ModerationRoutes: Routes = [ | 9 | export const ModerationRoutes: Routes = [ |
9 | { | 10 | { |
@@ -68,7 +69,19 @@ export const ModerationRoutes: Routes = [ | |||
68 | } | 69 | } |
69 | }, | 70 | }, |
70 | 71 | ||
71 | // We move this component in admin overview pages | 72 | { |
73 | path: 'registrations/list', | ||
74 | component: RegistrationListComponent, | ||
75 | canActivate: [ UserRightGuard ], | ||
76 | data: { | ||
77 | userRight: UserRight.MANAGE_REGISTRATIONS, | ||
78 | meta: { | ||
79 | title: $localize`User registrations` | ||
80 | } | ||
81 | } | ||
82 | }, | ||
83 | |||
84 | // We moved this component in admin overview pages | ||
72 | { | 85 | { |
73 | path: 'video-comments', | 86 | path: 'video-comments', |
74 | redirectTo: 'video-comments/list', | 87 | redirectTo: 'video-comments/list', |
diff --git a/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts b/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts new file mode 100644 index 000000000..a9f13cf2f --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts | |||
@@ -0,0 +1,81 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { from } from 'rxjs' | ||
3 | import { catchError, concatMap, toArray } from 'rxjs/operators' | ||
4 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
5 | import { Injectable } from '@angular/core' | ||
6 | import { RestExtractor, RestPagination, RestService } from '@app/core' | ||
7 | import { arrayify } from '@shared/core-utils' | ||
8 | import { ResultList, UserRegistration, UserRegistrationUpdateState } from '@shared/models' | ||
9 | import { environment } from '../../../../environments/environment' | ||
10 | |||
11 | @Injectable() | ||
12 | export class AdminRegistrationService { | ||
13 | private static BASE_REGISTRATION_URL = environment.apiUrl + '/api/v1/users/registrations' | ||
14 | |||
15 | constructor ( | ||
16 | private authHttp: HttpClient, | ||
17 | private restExtractor: RestExtractor, | ||
18 | private restService: RestService | ||
19 | ) { } | ||
20 | |||
21 | listRegistrations (options: { | ||
22 | pagination: RestPagination | ||
23 | sort: SortMeta | ||
24 | search?: string | ||
25 | }) { | ||
26 | const { pagination, sort, search } = options | ||
27 | |||
28 | const url = AdminRegistrationService.BASE_REGISTRATION_URL | ||
29 | |||
30 | let params = new HttpParams() | ||
31 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
32 | |||
33 | if (search) { | ||
34 | params = params.append('search', search) | ||
35 | } | ||
36 | |||
37 | return this.authHttp.get<ResultList<UserRegistration>>(url, { params }) | ||
38 | .pipe( | ||
39 | catchError(res => this.restExtractor.handleError(res)) | ||
40 | ) | ||
41 | } | ||
42 | |||
43 | acceptRegistration (options: { | ||
44 | registration: UserRegistration | ||
45 | moderationResponse: string | ||
46 | preventEmailDelivery: boolean | ||
47 | }) { | ||
48 | const { registration, moderationResponse, preventEmailDelivery } = options | ||
49 | |||
50 | const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/accept' | ||
51 | const body: UserRegistrationUpdateState = { moderationResponse, preventEmailDelivery } | ||
52 | |||
53 | return this.authHttp.post(url, body) | ||
54 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
55 | } | ||
56 | |||
57 | rejectRegistration (options: { | ||
58 | registration: UserRegistration | ||
59 | moderationResponse: string | ||
60 | preventEmailDelivery: boolean | ||
61 | }) { | ||
62 | const { registration, moderationResponse, preventEmailDelivery } = options | ||
63 | |||
64 | const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/reject' | ||
65 | const body: UserRegistrationUpdateState = { moderationResponse, preventEmailDelivery } | ||
66 | |||
67 | return this.authHttp.post(url, body) | ||
68 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
69 | } | ||
70 | |||
71 | removeRegistrations (registrationsArg: UserRegistration | UserRegistration[]) { | ||
72 | const registrations = arrayify(registrationsArg) | ||
73 | |||
74 | return from(registrations) | ||
75 | .pipe( | ||
76 | concatMap(r => this.authHttp.delete(AdminRegistrationService.BASE_REGISTRATION_URL + '/' + r.id)), | ||
77 | toArray(), | ||
78 | catchError(err => this.restExtractor.handleError(err)) | ||
79 | ) | ||
80 | } | ||
81 | } | ||
diff --git a/client/src/app/+admin/moderation/registration-list/index.ts b/client/src/app/+admin/moderation/registration-list/index.ts new file mode 100644 index 000000000..060b676a4 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './admin-registration.service' | ||
2 | export * from './process-registration-modal.component' | ||
3 | export * from './process-registration-validators' | ||
4 | export * from './registration-list.component' | ||
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html new file mode 100644 index 000000000..8e46b0cf9 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html | |||
@@ -0,0 +1,74 @@ | |||
1 | <ng-template #modal> | ||
2 | <div class="modal-header"> | ||
3 | <h4 i18n class="modal-title"> | ||
4 | <ng-container *ngIf="isAccept()">Accept {{ registration.username }} registration</ng-container> | ||
5 | <ng-container *ngIf="isReject()">Reject {{ registration.username }} registration</ng-container> | ||
6 | </h4> | ||
7 | |||
8 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
9 | </div> | ||
10 | |||
11 | <form novalidate [formGroup]="form" (ngSubmit)="processRegistration()"> | ||
12 | <div class="modal-body mb-3"> | ||
13 | |||
14 | <div i18n *ngIf="!registration.emailVerified" class="alert alert-warning"> | ||
15 | Registration email has not been verified. Email delivery has been disabled by default. | ||
16 | </div> | ||
17 | |||
18 | <div class="description"> | ||
19 | <ng-container *ngIf="isAccept()"> | ||
20 | <p i18n> | ||
21 | <strong>Accepting</strong> <em>{{ registration.username }}</em> registration will create the account and channel. | ||
22 | </p> | ||
23 | |||
24 | <p *ngIf="isEmailEnabled()" i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }"> | ||
25 | An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below. | ||
26 | </p> | ||
27 | |||
28 | <div *ngIf="!isEmailEnabled()" class="alert alert-warning" i18n> | ||
29 | Emails are not enabled on this instance so PeerTube won't be able to send an email to <em>{{ registration.email }}</em> explaining its account has been created. | ||
30 | </div> | ||
31 | </ng-container> | ||
32 | |||
33 | <ng-container *ngIf="isReject()"> | ||
34 | <p i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }"> | ||
35 | An email will be sent to <em>{{ registration.email }}</em> explaining its registration request has been <strong>rejected</strong> with the moderation response you'll write below. | ||
36 | </p> | ||
37 | |||
38 | <div *ngIf="!isEmailEnabled()" class="alert alert-warning" i18n> | ||
39 | Emails are not enabled on this instance so PeerTube won't be able to send an email to <em>{{ registration.email }}</em> explaining its registration request has been rejected. | ||
40 | </div> | ||
41 | </ng-container> | ||
42 | </div> | ||
43 | |||
44 | <div class="form-group"> | ||
45 | <label for="moderationResponse" i18n>Send a message to the user</label> | ||
46 | |||
47 | <textarea | ||
48 | formControlName="moderationResponse" ngbAutofocus name="moderationResponse" id="moderationResponse" | ||
49 | [ngClass]="{ 'input-error': formErrors['moderationResponse'] }" class="form-control" | ||
50 | ></textarea> | ||
51 | |||
52 | <div *ngIf="formErrors.moderationResponse" class="form-error"> | ||
53 | {{ formErrors.moderationResponse }} | ||
54 | </div> | ||
55 | </div> | ||
56 | |||
57 | <div class="form-group"> | ||
58 | <my-peertube-checkbox | ||
59 | inputName="preventEmailDelivery" formControlName="preventEmailDelivery" [disabled]="!isEmailEnabled()" | ||
60 | i18n-labelText labelText="Prevent email from being sent to the user" | ||
61 | ></my-peertube-checkbox> | ||
62 | </div> | ||
63 | </div> | ||
64 | |||
65 | <div class="modal-footer inputs"> | ||
66 | <input | ||
67 | type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button" | ||
68 | (click)="hide()" (key.enter)="hide()" | ||
69 | > | ||
70 | |||
71 | <input type="submit" [value]="getSubmitValue()" class="peertube-button orange-button" [disabled]="!form.valid"> | ||
72 | </div> | ||
73 | </form> | ||
74 | </ng-template> | ||
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss new file mode 100644 index 000000000..3e03bed89 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss | |||
@@ -0,0 +1,3 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts new file mode 100644 index 000000000..3a7e5dea1 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts | |||
@@ -0,0 +1,122 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { Notifier, ServerService } from '@app/core' | ||
3 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | ||
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
6 | import { UserRegistration } from '@shared/models' | ||
7 | import { AdminRegistrationService } from './admin-registration.service' | ||
8 | import { REGISTRATION_MODERATION_RESPONSE_VALIDATOR } from './process-registration-validators' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-process-registration-modal', | ||
12 | templateUrl: './process-registration-modal.component.html', | ||
13 | styleUrls: [ './process-registration-modal.component.scss' ] | ||
14 | }) | ||
15 | export class ProcessRegistrationModalComponent extends FormReactive implements OnInit { | ||
16 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
17 | |||
18 | @Output() registrationProcessed = new EventEmitter() | ||
19 | |||
20 | registration: UserRegistration | ||
21 | |||
22 | private openedModal: NgbModalRef | ||
23 | private processMode: 'accept' | 'reject' | ||
24 | |||
25 | constructor ( | ||
26 | protected formReactiveService: FormReactiveService, | ||
27 | private server: ServerService, | ||
28 | private modalService: NgbModal, | ||
29 | private notifier: Notifier, | ||
30 | private registrationService: AdminRegistrationService | ||
31 | ) { | ||
32 | super() | ||
33 | } | ||
34 | |||
35 | ngOnInit () { | ||
36 | this.buildForm({ | ||
37 | moderationResponse: REGISTRATION_MODERATION_RESPONSE_VALIDATOR, | ||
38 | preventEmailDelivery: null | ||
39 | }) | ||
40 | } | ||
41 | |||
42 | isAccept () { | ||
43 | return this.processMode === 'accept' | ||
44 | } | ||
45 | |||
46 | isReject () { | ||
47 | return this.processMode === 'reject' | ||
48 | } | ||
49 | |||
50 | openModal (registration: UserRegistration, mode: 'accept' | 'reject') { | ||
51 | this.processMode = mode | ||
52 | this.registration = registration | ||
53 | |||
54 | this.form.patchValue({ | ||
55 | preventEmailDelivery: !this.isEmailEnabled() || registration.emailVerified !== true | ||
56 | }) | ||
57 | |||
58 | this.openedModal = this.modalService.open(this.modal, { centered: true }) | ||
59 | } | ||
60 | |||
61 | hide () { | ||
62 | this.form.reset() | ||
63 | |||
64 | this.openedModal.close() | ||
65 | } | ||
66 | |||
67 | getSubmitValue () { | ||
68 | if (this.isAccept()) { | ||
69 | return $localize`Accept registration` | ||
70 | } | ||
71 | |||
72 | return $localize`Reject registration` | ||
73 | } | ||
74 | |||
75 | processRegistration () { | ||
76 | if (this.isAccept()) return this.acceptRegistration() | ||
77 | |||
78 | return this.rejectRegistration() | ||
79 | } | ||
80 | |||
81 | isEmailEnabled () { | ||
82 | return this.server.getHTMLConfig().email.enabled | ||
83 | } | ||
84 | |||
85 | isPreventEmailDeliveryChecked () { | ||
86 | return this.form.value.preventEmailDelivery | ||
87 | } | ||
88 | |||
89 | private acceptRegistration () { | ||
90 | this.registrationService.acceptRegistration({ | ||
91 | registration: this.registration, | ||
92 | moderationResponse: this.form.value.moderationResponse, | ||
93 | preventEmailDelivery: this.form.value.preventEmailDelivery | ||
94 | }).subscribe({ | ||
95 | next: () => { | ||
96 | this.notifier.success($localize`${this.registration.username} account created`) | ||
97 | |||
98 | this.registrationProcessed.emit() | ||
99 | this.hide() | ||
100 | }, | ||
101 | |||
102 | error: err => this.notifier.error(err.message) | ||
103 | }) | ||
104 | } | ||
105 | |||
106 | private rejectRegistration () { | ||
107 | this.registrationService.rejectRegistration({ | ||
108 | registration: this.registration, | ||
109 | moderationResponse: this.form.value.moderationResponse, | ||
110 | preventEmailDelivery: this.form.value.preventEmailDelivery | ||
111 | }).subscribe({ | ||
112 | next: () => { | ||
113 | this.notifier.success($localize`${this.registration.username} registration rejected`) | ||
114 | |||
115 | this.registrationProcessed.emit() | ||
116 | this.hide() | ||
117 | }, | ||
118 | |||
119 | error: err => this.notifier.error(err.message) | ||
120 | }) | ||
121 | } | ||
122 | } | ||
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts b/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts new file mode 100644 index 000000000..e01a07d9d --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { Validators } from '@angular/forms' | ||
2 | import { BuildFormValidator } from '@app/shared/form-validators' | ||
3 | |||
4 | export const REGISTRATION_MODERATION_RESPONSE_VALIDATOR: BuildFormValidator = { | ||
5 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], | ||
6 | MESSAGES: { | ||
7 | required: $localize`Moderation response is required.`, | ||
8 | minlength: $localize`Moderation response must be at least 2 characters long.`, | ||
9 | maxlength: $localize`Moderation response cannot be more than 3000 characters long.` | ||
10 | } | ||
11 | } | ||
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.html b/client/src/app/+admin/moderation/registration-list/registration-list.component.html new file mode 100644 index 000000000..a2b888101 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.html | |||
@@ -0,0 +1,135 @@ | |||
1 | <h1> | ||
2 | <my-global-icon iconName="user" aria-hidden="true"></my-global-icon> | ||
3 | <ng-container i18n>Registration requests</ng-container> | ||
4 | </h1> | ||
5 | |||
6 | <p-table | ||
7 | [value]="registrations" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" | ||
8 | [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" | ||
9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" | ||
10 | [(selection)]="selectedRows" [showCurrentPageReport]="true" i18n-currentPageReportTemplate | ||
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} registrations" | ||
12 | [expandedRowKeys]="expandedRows" | ||
13 | > | ||
14 | <ng-template pTemplate="caption"> | ||
15 | <div class="caption"> | ||
16 | <div class="left-buttons"> | ||
17 | <my-action-dropdown | ||
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | ||
19 | [actions]="bulkActions" [entry]="selectedRows" | ||
20 | > | ||
21 | </my-action-dropdown> | ||
22 | </div> | ||
23 | |||
24 | <div class="ms-auto"> | ||
25 | <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> | ||
26 | </div> | ||
27 | </div> | ||
28 | </ng-template> | ||
29 | |||
30 | <ng-template pTemplate="header"> | ||
31 | <tr> <!-- header --> | ||
32 | <th style="width: 40px"> | ||
33 | <p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox> | ||
34 | </th> | ||
35 | <th style="width: 40px;"></th> | ||
36 | <th style="width: 150px;"></th> | ||
37 | <th i18n>Account</th> | ||
38 | <th i18n>Email</th> | ||
39 | <th i18n>Channel</th> | ||
40 | <th i18n>Registration reason</th> | ||
41 | <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> | ||
42 | <th i18n>Moderation response</th> | ||
43 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Requested on <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
44 | </tr> | ||
45 | </ng-template> | ||
46 | |||
47 | <ng-template pTemplate="body" let-expanded="expanded" let-registration> | ||
48 | <tr [pSelectableRow]="registration"> | ||
49 | <td class="checkbox-cell"> | ||
50 | <p-tableCheckbox [value]="registration" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox> | ||
51 | </td> | ||
52 | |||
53 | <td class="expand-cell" [pRowToggler]="registration"> | ||
54 | <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon> | ||
55 | </td> | ||
56 | |||
57 | <td class="action-cell"> | ||
58 | <my-action-dropdown | ||
59 | [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" | ||
60 | i18n-label label="Actions" [actions]="registrationActions" [entry]="registration" | ||
61 | ></my-action-dropdown> | ||
62 | </td> | ||
63 | |||
64 | <td> | ||
65 | <div class="chip two-lines"> | ||
66 | <div> | ||
67 | <span>{{ registration.username }}</span> | ||
68 | <span class="muted">{{ registration.accountDisplayName }}</span> | ||
69 | </div> | ||
70 | </div> | ||
71 | </td> | ||
72 | |||
73 | <td> | ||
74 | <my-user-email-info [entry]="registration" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info> | ||
75 | </td> | ||
76 | |||
77 | <td> | ||
78 | <div class="chip two-lines"> | ||
79 | <div> | ||
80 | <span>{{ registration.channelHandle }}</span> | ||
81 | <span class="muted">{{ registration.channelDisplayName }}</span> | ||
82 | </div> | ||
83 | </div> | ||
84 | </td> | ||
85 | |||
86 | <td container="body" placement="left auto" [ngbTooltip]="registration.registrationReason"> | ||
87 | {{ registration.registrationReason }} | ||
88 | </td> | ||
89 | |||
90 | <td class="c-hand abuse-states" [pRowToggler]="registration"> | ||
91 | <my-global-icon *ngIf="isRegistrationAccepted(registration)" [title]="registration.state.label" iconName="tick"></my-global-icon> | ||
92 | <my-global-icon *ngIf="isRegistrationRejected(registration)" [title]="registration.state.label" iconName="cross"></my-global-icon> | ||
93 | </td> | ||
94 | |||
95 | <td container="body" placement="left auto" [ngbTooltip]="registration.moderationResponse"> | ||
96 | {{ registration.moderationResponse }} | ||
97 | </td> | ||
98 | |||
99 | <td class="c-hand" [pRowToggler]="registration">{{ registration.createdAt | date: 'short' }}</td> | ||
100 | </tr> | ||
101 | </ng-template> | ||
102 | |||
103 | <ng-template pTemplate="rowexpansion" let-registration> | ||
104 | <tr> | ||
105 | <td colspan="9"> | ||
106 | <div class="moderation-expanded"> | ||
107 | <div class="left"> | ||
108 | <div class="d-flex"> | ||
109 | <span class="moderation-expanded-label" i18n>Registration reason:</span> | ||
110 | <span class="moderation-expanded-text" [innerHTML]="registration.registrationReasonHTML"></span> | ||
111 | </div> | ||
112 | |||
113 | <div *ngIf="registration.moderationResponse"> | ||
114 | <span class="moderation-expanded-label" i18n>Moderation response:</span> | ||
115 | <span class="moderation-expanded-text" [innerHTML]="registration.moderationResponseHTML"></span> | ||
116 | </div> | ||
117 | </div> | ||
118 | </div> | ||
119 | </td> | ||
120 | </tr> | ||
121 | </ng-template> | ||
122 | |||
123 | <ng-template pTemplate="emptymessage"> | ||
124 | <tr> | ||
125 | <td colspan="9"> | ||
126 | <div class="no-results"> | ||
127 | <ng-container *ngIf="search" i18n>No registrations found matching current filters.</ng-container> | ||
128 | <ng-container *ngIf="!search" i18n>No registrations found.</ng-container> | ||
129 | </div> | ||
130 | </td> | ||
131 | </tr> | ||
132 | </ng-template> | ||
133 | </p-table> | ||
134 | |||
135 | <my-process-registration-modal #processRegistrationModal (registrationProcessed)="onRegistrationProcessed()"></my-process-registration-modal> | ||
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.scss b/client/src/app/+admin/moderation/registration-list/registration-list.component.scss new file mode 100644 index 000000000..9cae08e85 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.scss | |||
@@ -0,0 +1,7 @@ | |||
1 | @use '_mixins' as *; | ||
2 | @use '_variables' as *; | ||
3 | |||
4 | my-global-icon { | ||
5 | width: 24px; | ||
6 | height: 24px; | ||
7 | } | ||
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts new file mode 100644 index 000000000..ed8fbec51 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts | |||
@@ -0,0 +1,151 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | ||
5 | import { prepareIcu } from '@app/helpers' | ||
6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | ||
7 | import { DropdownAction } from '@app/shared/shared-main' | ||
8 | import { UserRegistration, UserRegistrationState } from '@shared/models' | ||
9 | import { AdminRegistrationService } from './admin-registration.service' | ||
10 | import { ProcessRegistrationModalComponent } from './process-registration-modal.component' | ||
11 | |||
12 | @Component({ | ||
13 | selector: 'my-registration-list', | ||
14 | templateUrl: './registration-list.component.html', | ||
15 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './registration-list.component.scss' ] | ||
16 | }) | ||
17 | export class RegistrationListComponent extends RestTable <UserRegistration> implements OnInit { | ||
18 | @ViewChild('processRegistrationModal', { static: true }) processRegistrationModal: ProcessRegistrationModalComponent | ||
19 | |||
20 | registrations: (UserRegistration & { registrationReasonHTML?: string, moderationResponseHTML?: string })[] = [] | ||
21 | totalRecords = 0 | ||
22 | sort: SortMeta = { field: 'createdAt', order: -1 } | ||
23 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
24 | |||
25 | registrationActions: DropdownAction<UserRegistration>[][] = [] | ||
26 | bulkActions: DropdownAction<UserRegistration[]>[] = [] | ||
27 | |||
28 | inputFilters: AdvancedInputFilter[] = [] | ||
29 | |||
30 | requiresEmailVerification: boolean | ||
31 | |||
32 | constructor ( | ||
33 | protected route: ActivatedRoute, | ||
34 | protected router: Router, | ||
35 | private server: ServerService, | ||
36 | private notifier: Notifier, | ||
37 | private markdownRenderer: MarkdownService, | ||
38 | private confirmService: ConfirmService, | ||
39 | private adminRegistrationService: AdminRegistrationService | ||
40 | ) { | ||
41 | super() | ||
42 | |||
43 | this.registrationActions = [ | ||
44 | [ | ||
45 | { | ||
46 | label: $localize`Accept this request`, | ||
47 | handler: registration => this.openRegistrationRequestProcessModal(registration, 'accept'), | ||
48 | isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING | ||
49 | }, | ||
50 | { | ||
51 | label: $localize`Reject this request`, | ||
52 | handler: registration => this.openRegistrationRequestProcessModal(registration, 'reject'), | ||
53 | isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING | ||
54 | }, | ||
55 | { | ||
56 | label: $localize`Remove this request`, | ||
57 | handler: registration => this.removeRegistrations([ registration ]) | ||
58 | } | ||
59 | ] | ||
60 | ] | ||
61 | |||
62 | this.bulkActions = [ | ||
63 | { | ||
64 | label: $localize`Delete`, | ||
65 | handler: registrations => this.removeRegistrations(registrations) | ||
66 | } | ||
67 | ] | ||
68 | } | ||
69 | |||
70 | ngOnInit () { | ||
71 | this.initialize() | ||
72 | |||
73 | this.server.getConfig() | ||
74 | .subscribe(config => { | ||
75 | this.requiresEmailVerification = config.signup.requiresEmailVerification | ||
76 | }) | ||
77 | } | ||
78 | |||
79 | getIdentifier () { | ||
80 | return 'RegistrationListComponent' | ||
81 | } | ||
82 | |||
83 | isRegistrationAccepted (registration: UserRegistration) { | ||
84 | return registration.state.id === UserRegistrationState.ACCEPTED | ||
85 | } | ||
86 | |||
87 | isRegistrationRejected (registration: UserRegistration) { | ||
88 | return registration.state.id === UserRegistrationState.REJECTED | ||
89 | } | ||
90 | |||
91 | onRegistrationProcessed () { | ||
92 | this.reloadData() | ||
93 | } | ||
94 | |||
95 | protected reloadDataInternal () { | ||
96 | this.adminRegistrationService.listRegistrations({ | ||
97 | pagination: this.pagination, | ||
98 | sort: this.sort, | ||
99 | search: this.search | ||
100 | }).subscribe({ | ||
101 | next: async resultList => { | ||
102 | this.totalRecords = resultList.total | ||
103 | this.registrations = resultList.data | ||
104 | |||
105 | for (const registration of this.registrations) { | ||
106 | registration.registrationReasonHTML = await this.toHtml(registration.registrationReason) | ||
107 | registration.moderationResponseHTML = await this.toHtml(registration.moderationResponse) | ||
108 | } | ||
109 | }, | ||
110 | |||
111 | error: err => this.notifier.error(err.message) | ||
112 | }) | ||
113 | } | ||
114 | |||
115 | private openRegistrationRequestProcessModal (registration: UserRegistration, mode: 'accept' | 'reject') { | ||
116 | this.processRegistrationModal.openModal(registration, mode) | ||
117 | } | ||
118 | |||
119 | private async removeRegistrations (registrations: UserRegistration[]) { | ||
120 | const icuParams = { count: registrations.length, username: registrations[0].username } | ||
121 | |||
122 | // eslint-disable-next-line max-len | ||
123 | const message = prepareIcu($localize`Do you really want to delete {count, plural, =1 {{username} registration request?} other {{count} registration requests?}}`)( | ||
124 | icuParams, | ||
125 | $localize`Do you really want to delete these registration requests?` | ||
126 | ) | ||
127 | |||
128 | const res = await this.confirmService.confirm(message, $localize`Delete`) | ||
129 | if (res === false) return | ||
130 | |||
131 | this.adminRegistrationService.removeRegistrations(registrations) | ||
132 | .subscribe({ | ||
133 | next: () => { | ||
134 | // eslint-disable-next-line max-len | ||
135 | const message = prepareIcu($localize`Removed {count, plural, =1 {{username} registration request} other {{count} registration requests}}`)( | ||
136 | icuParams, | ||
137 | $localize`Registration requests removed` | ||
138 | ) | ||
139 | |||
140 | this.notifier.success(message) | ||
141 | this.reloadData() | ||
142 | }, | ||
143 | |||
144 | error: err => this.notifier.error(err.message) | ||
145 | }) | ||
146 | } | ||
147 | |||
148 | private toHtml (text: string) { | ||
149 | return this.markdownRenderer.textMarkdownToHTML({ markdown: text }) | ||
150 | } | ||
151 | } | ||
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts index efd99e52b..f365a2500 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts | |||
@@ -159,26 +159,25 @@ export class VideoBlockListComponent extends RestTable implements OnInit { | |||
159 | }) | 159 | }) |
160 | } | 160 | } |
161 | 161 | ||
162 | protected reloadData () { | 162 | protected reloadDataInternal () { |
163 | this.videoBlocklistService.listBlocks({ | 163 | this.videoBlocklistService.listBlocks({ |
164 | pagination: this.pagination, | 164 | pagination: this.pagination, |
165 | sort: this.sort, | 165 | sort: this.sort, |
166 | search: this.search | 166 | search: this.search |
167 | }) | 167 | }).subscribe({ |
168 | .subscribe({ | 168 | next: async resultList => { |
169 | next: async resultList => { | 169 | this.totalRecords = resultList.total |
170 | this.totalRecords = resultList.total | ||
171 | 170 | ||
172 | this.blocklist = resultList.data | 171 | this.blocklist = resultList.data |
173 | 172 | ||
174 | for (const element of this.blocklist) { | 173 | for (const element of this.blocklist) { |
175 | Object.assign(element, { | 174 | Object.assign(element, { |
176 | reasonHtml: await this.toHtml(element.reason) | 175 | reasonHtml: await this.toHtml(element.reason) |
177 | }) | 176 | }) |
178 | } | 177 | } |
179 | }, | 178 | }, |
180 | 179 | ||
181 | error: err => this.notifier.error(err.message) | 180 | error: err => this.notifier.error(err.message) |
182 | }) | 181 | }) |
183 | } | 182 | } |
184 | } | 183 | } |
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.html b/client/src/app/+admin/overview/comments/video-comment-list.component.html index d2ca5f700..b0d8131bf 100644 --- a/client/src/app/+admin/overview/comments/video-comment-list.component.html +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.html | |||
@@ -13,14 +13,14 @@ | |||
13 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" | 13 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" |
14 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 14 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
15 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} comments" | 15 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} comments" |
16 | [expandedRowKeys]="expandedRows" [(selection)]="selectedComments" | 16 | [expandedRowKeys]="expandedRows" [(selection)]="selectedRows" |
17 | > | 17 | > |
18 | <ng-template pTemplate="caption"> | 18 | <ng-template pTemplate="caption"> |
19 | <div class="caption"> | 19 | <div class="caption"> |
20 | <div> | 20 | <div> |
21 | <my-action-dropdown | 21 | <my-action-dropdown |
22 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | 22 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" |
23 | [actions]="bulkCommentActions" [entry]="selectedComments" | 23 | [actions]="bulkActions" [entry]="selectedRows" |
24 | > | 24 | > |
25 | </my-action-dropdown> | 25 | </my-action-dropdown> |
26 | </div> | 26 | </div> |
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.ts b/client/src/app/+admin/overview/comments/video-comment-list.component.ts index c95d2ffeb..28efdc076 100644 --- a/client/src/app/+admin/overview/comments/video-comment-list.component.ts +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts | |||
@@ -14,7 +14,7 @@ import { prepareIcu } from '@app/helpers' | |||
14 | templateUrl: './video-comment-list.component.html', | 14 | templateUrl: './video-comment-list.component.html', |
15 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ] | 15 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ] |
16 | }) | 16 | }) |
17 | export class VideoCommentListComponent extends RestTable implements OnInit { | 17 | export class VideoCommentListComponent extends RestTable <VideoCommentAdmin> implements OnInit { |
18 | comments: VideoCommentAdmin[] | 18 | comments: VideoCommentAdmin[] |
19 | totalRecords = 0 | 19 | totalRecords = 0 |
20 | sort: SortMeta = { field: 'createdAt', order: -1 } | 20 | sort: SortMeta = { field: 'createdAt', order: -1 } |
@@ -40,8 +40,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
40 | } | 40 | } |
41 | ] | 41 | ] |
42 | 42 | ||
43 | selectedComments: VideoCommentAdmin[] = [] | 43 | bulkActions: DropdownAction<VideoCommentAdmin[]>[] = [] |
44 | bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = [] | ||
45 | 44 | ||
46 | inputFilters: AdvancedInputFilter[] = [ | 45 | inputFilters: AdvancedInputFilter[] = [ |
47 | { | 46 | { |
@@ -100,7 +99,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
100 | ngOnInit () { | 99 | ngOnInit () { |
101 | this.initialize() | 100 | this.initialize() |
102 | 101 | ||
103 | this.bulkCommentActions = [ | 102 | this.bulkActions = [ |
104 | { | 103 | { |
105 | label: $localize`Delete`, | 104 | label: $localize`Delete`, |
106 | handler: comments => this.removeComments(comments), | 105 | handler: comments => this.removeComments(comments), |
@@ -118,11 +117,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
118 | return this.markdownRenderer.textMarkdownToHTML({ markdown: text, withHtml: true, withEmoji: true }) | 117 | return this.markdownRenderer.textMarkdownToHTML({ markdown: text, withHtml: true, withEmoji: true }) |
119 | } | 118 | } |
120 | 119 | ||
121 | isInSelectionMode () { | 120 | protected reloadDataInternal () { |
122 | return this.selectedComments.length !== 0 | ||
123 | } | ||
124 | |||
125 | reloadData () { | ||
126 | this.videoCommentService.getAdminVideoComments({ | 121 | this.videoCommentService.getAdminVideoComments({ |
127 | pagination: this.pagination, | 122 | pagination: this.pagination, |
128 | sort: this.sort, | 123 | sort: this.sort, |
@@ -162,7 +157,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
162 | 157 | ||
163 | error: err => this.notifier.error(err.message), | 158 | error: err => this.notifier.error(err.message), |
164 | 159 | ||
165 | complete: () => this.selectedComments = [] | 160 | complete: () => this.selectedRows = [] |
166 | }) | 161 | }) |
167 | } | 162 | } |
168 | 163 | ||
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.html b/client/src/app/+admin/overview/users/user-list/user-list.component.html index a96ce561c..7eb5e0fc7 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.html +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.html | |||
@@ -6,7 +6,7 @@ | |||
6 | <p-table | 6 | <p-table |
7 | [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" | 7 | [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" |
8 | [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" | 8 | [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" |
9 | [(selection)]="selectedUsers" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" | 9 | [(selection)]="selectedRows" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" |
10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users" | 11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users" |
12 | [expandedRowKeys]="expandedRows" | 12 | [expandedRowKeys]="expandedRows" |
@@ -16,7 +16,7 @@ | |||
16 | <div class="left-buttons"> | 16 | <div class="left-buttons"> |
17 | <my-action-dropdown | 17 | <my-action-dropdown |
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | 18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" |
19 | [actions]="bulkUserActions" [entry]="selectedUsers" | 19 | [actions]="bulkActions" [entry]="selectedRows" |
20 | > | 20 | > |
21 | </my-action-dropdown> | 21 | </my-action-dropdown> |
22 | 22 | ||
@@ -95,7 +95,7 @@ | |||
95 | <div class="chip two-lines"> | 95 | <div class="chip two-lines"> |
96 | <my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar> | 96 | <my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar> |
97 | <div> | 97 | <div> |
98 | <span class="user-table-primary-text">{{ user.account.displayName }}</span> | 98 | <span>{{ user.account.displayName }}</span> |
99 | <span class="muted">{{ user.username }}</span> | 99 | <span class="muted">{{ user.username }}</span> |
100 | </div> | 100 | </div> |
101 | </div> | 101 | </div> |
@@ -110,23 +110,10 @@ | |||
110 | <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span> | 110 | <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span> |
111 | </td> | 111 | </td> |
112 | 112 | ||
113 | <td *ngIf="isSelected('email')" [title]="user.email"> | 113 | <td *ngIf="isSelected('email')"> |
114 | <ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus"> | 114 | <my-user-email-info [entry]="user" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info> |
115 | <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a> | ||
116 | </ng-container> | ||
117 | </td> | 115 | </td> |
118 | 116 | ||
119 | <ng-template #emailWithVerificationStatus> | ||
120 | <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login"> | ||
121 | <em>? {{ user.email }}</em> | ||
122 | </td> | ||
123 | <ng-template #emailVerifiedNotFalse> | ||
124 | <td i18n-title title="User's email is verified / User can login without email verification"> | ||
125 | ✓ {{ user.email }} | ||
126 | </td> | ||
127 | </ng-template> | ||
128 | </ng-template> | ||
129 | |||
130 | <td *ngIf="isSelected('quota')"> | 117 | <td *ngIf="isSelected('quota')"> |
131 | <div class="progress" i18n-title title="Total video quota"> | 118 | <div class="progress" i18n-title title="Total video quota"> |
132 | <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }" | 119 | <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }" |
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.scss b/client/src/app/+admin/overview/users/user-list/user-list.component.scss index 23e0d29ee..2a3b955d2 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.scss | |||
@@ -10,12 +10,6 @@ tr.banned > td { | |||
10 | background-color: lighten($color: $red, $amount: 40) !important; | 10 | background-color: lighten($color: $red, $amount: 40) !important; |
11 | } | 11 | } |
12 | 12 | ||
13 | .table-email { | ||
14 | @include disable-default-a-behaviour; | ||
15 | |||
16 | color: pvar(--mainForegroundColor); | ||
17 | } | ||
18 | |||
19 | .banned-info { | 13 | .banned-info { |
20 | font-style: italic; | 14 | font-style: italic; |
21 | } | 15 | } |
@@ -37,10 +31,6 @@ my-global-icon { | |||
37 | width: 18px; | 31 | width: 18px; |
38 | } | 32 | } |
39 | 33 | ||
40 | .chip { | ||
41 | @include chip; | ||
42 | } | ||
43 | |||
44 | .progress { | 34 | .progress { |
45 | @include progressbar($small: true); | 35 | @include progressbar($small: true); |
46 | 36 | ||
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts index 99987fdff..19420b748 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts | |||
@@ -22,7 +22,7 @@ type UserForList = User & { | |||
22 | templateUrl: './user-list.component.html', | 22 | templateUrl: './user-list.component.html', |
23 | styleUrls: [ './user-list.component.scss' ] | 23 | styleUrls: [ './user-list.component.scss' ] |
24 | }) | 24 | }) |
25 | export class UserListComponent extends RestTable implements OnInit { | 25 | export class UserListComponent extends RestTable <User> implements OnInit { |
26 | private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns' | 26 | private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns' |
27 | 27 | ||
28 | @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent | 28 | @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent |
@@ -35,8 +35,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
35 | 35 | ||
36 | highlightBannedUsers = false | 36 | highlightBannedUsers = false |
37 | 37 | ||
38 | selectedUsers: User[] = [] | 38 | bulkActions: DropdownAction<User[]>[][] = [] |
39 | bulkUserActions: DropdownAction<User[]>[][] = [] | ||
40 | columns: { id: string, label: string }[] | 39 | columns: { id: string, label: string }[] |
41 | 40 | ||
42 | inputFilters: AdvancedInputFilter[] = [ | 41 | inputFilters: AdvancedInputFilter[] = [ |
@@ -95,7 +94,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
95 | 94 | ||
96 | this.initialize() | 95 | this.initialize() |
97 | 96 | ||
98 | this.bulkUserActions = [ | 97 | this.bulkActions = [ |
99 | [ | 98 | [ |
100 | { | 99 | { |
101 | label: $localize`Delete`, | 100 | label: $localize`Delete`, |
@@ -249,7 +248,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
249 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 248 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
250 | if (res === false) return | 249 | if (res === false) return |
251 | 250 | ||
252 | this.userAdminService.removeUser(users) | 251 | this.userAdminService.removeUsers(users) |
253 | .subscribe({ | 252 | .subscribe({ |
254 | next: () => { | 253 | next: () => { |
255 | this.notifier.success( | 254 | this.notifier.success( |
@@ -284,13 +283,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
284 | }) | 283 | }) |
285 | } | 284 | } |
286 | 285 | ||
287 | isInSelectionMode () { | 286 | protected reloadDataInternal () { |
288 | return this.selectedUsers.length !== 0 | ||
289 | } | ||
290 | |||
291 | protected reloadData () { | ||
292 | this.selectedUsers = [] | ||
293 | |||
294 | this.userAdminService.getUsers({ | 287 | this.userAdminService.getUsers({ |
295 | pagination: this.pagination, | 288 | pagination: this.pagination, |
296 | sort: this.sort, | 289 | sort: this.sort, |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html index a6cd2e257..5b8405ad9 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.html +++ b/client/src/app/+admin/overview/videos/video-list.component.html | |||
@@ -6,7 +6,7 @@ | |||
6 | <p-table | 6 | <p-table |
7 | [value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" | 7 | [value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" |
8 | [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" | 8 | [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" |
9 | [(selection)]="selectedVideos" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" | 9 | [(selection)]="selectedRows" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" |
10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} videos" | 11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} videos" |
12 | [expandedRowKeys]="expandedRows" [ngClass]="{ loading: loading }" | 12 | [expandedRowKeys]="expandedRows" [ngClass]="{ loading: loading }" |
@@ -16,7 +16,7 @@ | |||
16 | <div class="left-buttons"> | 16 | <div class="left-buttons"> |
17 | <my-action-dropdown | 17 | <my-action-dropdown |
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | 18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" |
19 | [actions]="bulkVideoActions" [entry]="selectedVideos" | 19 | [actions]="bulkActions" [entry]="selectedRows" |
20 | > | 20 | > |
21 | </my-action-dropdown> | 21 | </my-action-dropdown> |
22 | </div> | 22 | </div> |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts index 4d3e9873c..1ea295499 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.ts +++ b/client/src/app/+admin/overview/videos/video-list.component.ts | |||
@@ -17,7 +17,7 @@ import { VideoAdminService } from './video-admin.service' | |||
17 | templateUrl: './video-list.component.html', | 17 | templateUrl: './video-list.component.html', |
18 | styleUrls: [ './video-list.component.scss' ] | 18 | styleUrls: [ './video-list.component.scss' ] |
19 | }) | 19 | }) |
20 | export class VideoListComponent extends RestTable implements OnInit { | 20 | export class VideoListComponent extends RestTable <Video> implements OnInit { |
21 | @ViewChild('videoBlockModal') videoBlockModal: VideoBlockComponent | 21 | @ViewChild('videoBlockModal') videoBlockModal: VideoBlockComponent |
22 | 22 | ||
23 | videos: Video[] = [] | 23 | videos: Video[] = [] |
@@ -26,9 +26,7 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
26 | sort: SortMeta = { field: 'publishedAt', order: -1 } | 26 | sort: SortMeta = { field: 'publishedAt', order: -1 } |
27 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 27 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
28 | 28 | ||
29 | bulkVideoActions: DropdownAction<Video[]>[][] = [] | 29 | bulkActions: DropdownAction<Video[]>[][] = [] |
30 | |||
31 | selectedVideos: Video[] = [] | ||
32 | 30 | ||
33 | inputFilters: AdvancedInputFilter[] | 31 | inputFilters: AdvancedInputFilter[] |
34 | 32 | ||
@@ -72,7 +70,7 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
72 | 70 | ||
73 | this.inputFilters = this.videoAdminService.buildAdminInputFilter() | 71 | this.inputFilters = this.videoAdminService.buildAdminInputFilter() |
74 | 72 | ||
75 | this.bulkVideoActions = [ | 73 | this.bulkActions = [ |
76 | [ | 74 | [ |
77 | { | 75 | { |
78 | label: $localize`Delete`, | 76 | label: $localize`Delete`, |
@@ -126,10 +124,6 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
126 | return 'VideoListComponent' | 124 | return 'VideoListComponent' |
127 | } | 125 | } |
128 | 126 | ||
129 | isInSelectionMode () { | ||
130 | return this.selectedVideos.length !== 0 | ||
131 | } | ||
132 | |||
133 | getPrivacyBadgeClass (video: Video) { | 127 | getPrivacyBadgeClass (video: Video) { |
134 | if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green' | 128 | if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green' |
135 | 129 | ||
@@ -189,9 +183,23 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
189 | return files.reduce((p, f) => p += f.size, 0) | 183 | return files.reduce((p, f) => p += f.size, 0) |
190 | } | 184 | } |
191 | 185 | ||
192 | reloadData () { | 186 | async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'webtorrent') { |
193 | this.selectedVideos = [] | 187 | const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?` |
188 | const res = await this.confirmService.confirm(message, $localize`Delete file`) | ||
189 | if (res === false) return | ||
190 | |||
191 | this.videoService.removeFile(video.uuid, file.id, type) | ||
192 | .subscribe({ | ||
193 | next: () => { | ||
194 | this.notifier.success($localize`File removed.`) | ||
195 | this.reloadData() | ||
196 | }, | ||
197 | |||
198 | error: err => this.notifier.error(err.message) | ||
199 | }) | ||
200 | } | ||
194 | 201 | ||
202 | protected reloadDataInternal () { | ||
195 | this.loading = true | 203 | this.loading = true |
196 | 204 | ||
197 | this.videoAdminService.getAdminVideos({ | 205 | this.videoAdminService.getAdminVideos({ |
@@ -209,22 +217,6 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
209 | }) | 217 | }) |
210 | } | 218 | } |
211 | 219 | ||
212 | async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'webtorrent') { | ||
213 | const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?` | ||
214 | const res = await this.confirmService.confirm(message, $localize`Delete file`) | ||
215 | if (res === false) return | ||
216 | |||
217 | this.videoService.removeFile(video.uuid, file.id, type) | ||
218 | .subscribe({ | ||
219 | next: () => { | ||
220 | this.notifier.success($localize`File removed.`) | ||
221 | this.reloadData() | ||
222 | }, | ||
223 | |||
224 | error: err => this.notifier.error(err.message) | ||
225 | }) | ||
226 | } | ||
227 | |||
228 | private async removeVideos (videos: Video[]) { | 220 | private async removeVideos (videos: Video[]) { |
229 | const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)( | 221 | const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)( |
230 | { count: videos.length }, | 222 | { count: videos.length }, |
diff --git a/client/src/app/+admin/shared/shared-admin.module.ts b/client/src/app/+admin/shared/shared-admin.module.ts index bef7d54ef..a5c300d12 100644 --- a/client/src/app/+admin/shared/shared-admin.module.ts +++ b/client/src/app/+admin/shared/shared-admin.module.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { SharedMainModule } from '../../shared/shared-main/shared-main.module' | 2 | import { SharedMainModule } from '../../shared/shared-main/shared-main.module' |
3 | import { UserEmailInfoComponent } from './user-email-info.component' | ||
3 | import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' | 4 | import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' |
4 | 5 | ||
5 | @NgModule({ | 6 | @NgModule({ |
@@ -8,11 +9,13 @@ import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' | |||
8 | ], | 9 | ], |
9 | 10 | ||
10 | declarations: [ | 11 | declarations: [ |
11 | UserRealQuotaInfoComponent | 12 | UserRealQuotaInfoComponent, |
13 | UserEmailInfoComponent | ||
12 | ], | 14 | ], |
13 | 15 | ||
14 | exports: [ | 16 | exports: [ |
15 | UserRealQuotaInfoComponent | 17 | UserRealQuotaInfoComponent, |
18 | UserEmailInfoComponent | ||
16 | ], | 19 | ], |
17 | 20 | ||
18 | providers: [] | 21 | providers: [] |
diff --git a/client/src/app/+admin/shared/user-email-info.component.html b/client/src/app/+admin/shared/user-email-info.component.html new file mode 100644 index 000000000..244240619 --- /dev/null +++ b/client/src/app/+admin/shared/user-email-info.component.html | |||
@@ -0,0 +1,13 @@ | |||
1 | <ng-container> | ||
2 | <a [href]="'mailto:' + entry.email" [title]="getTitle()"> | ||
3 | <ng-container *ngIf="!requiresEmailVerification"> | ||
4 | {{ entry.email }} | ||
5 | </ng-container> | ||
6 | |||
7 | <ng-container *ngIf="requiresEmailVerification"> | ||
8 | <em *ngIf="!entry.emailVerified">? {{ entry.email }}</em> | ||
9 | |||
10 | <ng-container *ngIf="entry.emailVerified === true">✓ {{ entry.email }}</ng-container> | ||
11 | </ng-container> | ||
12 | </a> | ||
13 | </ng-container> | ||
diff --git a/client/src/app/+admin/shared/user-email-info.component.scss b/client/src/app/+admin/shared/user-email-info.component.scss new file mode 100644 index 000000000..d34947edd --- /dev/null +++ b/client/src/app/+admin/shared/user-email-info.component.scss | |||
@@ -0,0 +1,10 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
4 | a { | ||
5 | color: pvar(--mainForegroundColor); | ||
6 | |||
7 | &:hover { | ||
8 | text-decoration: underline; | ||
9 | } | ||
10 | } | ||
diff --git a/client/src/app/+admin/shared/user-email-info.component.ts b/client/src/app/+admin/shared/user-email-info.component.ts new file mode 100644 index 000000000..e33948b60 --- /dev/null +++ b/client/src/app/+admin/shared/user-email-info.component.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { User, UserRegistration } from '@shared/models/users' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-user-email-info', | ||
6 | templateUrl: './user-email-info.component.html', | ||
7 | styleUrls: [ './user-email-info.component.scss' ] | ||
8 | }) | ||
9 | export class UserEmailInfoComponent { | ||
10 | @Input() entry: User | UserRegistration | ||
11 | @Input() requiresEmailVerification: boolean | ||
12 | |||
13 | getTitle () { | ||
14 | if (this.entry.emailVerified) { | ||
15 | return $localize`User email has been verified` | ||
16 | } | ||
17 | |||
18 | return $localize`User email hasn't been verified` | ||
19 | } | ||
20 | } | ||
diff --git a/client/src/app/+admin/system/jobs/job.service.ts b/client/src/app/+admin/system/jobs/job.service.ts index ef8ddd3b4..031e2bad8 100644 --- a/client/src/app/+admin/system/jobs/job.service.ts +++ b/client/src/app/+admin/system/jobs/job.service.ts | |||
@@ -19,7 +19,7 @@ export class JobService { | |||
19 | private restExtractor: RestExtractor | 19 | private restExtractor: RestExtractor |
20 | ) {} | 20 | ) {} |
21 | 21 | ||
22 | getJobs (options: { | 22 | listJobs (options: { |
23 | jobState?: JobStateClient | 23 | jobState?: JobStateClient |
24 | jobType: JobTypeClient | 24 | jobType: JobTypeClient |
25 | pagination: RestPagination | 25 | pagination: RestPagination |
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts index b8f3c3a68..6e10c81ff 100644 --- a/client/src/app/+admin/system/jobs/jobs.component.ts +++ b/client/src/app/+admin/system/jobs/jobs.component.ts | |||
@@ -120,12 +120,12 @@ export class JobsComponent extends RestTable implements OnInit { | |||
120 | this.reloadData() | 120 | this.reloadData() |
121 | } | 121 | } |
122 | 122 | ||
123 | protected reloadData () { | 123 | protected reloadDataInternal () { |
124 | let jobState = this.jobState as JobState | 124 | let jobState = this.jobState as JobState |
125 | if (this.jobState === 'all') jobState = null | 125 | if (this.jobState === 'all') jobState = null |
126 | 126 | ||
127 | this.jobsService | 127 | this.jobsService |
128 | .getJobs({ | 128 | .listJobs({ |
129 | jobState, | 129 | jobState, |
130 | jobType: this.jobType, | 130 | jobType: this.jobType, |
131 | pagination: this.pagination, | 131 | pagination: this.pagination, |
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts index c1705807f..c03af38f2 100644 --- a/client/src/app/+login/login.component.ts +++ b/client/src/app/+login/login.component.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { environment } from 'src/environments/environment' | ||
1 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 2 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' | 4 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' |
@@ -7,8 +8,8 @@ import { USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-valid | |||
7 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' | 8 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' |
8 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | 9 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' |
9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 10 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
10 | import { PluginsManager } from '@root-helpers/plugins-manager' | 11 | import { getExternalAuthHref } from '@shared/core-utils' |
11 | import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' | 12 | import { RegisteredExternalAuthConfig, ServerConfig, ServerErrorCode } from '@shared/models' |
12 | 13 | ||
13 | @Component({ | 14 | @Component({ |
14 | selector: 'my-login', | 15 | selector: 'my-login', |
@@ -119,7 +120,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni | |||
119 | } | 120 | } |
120 | 121 | ||
121 | getAuthHref (auth: RegisteredExternalAuthConfig) { | 122 | getAuthHref (auth: RegisteredExternalAuthConfig) { |
122 | return PluginsManager.getExternalAuthHref(auth) | 123 | return getExternalAuthHref(environment.apiUrl, auth) |
123 | } | 124 | } |
124 | 125 | ||
125 | login () { | 126 | login () { |
@@ -196,6 +197,8 @@ The link will expire within 1 hour.` | |||
196 | } | 197 | } |
197 | 198 | ||
198 | private handleError (err: any) { | 199 | private handleError (err: any) { |
200 | console.log(err) | ||
201 | |||
199 | if (this.authService.isOTPMissingError(err)) { | 202 | if (this.authService.isOTPMissingError(err)) { |
200 | this.otpStep = true | 203 | this.otpStep = true |
201 | 204 | ||
@@ -207,8 +210,26 @@ The link will expire within 1 hour.` | |||
207 | return | 210 | return |
208 | } | 211 | } |
209 | 212 | ||
210 | if (err.message.indexOf('credentials are invalid') !== -1) this.error = $localize`Incorrect username or password.` | 213 | if (err.message.includes('credentials are invalid')) { |
211 | else if (err.message.indexOf('blocked') !== -1) this.error = $localize`Your account is blocked.` | 214 | this.error = $localize`Incorrect username or password.` |
212 | else this.error = err.message | 215 | return |
216 | } | ||
217 | |||
218 | if (err.message.includes('blocked')) { | ||
219 | this.error = $localize`Your account is blocked.` | ||
220 | return | ||
221 | } | ||
222 | |||
223 | if (err.body?.code === ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL) { | ||
224 | this.error = $localize`This account is awaiting approval by moderators.` | ||
225 | return | ||
226 | } | ||
227 | |||
228 | if (err.body?.code === ServerErrorCode.ACCOUNT_APPROVAL_REJECTED) { | ||
229 | this.error = $localize`Registration approval has been rejected for this account.` | ||
230 | return | ||
231 | } | ||
232 | |||
233 | this.error = err.message | ||
213 | } | 234 | } |
214 | } | 235 | } |
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.scss b/client/src/app/+my-library/my-ownership/my-ownership.component.scss index a8450ff1b..98bed226d 100644 --- a/client/src/app/+my-library/my-ownership/my-ownership.component.scss +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.scss | |||
@@ -2,10 +2,6 @@ | |||
2 | @use '_miniature' as *; | 2 | @use '_miniature' as *; |
3 | @use '_mixins' as *; | 3 | @use '_mixins' as *; |
4 | 4 | ||
5 | .chip { | ||
6 | @include chip; | ||
7 | } | ||
8 | |||
9 | .video-table-video { | 5 | .video-table-video { |
10 | display: inline-flex; | 6 | display: inline-flex; |
11 | 7 | ||
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-ownership.component.ts index 7ea940ceb..8d6a42dfb 100644 --- a/client/src/app/+my-library/my-ownership/my-ownership.component.ts +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.ts | |||
@@ -59,7 +59,7 @@ export class MyOwnershipComponent extends RestTable implements OnInit { | |||
59 | }) | 59 | }) |
60 | } | 60 | } |
61 | 61 | ||
62 | protected reloadData () { | 62 | protected reloadDataInternal () { |
63 | return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) | 63 | return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) |
64 | .subscribe({ | 64 | .subscribe({ |
65 | next: resultList => { | 65 | next: resultList => { |
diff --git a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts index d18e78201..74dbe222d 100644 --- a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts +++ b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts | |||
@@ -68,7 +68,7 @@ export class MyVideoChannelSyncsComponent extends RestTable implements OnInit { | |||
68 | ] | 68 | ] |
69 | } | 69 | } |
70 | 70 | ||
71 | protected reloadData () { | 71 | protected reloadDataInternal () { |
72 | this.error = undefined | 72 | this.error = undefined |
73 | 73 | ||
74 | this.authService.userInformationLoaded | 74 | this.authService.userInformationLoaded |
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts index 46d689bd1..7d82f62b9 100644 --- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts +++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts | |||
@@ -90,7 +90,7 @@ export class MyVideoImportsComponent extends RestTable implements OnInit { | |||
90 | }) | 90 | }) |
91 | } | 91 | } |
92 | 92 | ||
93 | protected reloadData () { | 93 | protected reloadDataInternal () { |
94 | this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search) | 94 | this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search) |
95 | .subscribe({ | 95 | .subscribe({ |
96 | next: resultList => { | 96 | next: resultList => { |
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html index bafb96a49..86763e801 100644 --- a/client/src/app/+signup/+register/register.component.html +++ b/client/src/app/+signup/+register/register.component.html | |||
@@ -5,29 +5,34 @@ | |||
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <ng-container *ngIf="!signupDisabled"> | 7 | <ng-container *ngIf="!signupDisabled"> |
8 | <h1 i18n class="title-page-v2"> | 8 | <h1 class="title-page-v2"> |
9 | <strong class="underline-orange">{{ instanceName }}</strong> | 9 | <strong class="underline-orange">{{ instanceName }}</strong> |
10 | > | 10 | > |
11 | Create an account | 11 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> |
12 | </h1> | 12 | </h1> |
13 | 13 | ||
14 | <div class="register-content"> | 14 | <div class="register-content"> |
15 | <my-custom-stepper linear> | 15 | <my-custom-stepper linear> |
16 | 16 | ||
17 | <cdk-step i18n-label label="About" [editable]="!signupSuccess"> | 17 | <cdk-step i18n-label label="About" [editable]="!signupSuccess"> |
18 | <my-signup-step-title mascotImageName="about" i18n> | 18 | <my-signup-step-title mascotImageName="about"> |
19 | <strong>Create an account</strong> | 19 | <strong> |
20 | <div>on {{ instanceName }}</div> | 20 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> |
21 | </strong> | ||
22 | |||
23 | <div i18n>on {{ instanceName }}</div> | ||
21 | </my-signup-step-title> | 24 | </my-signup-step-title> |
22 | 25 | ||
23 | <my-register-step-about [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about> | 26 | <my-register-step-about [requiresApproval]="requiresApproval" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about> |
24 | 27 | ||
25 | <div class="step-buttons"> | 28 | <div class="step-buttons"> |
26 | <a i18n class="skip-step underline-orange" routerLink="/login"> | 29 | <a i18n class="skip-step underline-orange" routerLink="/login"> |
27 | <strong>I already have an account</strong>, I log in | 30 | <strong>I already have an account</strong>, I log in |
28 | </a> | 31 | </a> |
29 | 32 | ||
30 | <button i18n cdkStepperNext>Create an account</button> | 33 | <button cdkStepperNext> |
34 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> | ||
35 | </button> | ||
31 | </div> | 36 | </div> |
32 | </cdk-step> | 37 | </cdk-step> |
33 | 38 | ||
@@ -44,8 +49,8 @@ | |||
44 | ></my-instance-about-accordion> | 49 | ></my-instance-about-accordion> |
45 | 50 | ||
46 | <my-register-step-terms | 51 | <my-register-step-terms |
47 | [hasCodeOfConduct]="!!aboutHtml.codeOfConduct" | 52 | [hasCodeOfConduct]="!!aboutHtml.codeOfConduct" [minimumAge]="minimumAge" [instanceName]="instanceName" |
48 | [minimumAge]="minimumAge" | 53 | [requiresApproval]="requiresApproval" |
49 | (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()" | 54 | (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()" |
50 | ></my-register-step-terms> | 55 | ></my-register-step-terms> |
51 | 56 | ||
@@ -94,14 +99,15 @@ | |||
94 | <div class="skip-step-description" i18n>You will be able to create a channel later</div> | 99 | <div class="skip-step-description" i18n>You will be able to create a channel later</div> |
95 | </div> | 100 | </div> |
96 | 101 | ||
97 | <button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()" i18n> | 102 | <button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()"> |
98 | Create my account | 103 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> |
99 | </button> | 104 | </button> |
100 | </div> | 105 | </div> |
101 | </cdk-step> | 106 | </cdk-step> |
102 | 107 | ||
103 | <cdk-step #lastStep i18n-label label="Done!" [editable]="false"> | 108 | <cdk-step #lastStep i18n-label label="Done!" [editable]="false"> |
104 | <div *ngIf="!signupSuccess && !signupError" class="done-loader"> | 109 | <!-- Account creation can be a little bit long so display a loader --> |
110 | <div *ngIf="!requiresApproval && !signupSuccess && !signupError" class="done-loader"> | ||
105 | <my-loader [loading]="true"></my-loader> | 111 | <my-loader [loading]="true"></my-loader> |
106 | 112 | ||
107 | <div i18n>PeerTube is creating your account...</div> | 113 | <div i18n>PeerTube is creating your account...</div> |
@@ -109,7 +115,10 @@ | |||
109 | 115 | ||
110 | <div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div> | 116 | <div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div> |
111 | 117 | ||
112 | <my-signup-success *ngIf="signupSuccess" [requiresEmailVerification]="requiresEmailVerification"></my-signup-success> | 118 | <my-signup-success-before-email |
119 | *ngIf="signupSuccess" | ||
120 | [requiresEmailVerification]="requiresEmailVerification" [requiresApproval]="requiresApproval" [instanceName]="instanceName" | ||
121 | ></my-signup-success-before-email> | ||
113 | 122 | ||
114 | <div *ngIf="signupError" class="steps-button"> | 123 | <div *ngIf="signupError" class="steps-button"> |
115 | <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button> | 124 | <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button> |
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts index 958770ebf..9259d902c 100644 --- a/client/src/app/+signup/+register/register.component.ts +++ b/client/src/app/+signup/+register/register.component.ts | |||
@@ -5,10 +5,10 @@ import { ActivatedRoute } from '@angular/router' | |||
5 | import { AuthService } from '@app/core' | 5 | import { AuthService } from '@app/core' |
6 | import { HooksService } from '@app/core/plugins/hooks.service' | 6 | import { HooksService } from '@app/core/plugins/hooks.service' |
7 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | 7 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' |
8 | import { UserSignupService } from '@app/shared/shared-users' | ||
9 | import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap' | 8 | import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap' |
10 | import { UserRegister } from '@shared/models' | 9 | import { UserRegister } from '@shared/models' |
11 | import { ServerConfig } from '@shared/models/server' | 10 | import { ServerConfig } from '@shared/models/server' |
11 | import { SignupService } from '../shared/signup.service' | ||
12 | 12 | ||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-register', | 14 | selector: 'my-register', |
@@ -53,7 +53,7 @@ export class RegisterComponent implements OnInit { | |||
53 | constructor ( | 53 | constructor ( |
54 | private route: ActivatedRoute, | 54 | private route: ActivatedRoute, |
55 | private authService: AuthService, | 55 | private authService: AuthService, |
56 | private userSignupService: UserSignupService, | 56 | private signupService: SignupService, |
57 | private hooks: HooksService | 57 | private hooks: HooksService |
58 | ) { } | 58 | ) { } |
59 | 59 | ||
@@ -61,6 +61,10 @@ export class RegisterComponent implements OnInit { | |||
61 | return this.serverConfig.signup.requiresEmailVerification | 61 | return this.serverConfig.signup.requiresEmailVerification |
62 | } | 62 | } |
63 | 63 | ||
64 | get requiresApproval () { | ||
65 | return this.serverConfig.signup.requiresApproval | ||
66 | } | ||
67 | |||
64 | get minimumAge () { | 68 | get minimumAge () { |
65 | return this.serverConfig.signup.minimumAge | 69 | return this.serverConfig.signup.minimumAge |
66 | } | 70 | } |
@@ -132,42 +136,49 @@ export class RegisterComponent implements OnInit { | |||
132 | skipChannelCreation () { | 136 | skipChannelCreation () { |
133 | this.formStepChannel.reset() | 137 | this.formStepChannel.reset() |
134 | this.lastStep.select() | 138 | this.lastStep.select() |
139 | |||
135 | this.signup() | 140 | this.signup() |
136 | } | 141 | } |
137 | 142 | ||
138 | async signup () { | 143 | async signup () { |
139 | this.signupError = undefined | 144 | this.signupError = undefined |
140 | 145 | ||
141 | const body: UserRegister = await this.hooks.wrapObject( | 146 | const termsForm = this.formStepTerms.value |
147 | const userForm = this.formStepUser.value | ||
148 | const channelForm = this.formStepChannel?.value | ||
149 | |||
150 | const channel = this.formStepChannel?.value?.name | ||
151 | ? { name: channelForm?.name, displayName: channelForm?.displayName } | ||
152 | : undefined | ||
153 | |||
154 | const body = await this.hooks.wrapObject( | ||
142 | { | 155 | { |
143 | ...this.formStepUser.value, | 156 | username: userForm.username, |
157 | password: userForm.password, | ||
158 | email: userForm.email, | ||
159 | displayName: userForm.displayName, | ||
160 | |||
161 | registrationReason: termsForm.registrationReason, | ||
144 | 162 | ||
145 | channel: this.formStepChannel?.value?.name | 163 | channel |
146 | ? this.formStepChannel.value | ||
147 | : undefined | ||
148 | }, | 164 | }, |
149 | 'signup', | 165 | 'signup', |
150 | 'filter:api.signup.registration.create.params' | 166 | 'filter:api.signup.registration.create.params' |
151 | ) | 167 | ) |
152 | 168 | ||
153 | this.userSignupService.signup(body).subscribe({ | 169 | const obs = this.requiresApproval |
170 | ? this.signupService.requestSignup(body) | ||
171 | : this.signupService.directSignup(body) | ||
172 | |||
173 | obs.subscribe({ | ||
154 | next: () => { | 174 | next: () => { |
155 | if (this.requiresEmailVerification) { | 175 | if (this.requiresEmailVerification || this.requiresApproval) { |
156 | this.signupSuccess = true | 176 | this.signupSuccess = true |
157 | return | 177 | return |
158 | } | 178 | } |
159 | 179 | ||
160 | // Auto login | 180 | // Auto login |
161 | this.authService.login({ username: body.username, password: body.password }) | 181 | this.autoLogin(body) |
162 | .subscribe({ | ||
163 | next: () => { | ||
164 | this.signupSuccess = true | ||
165 | }, | ||
166 | |||
167 | error: err => { | ||
168 | this.signupError = err.message | ||
169 | } | ||
170 | }) | ||
171 | }, | 182 | }, |
172 | 183 | ||
173 | error: err => { | 184 | error: err => { |
@@ -175,4 +186,17 @@ export class RegisterComponent implements OnInit { | |||
175 | } | 186 | } |
176 | }) | 187 | }) |
177 | } | 188 | } |
189 | |||
190 | private autoLogin (body: UserRegister) { | ||
191 | this.authService.login({ username: body.username, password: body.password }) | ||
192 | .subscribe({ | ||
193 | next: () => { | ||
194 | this.signupSuccess = true | ||
195 | }, | ||
196 | |||
197 | error: err => { | ||
198 | this.signupError = err.message | ||
199 | } | ||
200 | }) | ||
201 | } | ||
178 | } | 202 | } |
diff --git a/client/src/app/+signup/+register/shared/index.ts b/client/src/app/+signup/+register/shared/index.ts new file mode 100644 index 000000000..affb54bf4 --- /dev/null +++ b/client/src/app/+signup/+register/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './register-validators' | |||
diff --git a/client/src/app/+signup/+register/shared/register-validators.ts b/client/src/app/+signup/+register/shared/register-validators.ts new file mode 100644 index 000000000..f14803b68 --- /dev/null +++ b/client/src/app/+signup/+register/shared/register-validators.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | import { Validators } from '@angular/forms' | ||
2 | import { BuildFormValidator } from '@app/shared/form-validators' | ||
3 | |||
4 | export const REGISTER_TERMS_VALIDATOR: BuildFormValidator = { | ||
5 | VALIDATORS: [ Validators.requiredTrue ], | ||
6 | MESSAGES: { | ||
7 | required: $localize`You must agree with the instance terms in order to register on it.` | ||
8 | } | ||
9 | } | ||
10 | |||
11 | export const REGISTER_REASON_VALIDATOR: BuildFormValidator = { | ||
12 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], | ||
13 | MESSAGES: { | ||
14 | required: $localize`Registration reason is required.`, | ||
15 | minlength: $localize`Registration reason must be at least 2 characters long.`, | ||
16 | maxlength: $localize`Registration reason cannot be more than 3000 characters long.` | ||
17 | } | ||
18 | } | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.html b/client/src/app/+signup/+register/steps/register-step-about.component.html index 769fe3127..580e8a92c 100644 --- a/client/src/app/+signup/+register/steps/register-step-about.component.html +++ b/client/src/app/+signup/+register/steps/register-step-about.component.html | |||
@@ -13,6 +13,10 @@ | |||
13 | <li i18n>Have access to your <strong>watch history</strong></li> | 13 | <li i18n>Have access to your <strong>watch history</strong></li> |
14 | <li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li> | 14 | <li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li> |
15 | </ul> | 15 | </ul> |
16 | |||
17 | <p *ngIf="requiresApproval" i18n> | ||
18 | Moderators of {{ instanceName }} will have to approve your registration request once you have finished to fill the form. | ||
19 | </p> | ||
16 | </div> | 20 | </div> |
17 | 21 | ||
18 | <div> | 22 | <div> |
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.ts b/client/src/app/+signup/+register/steps/register-step-about.component.ts index 9a0941016..b176ffa59 100644 --- a/client/src/app/+signup/+register/steps/register-step-about.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-about.component.ts | |||
@@ -7,6 +7,7 @@ import { ServerService } from '@app/core' | |||
7 | styleUrls: [ './register-step-about.component.scss' ] | 7 | styleUrls: [ './register-step-about.component.scss' ] |
8 | }) | 8 | }) |
9 | export class RegisterStepAboutComponent { | 9 | export class RegisterStepAboutComponent { |
10 | @Input() requiresApproval: boolean | ||
10 | @Input() videoUploadDisabled: boolean | 11 | @Input() videoUploadDisabled: boolean |
11 | 12 | ||
12 | constructor (private serverService: ServerService) { | 13 | constructor (private serverService: ServerService) { |
diff --git a/client/src/app/+signup/+register/steps/register-step-channel.component.ts b/client/src/app/+signup/+register/steps/register-step-channel.component.ts index df92c5145..478ca0177 100644 --- a/client/src/app/+signup/+register/steps/register-step-channel.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-channel.component.ts | |||
@@ -2,9 +2,9 @@ import { concat, of } from 'rxjs' | |||
2 | import { pairwise } from 'rxjs/operators' | 2 | import { pairwise } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
4 | import { FormGroup } from '@angular/forms' | 4 | import { FormGroup } from '@angular/forms' |
5 | import { SignupService } from '@app/+signup/shared/signup.service' | ||
5 | import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' | 6 | import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' |
6 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 7 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
7 | import { UserSignupService } from '@app/shared/shared-users' | ||
8 | 8 | ||
9 | @Component({ | 9 | @Component({ |
10 | selector: 'my-register-step-channel', | 10 | selector: 'my-register-step-channel', |
@@ -20,7 +20,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit | |||
20 | 20 | ||
21 | constructor ( | 21 | constructor ( |
22 | protected formReactiveService: FormReactiveService, | 22 | protected formReactiveService: FormReactiveService, |
23 | private userSignupService: UserSignupService | 23 | private signupService: SignupService |
24 | ) { | 24 | ) { |
25 | super() | 25 | super() |
26 | } | 26 | } |
@@ -51,7 +51,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit | |||
51 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { | 51 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { |
52 | const name = this.form.value['name'] || '' | 52 | const name = this.form.value['name'] || '' |
53 | 53 | ||
54 | const newName = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, name) | 54 | const newName = this.signupService.getNewUsername(oldDisplayName, newDisplayName, name) |
55 | this.form.patchValue({ name: newName }) | 55 | this.form.patchValue({ name: newName }) |
56 | } | 56 | } |
57 | } | 57 | } |
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.html b/client/src/app/+signup/+register/steps/register-step-terms.component.html index cbfb32518..1d753a3f2 100644 --- a/client/src/app/+signup/+register/steps/register-step-terms.component.html +++ b/client/src/app/+signup/+register/steps/register-step-terms.component.html | |||
@@ -1,4 +1,16 @@ | |||
1 | <form role="form" [formGroup]="form"> | 1 | <form role="form" [formGroup]="form"> |
2 | |||
3 | <div *ngIf="requiresApproval" class="form-group"> | ||
4 | <label i18n for="registrationReason">Why do you want to join {{ instanceName }}?</label> | ||
5 | |||
6 | <textarea | ||
7 | id="registrationReason" formControlName="registrationReason" class="form-control" rows="4" | ||
8 | [ngClass]="{ 'input-error': formErrors['registrationReason'] }" | ||
9 | ></textarea> | ||
10 | |||
11 | <div *ngIf="formErrors.registrationReason" class="form-error">{{ formErrors.registrationReason }}</div> | ||
12 | </div> | ||
13 | |||
2 | <div class="form-group"> | 14 | <div class="form-group"> |
3 | <my-peertube-checkbox inputName="terms" formControlName="terms"> | 15 | <my-peertube-checkbox inputName="terms" formControlName="terms"> |
4 | <ng-template ptTemplate="label"> | 16 | <ng-template ptTemplate="label"> |
@@ -6,7 +18,7 @@ | |||
6 | I am at least {{ minimumAge }} years old and agree | 18 | I am at least {{ minimumAge }} years old and agree |
7 | to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a> | 19 | to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a> |
8 | <ng-container *ngIf="hasCodeOfConduct"> and to the <a class="link-orange" (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> | 20 | <ng-container *ngIf="hasCodeOfConduct"> and to the <a class="link-orange" (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> |
9 | of this instance | 21 | of {{ instanceName }} |
10 | </ng-container> | 22 | </ng-container> |
11 | </ng-template> | 23 | </ng-template> |
12 | </my-peertube-checkbox> | 24 | </my-peertube-checkbox> |
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.ts b/client/src/app/+signup/+register/steps/register-step-terms.component.ts index 2df963b30..1b1fb49ee 100644 --- a/client/src/app/+signup/+register/steps/register-step-terms.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-terms.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
2 | import { FormGroup } from '@angular/forms' | 2 | import { FormGroup } from '@angular/forms' |
3 | import { USER_TERMS_VALIDATOR } from '@app/shared/form-validators/user-validators' | ||
4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 3 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
4 | import { REGISTER_REASON_VALIDATOR, REGISTER_TERMS_VALIDATOR } from '../shared' | ||
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'my-register-step-terms', | 7 | selector: 'my-register-step-terms', |
@@ -10,7 +10,9 @@ import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | |||
10 | }) | 10 | }) |
11 | export class RegisterStepTermsComponent extends FormReactive implements OnInit { | 11 | export class RegisterStepTermsComponent extends FormReactive implements OnInit { |
12 | @Input() hasCodeOfConduct = false | 12 | @Input() hasCodeOfConduct = false |
13 | @Input() requiresApproval: boolean | ||
13 | @Input() minimumAge = 16 | 14 | @Input() minimumAge = 16 |
15 | @Input() instanceName: string | ||
14 | 16 | ||
15 | @Output() formBuilt = new EventEmitter<FormGroup>() | 17 | @Output() formBuilt = new EventEmitter<FormGroup>() |
16 | @Output() termsClick = new EventEmitter<void>() | 18 | @Output() termsClick = new EventEmitter<void>() |
@@ -28,7 +30,11 @@ export class RegisterStepTermsComponent extends FormReactive implements OnInit { | |||
28 | 30 | ||
29 | ngOnInit () { | 31 | ngOnInit () { |
30 | this.buildForm({ | 32 | this.buildForm({ |
31 | terms: USER_TERMS_VALIDATOR | 33 | terms: REGISTER_TERMS_VALIDATOR, |
34 | |||
35 | registrationReason: this.requiresApproval | ||
36 | ? REGISTER_REASON_VALIDATOR | ||
37 | : null | ||
32 | }) | 38 | }) |
33 | 39 | ||
34 | setTimeout(() => this.formBuilt.emit(this.form)) | 40 | setTimeout(() => this.formBuilt.emit(this.form)) |
diff --git a/client/src/app/+signup/+register/steps/register-step-user.component.ts b/client/src/app/+signup/+register/steps/register-step-user.component.ts index 822f8f5c5..0a5d2e437 100644 --- a/client/src/app/+signup/+register/steps/register-step-user.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-user.component.ts | |||
@@ -2,6 +2,7 @@ import { concat, of } from 'rxjs' | |||
2 | import { pairwise } from 'rxjs/operators' | 2 | import { pairwise } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
4 | import { FormGroup } from '@angular/forms' | 4 | import { FormGroup } from '@angular/forms' |
5 | import { SignupService } from '@app/+signup/shared/signup.service' | ||
5 | import { | 6 | import { |
6 | USER_DISPLAY_NAME_REQUIRED_VALIDATOR, | 7 | USER_DISPLAY_NAME_REQUIRED_VALIDATOR, |
7 | USER_EMAIL_VALIDATOR, | 8 | USER_EMAIL_VALIDATOR, |
@@ -9,7 +10,6 @@ import { | |||
9 | USER_USERNAME_VALIDATOR | 10 | USER_USERNAME_VALIDATOR |
10 | } from '@app/shared/form-validators/user-validators' | 11 | } from '@app/shared/form-validators/user-validators' |
11 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 12 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
12 | import { UserSignupService } from '@app/shared/shared-users' | ||
13 | 13 | ||
14 | @Component({ | 14 | @Component({ |
15 | selector: 'my-register-step-user', | 15 | selector: 'my-register-step-user', |
@@ -24,7 +24,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { | |||
24 | 24 | ||
25 | constructor ( | 25 | constructor ( |
26 | protected formReactiveService: FormReactiveService, | 26 | protected formReactiveService: FormReactiveService, |
27 | private userSignupService: UserSignupService | 27 | private signupService: SignupService |
28 | ) { | 28 | ) { |
29 | super() | 29 | super() |
30 | } | 30 | } |
@@ -57,7 +57,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { | |||
57 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { | 57 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { |
58 | const username = this.form.value['username'] || '' | 58 | const username = this.form.value['username'] || '' |
59 | 59 | ||
60 | const newUsername = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, username) | 60 | const newUsername = this.signupService.getNewUsername(oldDisplayName, newDisplayName, username) |
61 | this.form.patchValue({ username: newUsername }) | 61 | this.form.patchValue({ username: newUsername }) |
62 | } | 62 | } |
63 | } | 63 | } |
diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts index 06905f678..75b599e0e 100644 --- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { SignupService } from '@app/+signup/shared/signup.service' | ||
2 | import { Notifier, RedirectService, ServerService } from '@app/core' | 3 | import { Notifier, RedirectService, ServerService } from '@app/core' |
3 | import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' | 4 | import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' |
4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { UserSignupService } from '@app/shared/shared-users' | ||
6 | 6 | ||
7 | @Component({ | 7 | @Component({ |
8 | selector: 'my-verify-account-ask-send-email', | 8 | selector: 'my-verify-account-ask-send-email', |
@@ -15,7 +15,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements | |||
15 | 15 | ||
16 | constructor ( | 16 | constructor ( |
17 | protected formReactiveService: FormReactiveService, | 17 | protected formReactiveService: FormReactiveService, |
18 | private userSignupService: UserSignupService, | 18 | private signupService: SignupService, |
19 | private serverService: ServerService, | 19 | private serverService: ServerService, |
20 | private notifier: Notifier, | 20 | private notifier: Notifier, |
21 | private redirectService: RedirectService | 21 | private redirectService: RedirectService |
@@ -34,7 +34,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements | |||
34 | 34 | ||
35 | askSendVerifyEmail () { | 35 | askSendVerifyEmail () { |
36 | const email = this.form.value['verify-email-email'] | 36 | const email = this.form.value['verify-email-email'] |
37 | this.userSignupService.askSendVerifyEmail(email) | 37 | this.signupService.askSendVerifyEmail(email) |
38 | .subscribe({ | 38 | .subscribe({ |
39 | next: () => { | 39 | next: () => { |
40 | this.notifier.success($localize`An email with verification link will be sent to ${email}.`) | 40 | this.notifier.success($localize`An email with verification link will be sent to ${email}.`) |
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html index 122f3c28c..8c8b1098e 100644 --- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html | |||
@@ -1,14 +1,19 @@ | |||
1 | <div class="margin-content"> | 1 | <div *ngIf="loaded" class="margin-content"> |
2 | <h1 i18n class="title-page">Verify account email confirmation</h1> | 2 | <h1 i18n class="title-page">Verify email</h1> |
3 | 3 | ||
4 | <my-signup-success i18n *ngIf="!isPendingEmail && success" [requiresEmailVerification]="false"> | 4 | <my-signup-success-after-email |
5 | </my-signup-success> | 5 | *ngIf="displaySignupSuccess()" |
6 | [requiresApproval]="isRegistrationRequest() && requiresApproval" | ||
7 | > | ||
8 | </my-signup-success-after-email> | ||
6 | 9 | ||
7 | <div i18n class="alert alert-success" *ngIf="isPendingEmail && success">Email updated.</div> | 10 | <div i18n class="alert alert-success" *ngIf="!isRegistrationRequest() && isPendingEmail && success">Email updated.</div> |
8 | 11 | ||
9 | <div class="alert alert-danger" *ngIf="failed"> | 12 | <div class="alert alert-danger" *ngIf="failed"> |
10 | <span i18n>An error occurred.</span> | 13 | <span i18n>An error occurred.</span> |
11 | 14 | ||
12 | <a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email" [queryParams]="{ isPendingEmail: isPendingEmail }">Request new verification email</a> | 15 | <a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email"> |
16 | Request a new verification email | ||
17 | </a> | ||
13 | </div> | 18 | </div> |
14 | </div> | 19 | </div> |
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts index 88efce4a1..faf663391 100644 --- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ActivatedRoute } from '@angular/router' | 2 | import { ActivatedRoute } from '@angular/router' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { SignupService } from '@app/+signup/shared/signup.service' |
4 | import { UserSignupService } from '@app/shared/shared-users' | 4 | import { AuthService, Notifier, ServerService } from '@app/core' |
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'my-verify-account-email', | 7 | selector: 'my-verify-account-email', |
@@ -13,32 +13,82 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
13 | failed = false | 13 | failed = false |
14 | isPendingEmail = false | 14 | isPendingEmail = false |
15 | 15 | ||
16 | requiresApproval: boolean | ||
17 | loaded = false | ||
18 | |||
16 | private userId: number | 19 | private userId: number |
20 | private registrationId: number | ||
17 | private verificationString: string | 21 | private verificationString: string |
18 | 22 | ||
19 | constructor ( | 23 | constructor ( |
20 | private userSignupService: UserSignupService, | 24 | private signupService: SignupService, |
25 | private server: ServerService, | ||
21 | private authService: AuthService, | 26 | private authService: AuthService, |
22 | private notifier: Notifier, | 27 | private notifier: Notifier, |
23 | private route: ActivatedRoute | 28 | private route: ActivatedRoute |
24 | ) { | 29 | ) { |
25 | } | 30 | } |
26 | 31 | ||
32 | get instanceName () { | ||
33 | return this.server.getHTMLConfig().instance.name | ||
34 | } | ||
35 | |||
27 | ngOnInit () { | 36 | ngOnInit () { |
28 | const queryParams = this.route.snapshot.queryParams | 37 | const queryParams = this.route.snapshot.queryParams |
38 | |||
39 | this.server.getConfig().subscribe(config => { | ||
40 | this.requiresApproval = config.signup.requiresApproval | ||
41 | |||
42 | this.loaded = true | ||
43 | }) | ||
44 | |||
29 | this.userId = queryParams['userId'] | 45 | this.userId = queryParams['userId'] |
46 | this.registrationId = queryParams['registrationId'] | ||
47 | |||
30 | this.verificationString = queryParams['verificationString'] | 48 | this.verificationString = queryParams['verificationString'] |
49 | |||
31 | this.isPendingEmail = queryParams['isPendingEmail'] === 'true' | 50 | this.isPendingEmail = queryParams['isPendingEmail'] === 'true' |
32 | 51 | ||
33 | if (!this.userId || !this.verificationString) { | 52 | if (!this.verificationString) { |
34 | this.notifier.error($localize`Unable to find user id or verification string.`) | 53 | this.notifier.error($localize`Unable to find verification string in URL query.`) |
35 | } else { | 54 | return |
36 | this.verifyEmail() | 55 | } |
56 | |||
57 | if (!this.userId && !this.registrationId) { | ||
58 | this.notifier.error($localize`Unable to find user id or registration id in URL query.`) | ||
59 | return | ||
37 | } | 60 | } |
61 | |||
62 | this.verifyEmail() | ||
63 | } | ||
64 | |||
65 | isRegistrationRequest () { | ||
66 | return !!this.registrationId | ||
67 | } | ||
68 | |||
69 | displaySignupSuccess () { | ||
70 | if (!this.success) return false | ||
71 | if (!this.isRegistrationRequest() && this.isPendingEmail) return false | ||
72 | |||
73 | return true | ||
38 | } | 74 | } |
39 | 75 | ||
40 | verifyEmail () { | 76 | verifyEmail () { |
41 | this.userSignupService.verifyEmail(this.userId, this.verificationString, this.isPendingEmail) | 77 | if (this.isRegistrationRequest()) { |
78 | return this.verifyRegistrationEmail() | ||
79 | } | ||
80 | |||
81 | return this.verifyUserEmail() | ||
82 | } | ||
83 | |||
84 | private verifyUserEmail () { | ||
85 | const options = { | ||
86 | userId: this.userId, | ||
87 | verificationString: this.verificationString, | ||
88 | isPendingEmail: this.isPendingEmail | ||
89 | } | ||
90 | |||
91 | this.signupService.verifyUserEmail(options) | ||
42 | .subscribe({ | 92 | .subscribe({ |
43 | next: () => { | 93 | next: () => { |
44 | if (this.authService.isLoggedIn()) { | 94 | if (this.authService.isLoggedIn()) { |
@@ -55,4 +105,24 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
55 | } | 105 | } |
56 | }) | 106 | }) |
57 | } | 107 | } |
108 | |||
109 | private verifyRegistrationEmail () { | ||
110 | const options = { | ||
111 | registrationId: this.registrationId, | ||
112 | verificationString: this.verificationString | ||
113 | } | ||
114 | |||
115 | this.signupService.verifyRegistrationEmail(options) | ||
116 | .subscribe({ | ||
117 | next: () => { | ||
118 | this.success = true | ||
119 | }, | ||
120 | |||
121 | error: err => { | ||
122 | this.failed = true | ||
123 | |||
124 | this.notifier.error(err.message) | ||
125 | } | ||
126 | }) | ||
127 | } | ||
58 | } | 128 | } |
diff --git a/client/src/app/+signup/shared/shared-signup.module.ts b/client/src/app/+signup/shared/shared-signup.module.ts index 0aa08f3e2..0600f0af8 100644 --- a/client/src/app/+signup/shared/shared-signup.module.ts +++ b/client/src/app/+signup/shared/shared-signup.module.ts | |||
@@ -5,7 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main' | |||
5 | import { SharedUsersModule } from '@app/shared/shared-users' | 5 | import { SharedUsersModule } from '@app/shared/shared-users' |
6 | import { SignupMascotComponent } from './signup-mascot.component' | 6 | import { SignupMascotComponent } from './signup-mascot.component' |
7 | import { SignupStepTitleComponent } from './signup-step-title.component' | 7 | import { SignupStepTitleComponent } from './signup-step-title.component' |
8 | import { SignupSuccessComponent } from './signup-success.component' | 8 | import { SignupSuccessBeforeEmailComponent } from './signup-success-before-email.component' |
9 | import { SignupSuccessAfterEmailComponent } from './signup-success-after-email.component' | ||
10 | import { SignupService } from './signup.service' | ||
9 | 11 | ||
10 | @NgModule({ | 12 | @NgModule({ |
11 | imports: [ | 13 | imports: [ |
@@ -16,7 +18,8 @@ import { SignupSuccessComponent } from './signup-success.component' | |||
16 | ], | 18 | ], |
17 | 19 | ||
18 | declarations: [ | 20 | declarations: [ |
19 | SignupSuccessComponent, | 21 | SignupSuccessBeforeEmailComponent, |
22 | SignupSuccessAfterEmailComponent, | ||
20 | SignupStepTitleComponent, | 23 | SignupStepTitleComponent, |
21 | SignupMascotComponent | 24 | SignupMascotComponent |
22 | ], | 25 | ], |
@@ -26,12 +29,14 @@ import { SignupSuccessComponent } from './signup-success.component' | |||
26 | SharedFormModule, | 29 | SharedFormModule, |
27 | SharedGlobalIconModule, | 30 | SharedGlobalIconModule, |
28 | 31 | ||
29 | SignupSuccessComponent, | 32 | SignupSuccessBeforeEmailComponent, |
33 | SignupSuccessAfterEmailComponent, | ||
30 | SignupStepTitleComponent, | 34 | SignupStepTitleComponent, |
31 | SignupMascotComponent | 35 | SignupMascotComponent |
32 | ], | 36 | ], |
33 | 37 | ||
34 | providers: [ | 38 | providers: [ |
39 | SignupService | ||
35 | ] | 40 | ] |
36 | }) | 41 | }) |
37 | export class SharedSignupModule { } | 42 | export class SharedSignupModule { } |
diff --git a/client/src/app/+signup/shared/signup-success-after-email.component.html b/client/src/app/+signup/shared/signup-success-after-email.component.html new file mode 100644 index 000000000..1c3536ada --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-after-email.component.html | |||
@@ -0,0 +1,21 @@ | |||
1 | <my-signup-step-title mascotImageName="success"> | ||
2 | <strong i18n>Email verified!</strong> | ||
3 | </my-signup-step-title> | ||
4 | |||
5 | <div class="alert pt-alert-primary"> | ||
6 | <ng-container *ngIf="requiresApproval"> | ||
7 | <p i18n>Your email has been verified and your account request has been sent!</p> | ||
8 | |||
9 | <p i18n> | ||
10 | A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected. | ||
11 | </p> | ||
12 | </ng-container> | ||
13 | |||
14 | <ng-container *ngIf="!requiresApproval"> | ||
15 | <p i18n>Your email has been verified and your account has been created!</p> | ||
16 | |||
17 | <p i18n> | ||
18 | If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
19 | </p> | ||
20 | </ng-container> | ||
21 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success-after-email.component.ts b/client/src/app/+signup/shared/signup-success-after-email.component.ts new file mode 100644 index 000000000..3d72fdae9 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-after-email.component.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-success-after-email', | ||
5 | templateUrl: './signup-success-after-email.component.html', | ||
6 | styleUrls: [ './signup-success.component.scss' ] | ||
7 | }) | ||
8 | export class SignupSuccessAfterEmailComponent { | ||
9 | @Input() requiresApproval: boolean | ||
10 | } | ||
diff --git a/client/src/app/+signup/shared/signup-success-before-email.component.html b/client/src/app/+signup/shared/signup-success-before-email.component.html new file mode 100644 index 000000000..b9668ee82 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-before-email.component.html | |||
@@ -0,0 +1,35 @@ | |||
1 | <my-signup-step-title mascotImageName="success"> | ||
2 | <ng-container *ngIf="requiresApproval"> | ||
3 | <strong i18n>Account request sent</strong> | ||
4 | </ng-container> | ||
5 | |||
6 | <ng-container *ngIf="!requiresApproval" i18n> | ||
7 | <strong>Welcome</strong> | ||
8 | <div>on {{ instanceName }}</div> | ||
9 | </ng-container> | ||
10 | </my-signup-step-title> | ||
11 | |||
12 | <div class="alert pt-alert-primary"> | ||
13 | <p *ngIf="requiresApproval" i18n>Your account request has been sent!</p> | ||
14 | <p *ngIf="!requiresApproval" i18n>Your account has been created!</p> | ||
15 | |||
16 | <ng-container *ngIf="requiresEmailVerification"> | ||
17 | <p i18n *ngIf="requiresApproval"> | ||
18 | <strong>Check your emails</strong> to validate your account and complete your registration request. | ||
19 | </p> | ||
20 | |||
21 | <p i18n *ngIf="!requiresApproval"> | ||
22 | <strong>Check your emails</strong> to validate your account and complete your registration. | ||
23 | </p> | ||
24 | </ng-container> | ||
25 | |||
26 | <ng-container *ngIf="!requiresEmailVerification"> | ||
27 | <p i18n *ngIf="requiresApproval"> | ||
28 | A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected. | ||
29 | </p> | ||
30 | |||
31 | <p *ngIf="!requiresApproval" i18n> | ||
32 | If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
33 | </p> | ||
34 | </ng-container> | ||
35 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success-before-email.component.ts b/client/src/app/+signup/shared/signup-success-before-email.component.ts new file mode 100644 index 000000000..d72462340 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-before-email.component.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-success-before-email', | ||
5 | templateUrl: './signup-success-before-email.component.html', | ||
6 | styleUrls: [ './signup-success.component.scss' ] | ||
7 | }) | ||
8 | export class SignupSuccessBeforeEmailComponent { | ||
9 | @Input() requiresApproval: boolean | ||
10 | @Input() requiresEmailVerification: boolean | ||
11 | @Input() instanceName: string | ||
12 | } | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.html b/client/src/app/+signup/shared/signup-success.component.html deleted file mode 100644 index c14889c72..000000000 --- a/client/src/app/+signup/shared/signup-success.component.html +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | <my-signup-step-title mascotImageName="success" i18n> | ||
2 | <strong>Welcome</strong> | ||
3 | <div>on {{ instanceName }}</div> | ||
4 | </my-signup-step-title> | ||
5 | |||
6 | <div class="alert pt-alert-primary"> | ||
7 | <p i18n>Your account has been created!</p> | ||
8 | |||
9 | <p i18n *ngIf="requiresEmailVerification"> | ||
10 | <strong>Check your emails</strong> to validate your account and complete your inscription. | ||
11 | </p> | ||
12 | |||
13 | <ng-container *ngIf="!requiresEmailVerification"> | ||
14 | <p i18n> | ||
15 | If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
16 | </p> | ||
17 | |||
18 | <p i18n> | ||
19 | To help moderators and other users to know <strong>who you are</strong>, don't forget to <a class="link-orange" routerLink="/my-account/settings">set up your account profile</a> by adding an <strong>avatar</strong> and a <strong>description</strong>. | ||
20 | </p> | ||
21 | </ng-container> | ||
22 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.ts b/client/src/app/+signup/shared/signup-success.component.ts deleted file mode 100644 index a03f3819d..000000000 --- a/client/src/app/+signup/shared/signup-success.component.ts +++ /dev/null | |||
@@ -1,19 +0,0 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { ServerService } from '@app/core' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-signup-success', | ||
6 | templateUrl: './signup-success.component.html', | ||
7 | styleUrls: [ './signup-success.component.scss' ] | ||
8 | }) | ||
9 | export class SignupSuccessComponent { | ||
10 | @Input() requiresEmailVerification: boolean | ||
11 | |||
12 | constructor (private serverService: ServerService) { | ||
13 | |||
14 | } | ||
15 | |||
16 | get instanceName () { | ||
17 | return this.serverService.getHTMLConfig().instance.name | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/shared/shared-users/user-signup.service.ts b/client/src/app/+signup/shared/signup.service.ts index 46fe34af1..f647298be 100644 --- a/client/src/app/shared/shared-users/user-signup.service.ts +++ b/client/src/app/+signup/shared/signup.service.ts | |||
@@ -2,17 +2,18 @@ import { catchError, tap } from 'rxjs/operators' | |||
2 | import { HttpClient } from '@angular/common/http' | 2 | import { HttpClient } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { RestExtractor, UserService } from '@app/core' | 4 | import { RestExtractor, UserService } from '@app/core' |
5 | import { UserRegister } from '@shared/models' | 5 | import { UserRegister, UserRegistrationRequest } from '@shared/models' |
6 | 6 | ||
7 | @Injectable() | 7 | @Injectable() |
8 | export class UserSignupService { | 8 | export class SignupService { |
9 | |||
9 | constructor ( | 10 | constructor ( |
10 | private authHttp: HttpClient, | 11 | private authHttp: HttpClient, |
11 | private restExtractor: RestExtractor, | 12 | private restExtractor: RestExtractor, |
12 | private userService: UserService | 13 | private userService: UserService |
13 | ) { } | 14 | ) { } |
14 | 15 | ||
15 | signup (userCreate: UserRegister) { | 16 | directSignup (userCreate: UserRegister) { |
16 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) | 17 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) |
17 | .pipe( | 18 | .pipe( |
18 | tap(() => this.userService.setSignupInThisSession(true)), | 19 | tap(() => this.userService.setSignupInThisSession(true)), |
@@ -20,8 +21,21 @@ export class UserSignupService { | |||
20 | ) | 21 | ) |
21 | } | 22 | } |
22 | 23 | ||
23 | verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) { | 24 | requestSignup (userCreate: UserRegistrationRequest) { |
24 | const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` | 25 | return this.authHttp.post(UserService.BASE_USERS_URL + 'registrations/request', userCreate) |
26 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
27 | } | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | verifyUserEmail (options: { | ||
32 | userId: number | ||
33 | verificationString: string | ||
34 | isPendingEmail: boolean | ||
35 | }) { | ||
36 | const { userId, verificationString, isPendingEmail } = options | ||
37 | |||
38 | const url = `${UserService.BASE_USERS_URL}${userId}/verify-email` | ||
25 | const body = { | 39 | const body = { |
26 | verificationString, | 40 | verificationString, |
27 | isPendingEmail | 41 | isPendingEmail |
@@ -31,13 +45,28 @@ export class UserSignupService { | |||
31 | .pipe(catchError(res => this.restExtractor.handleError(res))) | 45 | .pipe(catchError(res => this.restExtractor.handleError(res))) |
32 | } | 46 | } |
33 | 47 | ||
48 | verifyRegistrationEmail (options: { | ||
49 | registrationId: number | ||
50 | verificationString: string | ||
51 | }) { | ||
52 | const { registrationId, verificationString } = options | ||
53 | |||
54 | const url = `${UserService.BASE_USERS_URL}registrations/${registrationId}/verify-email` | ||
55 | const body = { verificationString } | ||
56 | |||
57 | return this.authHttp.post(url, body) | ||
58 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
59 | } | ||
60 | |||
34 | askSendVerifyEmail (email: string) { | 61 | askSendVerifyEmail (email: string) { |
35 | const url = UserService.BASE_USERS_URL + '/ask-send-verify-email' | 62 | const url = UserService.BASE_USERS_URL + 'ask-send-verify-email' |
36 | 63 | ||
37 | return this.authHttp.post(url, { email }) | 64 | return this.authHttp.post(url, { email }) |
38 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 65 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
39 | } | 66 | } |
40 | 67 | ||
68 | // --------------------------------------------------------------------------- | ||
69 | |||
41 | getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { | 70 | getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { |
42 | // Don't update display name, the user seems to have changed it | 71 | // Don't update display name, the user seems to have changed it |
43 | if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername | 72 | if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 94853423b..84548de97 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -133,8 +133,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
133 | this.loadRouteParams() | 133 | this.loadRouteParams() |
134 | this.loadRouteQuery() | 134 | this.loadRouteQuery() |
135 | 135 | ||
136 | this.initHotkeys() | ||
137 | |||
138 | this.theaterEnabled = getStoredTheater() | 136 | this.theaterEnabled = getStoredTheater() |
139 | 137 | ||
140 | this.hooks.runAction('action:video-watch.init', 'video-watch') | 138 | this.hooks.runAction('action:video-watch.init', 'video-watch') |
@@ -295,6 +293,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
295 | subtitle: queryParams.subtitle, | 293 | subtitle: queryParams.subtitle, |
296 | 294 | ||
297 | playerMode: queryParams.mode, | 295 | playerMode: queryParams.mode, |
296 | playbackRate: queryParams.playbackRate, | ||
298 | peertubeLink: false | 297 | peertubeLink: false |
299 | } | 298 | } |
300 | 299 | ||
@@ -406,6 +405,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
406 | if (res === false) return this.location.back() | 405 | if (res === false) return this.location.back() |
407 | } | 406 | } |
408 | 407 | ||
408 | this.buildHotkeysHelp(video) | ||
409 | |||
409 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) | 410 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) |
410 | .catch(err => logger.error('Cannot build the player', err)) | 411 | .catch(err => logger.error('Cannot build the player', err)) |
411 | 412 | ||
@@ -657,6 +658,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
657 | muted: urlOptions.muted, | 658 | muted: urlOptions.muted, |
658 | loop: urlOptions.loop, | 659 | loop: urlOptions.loop, |
659 | subtitle: urlOptions.subtitle, | 660 | subtitle: urlOptions.subtitle, |
661 | playbackRate: urlOptions.playbackRate, | ||
660 | 662 | ||
661 | peertubeLink: urlOptions.peertubeLink, | 663 | peertubeLink: urlOptions.peertubeLink, |
662 | 664 | ||
@@ -785,33 +787,43 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
785 | this.video.viewers = newViewers | 787 | this.video.viewers = newViewers |
786 | } | 788 | } |
787 | 789 | ||
788 | private initHotkeys () { | 790 | private buildHotkeysHelp (video: Video) { |
791 | if (this.hotkeys.length !== 0) { | ||
792 | this.hotkeysService.remove(this.hotkeys) | ||
793 | } | ||
794 | |||
789 | this.hotkeys = [ | 795 | this.hotkeys = [ |
790 | // These hotkeys are managed by the player | 796 | // These hotkeys are managed by the player |
791 | new Hotkey('f', e => e, undefined, $localize`Enter/exit fullscreen`), | 797 | new Hotkey('f', e => e, undefined, $localize`Enter/exit fullscreen`), |
792 | new Hotkey('space', e => e, undefined, $localize`Play/Pause the video`), | 798 | new Hotkey('space', e => e, undefined, $localize`Play/Pause the video`), |
793 | new Hotkey('m', e => e, undefined, $localize`Mute/unmute the video`), | 799 | new Hotkey('m', e => e, undefined, $localize`Mute/unmute the video`), |
794 | 800 | ||
795 | new Hotkey('0-9', e => e, undefined, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`), | ||
796 | |||
797 | new Hotkey('up', e => e, undefined, $localize`Increase the volume`), | 801 | new Hotkey('up', e => e, undefined, $localize`Increase the volume`), |
798 | new Hotkey('down', e => e, undefined, $localize`Decrease the volume`), | 802 | new Hotkey('down', e => e, undefined, $localize`Decrease the volume`), |
799 | 803 | ||
800 | new Hotkey('right', e => e, undefined, $localize`Seek the video forward`), | ||
801 | new Hotkey('left', e => e, undefined, $localize`Seek the video backward`), | ||
802 | |||
803 | new Hotkey('>', e => e, undefined, $localize`Increase playback rate`), | ||
804 | new Hotkey('<', e => e, undefined, $localize`Decrease playback rate`), | ||
805 | |||
806 | new Hotkey(',', e => e, undefined, $localize`Navigate in the video to the previous frame`), | ||
807 | new Hotkey('.', e => e, undefined, $localize`Navigate in the video to the next frame`), | ||
808 | |||
809 | new Hotkey('t', e => { | 804 | new Hotkey('t', e => { |
810 | this.theaterEnabled = !this.theaterEnabled | 805 | this.theaterEnabled = !this.theaterEnabled |
811 | return false | 806 | return false |
812 | }, undefined, $localize`Toggle theater mode`) | 807 | }, undefined, $localize`Toggle theater mode`) |
813 | ] | 808 | ] |
814 | 809 | ||
810 | if (!video.isLive) { | ||
811 | this.hotkeys = this.hotkeys.concat([ | ||
812 | // These hotkeys are also managed by the player but only for VOD | ||
813 | |||
814 | new Hotkey('0-9', e => e, undefined, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`), | ||
815 | |||
816 | new Hotkey('right', e => e, undefined, $localize`Seek the video forward`), | ||
817 | new Hotkey('left', e => e, undefined, $localize`Seek the video backward`), | ||
818 | |||
819 | new Hotkey('>', e => e, undefined, $localize`Increase playback rate`), | ||
820 | new Hotkey('<', e => e, undefined, $localize`Decrease playback rate`), | ||
821 | |||
822 | new Hotkey(',', e => e, undefined, $localize`Navigate in the video to the previous frame`), | ||
823 | new Hotkey('.', e => e, undefined, $localize`Navigate in the video to the next frame`) | ||
824 | ]) | ||
825 | } | ||
826 | |||
815 | if (this.isUserLoggedIn()) { | 827 | if (this.isUserLoggedIn()) { |
816 | this.hotkeys = this.hotkeys.concat([ | 828 | this.hotkeys = this.hotkeys.concat([ |
817 | new Hotkey('shift+s', () => { | 829 | new Hotkey('shift+s', () => { |
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.ts b/client/src/app/+videos/video-list/videos-list-common-page.component.ts index c8fa8ef30..bafe30fd7 100644 --- a/client/src/app/+videos/video-list/videos-list-common-page.component.ts +++ b/client/src/app/+videos/video-list/videos-list-common-page.component.ts | |||
@@ -177,6 +177,9 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable | |||
177 | case 'best': | 177 | case 'best': |
178 | return '-hot' | 178 | return '-hot' |
179 | 179 | ||
180 | case 'name': | ||
181 | return 'name' | ||
182 | |||
180 | default: | 183 | default: |
181 | return '-' + algorithm as VideoSortField | 184 | return '-' + algorithm as VideoSortField |
182 | } | 185 | } |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 4de28e51e..ed7eabb76 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -5,10 +5,11 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular | |||
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { Router } from '@angular/router' | 6 | import { Router } from '@angular/router' |
7 | import { Notifier } from '@app/core/notification/notifier.service' | 7 | import { Notifier } from '@app/core/notification/notifier.service' |
8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' | 8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage, PluginsManager } from '@root-helpers/index' |
9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' | 9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' |
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | import { RestExtractor } from '../rest/rest-extractor.service' | 11 | import { RestExtractor } from '../rest/rest-extractor.service' |
12 | import { ServerService } from '../server' | ||
12 | import { AuthStatus } from './auth-status.model' | 13 | import { AuthStatus } from './auth-status.model' |
13 | import { AuthUser } from './auth-user.model' | 14 | import { AuthUser } from './auth-user.model' |
14 | 15 | ||
@@ -44,6 +45,7 @@ export class AuthService { | |||
44 | private refreshingTokenObservable: Observable<any> | 45 | private refreshingTokenObservable: Observable<any> |
45 | 46 | ||
46 | constructor ( | 47 | constructor ( |
48 | private serverService: ServerService, | ||
47 | private http: HttpClient, | 49 | private http: HttpClient, |
48 | private notifier: Notifier, | 50 | private notifier: Notifier, |
49 | private hotkeysService: HotkeysService, | 51 | private hotkeysService: HotkeysService, |
@@ -213,25 +215,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular | |||
213 | const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') | 215 | const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') |
214 | 216 | ||
215 | this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) | 217 | this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) |
216 | .pipe( | 218 | .pipe( |
217 | map(res => this.handleRefreshToken(res)), | 219 | map(res => this.handleRefreshToken(res)), |
218 | tap(() => { | 220 | tap(() => { |
219 | this.refreshingTokenObservable = null | 221 | this.refreshingTokenObservable = null |
220 | }), | 222 | }), |
221 | catchError(err => { | 223 | catchError(err => { |
222 | this.refreshingTokenObservable = null | 224 | this.refreshingTokenObservable = null |
223 | 225 | ||
224 | logger.error(err) | 226 | logger.error(err) |
225 | logger.info('Cannot refresh token -> logout...') | 227 | logger.info('Cannot refresh token -> logout...') |
226 | this.logout() | 228 | this.logout() |
227 | this.router.navigate([ '/login' ]) | 229 | |
228 | 230 | const externalLoginUrl = PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverService.getHTMLConfig()) | |
229 | return observableThrowError(() => ({ | 231 | if (externalLoginUrl) window.location.href = externalLoginUrl |
230 | error: $localize`You need to reconnect.` | 232 | else this.router.navigate([ '/login' ]) |
231 | })) | 233 | |
232 | }), | 234 | return observableThrowError(() => ({ |
233 | share() | 235 | error: $localize`You need to reconnect.` |
234 | ) | 236 | })) |
237 | }), | ||
238 | share() | ||
239 | ) | ||
235 | 240 | ||
236 | return this.refreshingTokenObservable | 241 | return this.refreshingTokenObservable |
237 | } | 242 | } |
diff --git a/client/src/app/core/renderer/linkifier.service.ts b/client/src/app/core/renderer/linkifier.service.ts index 78df92cc9..d99591d6c 100644 --- a/client/src/app/core/renderer/linkifier.service.ts +++ b/client/src/app/core/renderer/linkifier.service.ts | |||
@@ -15,7 +15,7 @@ export class LinkifierService { | |||
15 | }, | 15 | }, |
16 | formatHref: { | 16 | formatHref: { |
17 | mention: (href: string) => { | 17 | mention: (href: string) => { |
18 | return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + href.substr(1) | 18 | return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + href.substring(1) |
19 | } | 19 | } |
20 | } | 20 | } |
21 | } | 21 | } |
diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts index a5fd72862..dd23a1b01 100644 --- a/client/src/app/core/renderer/markdown.service.ts +++ b/client/src/app/core/renderer/markdown.service.ts | |||
@@ -64,8 +64,8 @@ export class MarkdownService { | |||
64 | 64 | ||
65 | textMarkdownToHTML (options: { | 65 | textMarkdownToHTML (options: { |
66 | markdown: string | 66 | markdown: string |
67 | withHtml?: boolean | 67 | withHtml?: boolean // default false |
68 | withEmoji?: boolean | 68 | withEmoji?: boolean // default false |
69 | }) { | 69 | }) { |
70 | const { markdown, withHtml = false, withEmoji = false } = options | 70 | const { markdown, withHtml = false, withEmoji = false } = options |
71 | 71 | ||
@@ -76,8 +76,8 @@ export class MarkdownService { | |||
76 | 76 | ||
77 | enhancedMarkdownToHTML (options: { | 77 | enhancedMarkdownToHTML (options: { |
78 | markdown: string | 78 | markdown: string |
79 | withHtml?: boolean | 79 | withHtml?: boolean // default false |
80 | withEmoji?: boolean | 80 | withEmoji?: boolean // default false |
81 | }) { | 81 | }) { |
82 | const { markdown, withHtml = false, withEmoji = false } = options | 82 | const { markdown, withHtml = false, withEmoji = false } = options |
83 | 83 | ||
@@ -99,6 +99,8 @@ export class MarkdownService { | |||
99 | return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags }) | 99 | return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags }) |
100 | } | 100 | } |
101 | 101 | ||
102 | // --------------------------------------------------------------------------- | ||
103 | |||
102 | processVideoTimestamps (videoShortUUID: string, html: string) { | 104 | processVideoTimestamps (videoShortUUID: string, html: string) { |
103 | return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) { | 105 | return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) { |
104 | const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0)) | 106 | const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0)) |
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts index de3f2bfff..daed7f178 100644 --- a/client/src/app/core/rest/rest-extractor.service.ts +++ b/client/src/app/core/rest/rest-extractor.service.ts | |||
@@ -87,7 +87,11 @@ export class RestExtractor { | |||
87 | 87 | ||
88 | if (err.status !== undefined) { | 88 | if (err.status !== undefined) { |
89 | const errorMessage = this.buildServerErrorMessage(err) | 89 | const errorMessage = this.buildServerErrorMessage(err) |
90 | logger.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) | 90 | |
91 | const message = `Backend returned code ${err.status}, errorMessage is: ${errorMessage}` | ||
92 | |||
93 | if (err.status === HttpStatusCode.NOT_FOUND_404) logger.clientError(message) | ||
94 | else logger.error(message) | ||
91 | 95 | ||
92 | return errorMessage | 96 | return errorMessage |
93 | } | 97 | } |
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts index ec5646b5d..707110d7f 100644 --- a/client/src/app/core/rest/rest-table.ts +++ b/client/src/app/core/rest/rest-table.ts | |||
@@ -7,7 +7,7 @@ import { RestPagination } from './rest-pagination' | |||
7 | 7 | ||
8 | const debugLogger = debug('peertube:tables:RestTable') | 8 | const debugLogger = debug('peertube:tables:RestTable') |
9 | 9 | ||
10 | export abstract class RestTable { | 10 | export abstract class RestTable <T = unknown> { |
11 | 11 | ||
12 | abstract totalRecords: number | 12 | abstract totalRecords: number |
13 | abstract sort: SortMeta | 13 | abstract sort: SortMeta |
@@ -17,6 +17,8 @@ export abstract class RestTable { | |||
17 | rowsPerPage = this.rowsPerPageOptions[0] | 17 | rowsPerPage = this.rowsPerPageOptions[0] |
18 | expandedRows = {} | 18 | expandedRows = {} |
19 | 19 | ||
20 | selectedRows: T[] = [] | ||
21 | |||
20 | search: string | 22 | search: string |
21 | 23 | ||
22 | protected route: ActivatedRoute | 24 | protected route: ActivatedRoute |
@@ -75,7 +77,17 @@ export abstract class RestTable { | |||
75 | this.reloadData() | 77 | this.reloadData() |
76 | } | 78 | } |
77 | 79 | ||
78 | protected abstract reloadData (): void | 80 | isInSelectionMode () { |
81 | return this.selectedRows.length !== 0 | ||
82 | } | ||
83 | |||
84 | protected abstract reloadDataInternal (): void | ||
85 | |||
86 | protected reloadData () { | ||
87 | this.selectedRows = [] | ||
88 | |||
89 | this.reloadDataInternal() | ||
90 | } | ||
79 | 91 | ||
80 | private getSortLocalStorageKey () { | 92 | private getSortLocalStorageKey () { |
81 | return 'rest-table-sort-' + this.getIdentifier() | 93 | return 'rest-table-sort-' + this.getIdentifier() |
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index c5d08ab75..15b1a3c4a 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html | |||
@@ -103,7 +103,9 @@ | |||
103 | <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a> | 103 | <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a> |
104 | <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a> | 104 | <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a> |
105 | 105 | ||
106 | <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> | 106 | <a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button"> |
107 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> | ||
108 | </a> | ||
107 | </div> | 109 | </div> |
108 | 110 | ||
109 | <ng-container *ngFor="let menuSection of menuSections" > | 111 | <ng-container *ngFor="let menuSection of menuSections" > |
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 63f01df92..fc6d74cff 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { HotkeysService } from 'angular2-hotkeys' | 1 | import { HotkeysService } from 'angular2-hotkeys' |
2 | import * as debug from 'debug' | 2 | import * as debug from 'debug' |
3 | import { switchMap } from 'rxjs/operators' | 3 | import { switchMap } from 'rxjs/operators' |
4 | import { environment } from 'src/environments/environment' | ||
4 | import { ViewportScroller } from '@angular/common' | 5 | import { ViewportScroller } from '@angular/common' |
5 | import { Component, OnInit, ViewChild } from '@angular/core' | 6 | import { Component, OnInit, ViewChild } from '@angular/core' |
6 | import { Router } from '@angular/router' | 7 | import { Router } from '@angular/router' |
@@ -91,6 +92,10 @@ export class MenuComponent implements OnInit { | |||
91 | return this.languageChooserModal.getCurrentLanguage() | 92 | return this.languageChooserModal.getCurrentLanguage() |
92 | } | 93 | } |
93 | 94 | ||
95 | get requiresApproval () { | ||
96 | return this.serverConfig.signup.requiresApproval | ||
97 | } | ||
98 | |||
94 | ngOnInit () { | 99 | ngOnInit () { |
95 | this.htmlServerConfig = this.serverService.getHTMLConfig() | 100 | this.htmlServerConfig = this.serverService.getHTMLConfig() |
96 | this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() | 101 | this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() |
@@ -131,12 +136,7 @@ export class MenuComponent implements OnInit { | |||
131 | } | 136 | } |
132 | 137 | ||
133 | getExternalLoginHref () { | 138 | getExternalLoginHref () { |
134 | if (!this.serverConfig || this.serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined | 139 | return PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverConfig) |
135 | |||
136 | const externalAuths = this.serverConfig.plugin.registeredExternalAuths | ||
137 | if (externalAuths.length !== 1) return undefined | ||
138 | |||
139 | return PluginsManager.getExternalAuthHref(externalAuths[0]) | ||
140 | } | 140 | } |
141 | 141 | ||
142 | isRegistrationAllowed () { | 142 | isRegistrationAllowed () { |
diff --git a/client/src/app/shared/form-validators/form-validator.model.ts b/client/src/app/shared/form-validators/form-validator.model.ts index 31c253b9b..1e4bba86b 100644 --- a/client/src/app/shared/form-validators/form-validator.model.ts +++ b/client/src/app/shared/form-validators/form-validator.model.ts | |||
@@ -12,5 +12,5 @@ export type BuildFormArgument = { | |||
12 | } | 12 | } |
13 | 13 | ||
14 | export type BuildFormDefaultValues = { | 14 | export type BuildFormDefaultValues = { |
15 | [ name: string ]: number | string | string[] | BuildFormDefaultValues | 15 | [ name: string ]: boolean | number | string | string[] | BuildFormDefaultValues |
16 | } | 16 | } |
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts index b93de75ea..ed6e0582e 100644 --- a/client/src/app/shared/form-validators/user-validators.ts +++ b/client/src/app/shared/form-validators/user-validators.ts | |||
@@ -136,13 +136,6 @@ export const USER_DESCRIPTION_VALIDATOR: BuildFormValidator = { | |||
136 | } | 136 | } |
137 | } | 137 | } |
138 | 138 | ||
139 | export const USER_TERMS_VALIDATOR: BuildFormValidator = { | ||
140 | VALIDATORS: [ Validators.requiredTrue ], | ||
141 | MESSAGES: { | ||
142 | required: $localize`You must agree with the instance terms in order to register on it.` | ||
143 | } | ||
144 | } | ||
145 | |||
146 | export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = { | 139 | export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = { |
147 | VALIDATORS: [ | 140 | VALIDATORS: [ |
148 | Validators.minLength(3), | 141 | Validators.minLength(3), |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html index 089be501d..2d3e26a25 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-details.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html | |||
@@ -8,7 +8,7 @@ | |||
8 | 8 | ||
9 | <span class="moderation-expanded-text"> | 9 | <span class="moderation-expanded-text"> |
10 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" | 10 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" |
11 | class="chip" | 11 | class="chip me-1" |
12 | > | 12 | > |
13 | <my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar> | 13 | <my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar> |
14 | <div> | 14 | <div> |
@@ -29,7 +29,7 @@ | |||
29 | <span class="moderation-expanded-label" i18n>Reportee</span> | 29 | <span class="moderation-expanded-label" i18n>Reportee</span> |
30 | <span class="moderation-expanded-text"> | 30 | <span class="moderation-expanded-text"> |
31 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" | 31 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" |
32 | class="chip" | 32 | class="chip me-1" |
33 | > | 33 | > |
34 | <my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar> | 34 | <my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar> |
35 | <div> | 35 | <div> |
@@ -63,7 +63,7 @@ | |||
63 | <div *ngIf="predefinedReasons" class="mt-2 d-flex"> | 63 | <div *ngIf="predefinedReasons" class="mt-2 d-flex"> |
64 | <span> | 64 | <span> |
65 | <a *ngFor="let reason of predefinedReasons" [routerLink]="[ '.' ]" | 65 | <a *ngFor="let reason of predefinedReasons" [routerLink]="[ '.' ]" |
66 | [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" | 66 | [queryParams]="{ 'search': 'tag:' + reason.id }" class="pt-badge badge-secondary" |
67 | > | 67 | > |
68 | <div>{{ reason.label }}</div> | 68 | <div>{{ reason.label }}</div> |
69 | </a> | 69 | </a> |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index 569a37b17..d8470e927 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts | |||
@@ -175,7 +175,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit { | |||
175 | return Actor.IS_LOCAL(abuse.reporterAccount.host) | 175 | return Actor.IS_LOCAL(abuse.reporterAccount.host) |
176 | } | 176 | } |
177 | 177 | ||
178 | protected reloadData () { | 178 | protected reloadDataInternal () { |
179 | debugLogger('Loading data.') | 179 | debugLogger('Loading data.') |
180 | 180 | ||
181 | const options = { | 181 | const options = { |
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts b/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts index 4e802b14d..b2ee2d8f2 100644 --- a/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts +++ b/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts | |||
@@ -6,9 +6,9 @@ import { CustomMarkupService } from './custom-markup.service' | |||
6 | templateUrl: './custom-markup-container.component.html' | 6 | templateUrl: './custom-markup-container.component.html' |
7 | }) | 7 | }) |
8 | export class CustomMarkupContainerComponent implements OnChanges { | 8 | export class CustomMarkupContainerComponent implements OnChanges { |
9 | @ViewChild('contentWrapper') contentWrapper: ElementRef<HTMLInputElement> | 9 | @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef<HTMLInputElement> |
10 | 10 | ||
11 | @Input() content: string | 11 | @Input() content: string | HTMLDivElement |
12 | 12 | ||
13 | displayed = false | 13 | displayed = false |
14 | 14 | ||
@@ -17,17 +17,23 @@ export class CustomMarkupContainerComponent implements OnChanges { | |||
17 | ) { } | 17 | ) { } |
18 | 18 | ||
19 | async ngOnChanges () { | 19 | async ngOnChanges () { |
20 | await this.buildElement() | 20 | await this.rebuild() |
21 | } | 21 | } |
22 | 22 | ||
23 | private async buildElement () { | 23 | private async rebuild () { |
24 | if (!this.content) return | 24 | if (this.content instanceof HTMLDivElement) { |
25 | return this.loadElement(this.content) | ||
26 | } | ||
25 | 27 | ||
26 | const { rootElement, componentsLoaded } = await this.customMarkupService.buildElement(this.content) | 28 | const { rootElement, componentsLoaded } = await this.customMarkupService.buildElement(this.content) |
27 | this.contentWrapper.nativeElement.appendChild(rootElement) | ||
28 | |||
29 | await componentsLoaded | 29 | await componentsLoaded |
30 | 30 | ||
31 | return this.loadElement(rootElement) | ||
32 | } | ||
33 | |||
34 | private loadElement (el: HTMLDivElement) { | ||
35 | this.contentWrapper.nativeElement.appendChild(el) | ||
36 | |||
31 | this.displayed = true | 37 | this.displayed = true |
32 | } | 38 | } |
33 | } | 39 | } |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts index 1af060548..264dd9577 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core' |
2 | import { VideoChannel } from '../../shared-main' | 2 | import { VideoChannel } from '../../shared-main' |
3 | import { CustomMarkupComponent } from './shared' | 3 | import { CustomMarkupComponent } from './shared' |
4 | 4 | ||
@@ -9,7 +9,8 @@ import { CustomMarkupComponent } from './shared' | |||
9 | @Component({ | 9 | @Component({ |
10 | selector: 'my-button-markup', | 10 | selector: 'my-button-markup', |
11 | templateUrl: 'button-markup.component.html', | 11 | templateUrl: 'button-markup.component.html', |
12 | styleUrls: [ 'button-markup.component.scss' ] | 12 | styleUrls: [ 'button-markup.component.scss' ], |
13 | changeDetection: ChangeDetectionStrategy.OnPush | ||
13 | }) | 14 | }) |
14 | export class ButtonMarkupComponent implements CustomMarkupComponent { | 15 | export class ButtonMarkupComponent implements CustomMarkupComponent { |
15 | @Input() theme: 'primary' | 'secondary' | 16 | @Input() theme: 'primary' | 'secondary' |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts index ba12b7139..1e7860750 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { from } from 'rxjs' | 1 | import { from } from 'rxjs' |
2 | import { finalize, map, switchMap, tap } from 'rxjs/operators' | 2 | import { finalize, map, switchMap, tap } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 3 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
4 | import { MarkdownService, Notifier, UserService } from '@app/core' | 4 | import { MarkdownService, Notifier, UserService } from '@app/core' |
5 | import { FindInBulkService } from '@app/shared/shared-search' | 5 | import { FindInBulkService } from '@app/shared/shared-search' |
6 | import { VideoSortField } from '@shared/models' | 6 | import { VideoSortField } from '@shared/models' |
@@ -14,7 +14,8 @@ import { CustomMarkupComponent } from './shared' | |||
14 | @Component({ | 14 | @Component({ |
15 | selector: 'my-channel-miniature-markup', | 15 | selector: 'my-channel-miniature-markup', |
16 | templateUrl: 'channel-miniature-markup.component.html', | 16 | templateUrl: 'channel-miniature-markup.component.html', |
17 | styleUrls: [ 'channel-miniature-markup.component.scss' ] | 17 | styleUrls: [ 'channel-miniature-markup.component.scss' ], |
18 | changeDetection: ChangeDetectionStrategy.OnPush | ||
18 | }) | 19 | }) |
19 | export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { | 20 | export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
20 | @Input() name: string | 21 | @Input() name: string |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts index 07fa6fd2d..ab52e7e37 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { finalize } from 'rxjs/operators' | 1 | import { finalize } from 'rxjs/operators' |
2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { FindInBulkService } from '@app/shared/shared-search' | 4 | import { FindInBulkService } from '@app/shared/shared-search' |
5 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' | 5 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' |
@@ -13,7 +13,8 @@ import { CustomMarkupComponent } from './shared' | |||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-playlist-miniature-markup', | 14 | selector: 'my-playlist-miniature-markup', |
15 | templateUrl: 'playlist-miniature-markup.component.html', | 15 | templateUrl: 'playlist-miniature-markup.component.html', |
16 | styleUrls: [ 'playlist-miniature-markup.component.scss' ] | 16 | styleUrls: [ 'playlist-miniature-markup.component.scss' ], |
17 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | 18 | }) |
18 | export class PlaylistMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { | 19 | export class PlaylistMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
19 | @Input() uuid: string | 20 | @Input() uuid: string |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts index cbbacf77c..c37666359 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { finalize } from 'rxjs/operators' | 1 | import { finalize } from 'rxjs/operators' |
2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { FindInBulkService } from '@app/shared/shared-search' | 4 | import { FindInBulkService } from '@app/shared/shared-search' |
5 | import { Video } from '../../shared-main' | 5 | import { Video } from '../../shared-main' |
@@ -13,7 +13,8 @@ import { CustomMarkupComponent } from './shared' | |||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-video-miniature-markup', | 14 | selector: 'my-video-miniature-markup', |
15 | templateUrl: 'video-miniature-markup.component.html', | 15 | templateUrl: 'video-miniature-markup.component.html', |
16 | styleUrls: [ 'video-miniature-markup.component.scss' ] | 16 | styleUrls: [ 'video-miniature-markup.component.scss' ], |
17 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | 18 | }) |
18 | export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { | 19 | export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
19 | @Input() uuid: string | 20 | @Input() uuid: string |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts index 7d3498d4c..70e88ea51 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { finalize } from 'rxjs/operators' | 1 | import { finalize } from 'rxjs/operators' |
2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { VideoSortField } from '@shared/models' | 4 | import { VideoSortField } from '@shared/models' |
5 | import { Video, VideoService } from '../../shared-main' | 5 | import { Video, VideoService } from '../../shared-main' |
@@ -13,7 +13,8 @@ import { CustomMarkupComponent } from './shared' | |||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-videos-list-markup', | 14 | selector: 'my-videos-list-markup', |
15 | templateUrl: 'videos-list-markup.component.html', | 15 | templateUrl: 'videos-list-markup.component.html', |
16 | styleUrls: [ 'videos-list-markup.component.scss' ] | 16 | styleUrls: [ 'videos-list-markup.component.scss' ], |
17 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | 18 | }) |
18 | export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit { | 19 | export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit { |
19 | @Input() sort: string | 20 | @Input() sort: string |
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts index e3371f22c..c6527e169 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts | |||
@@ -31,6 +31,8 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
31 | @Input() markdownType: 'text' | 'enhanced' = 'text' | 31 | @Input() markdownType: 'text' | 'enhanced' = 'text' |
32 | @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> | 32 | @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> |
33 | 33 | ||
34 | @Input() debounceTime = 150 | ||
35 | |||
34 | @Input() markdownVideo: Video | 36 | @Input() markdownVideo: Video |
35 | 37 | ||
36 | @Input() name = 'description' | 38 | @Input() name = 'description' |
@@ -59,7 +61,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
59 | ngOnInit () { | 61 | ngOnInit () { |
60 | this.contentChanged | 62 | this.contentChanged |
61 | .pipe( | 63 | .pipe( |
62 | debounceTime(150), | 64 | debounceTime(this.debounceTime), |
63 | distinctUntilChanged() | 65 | distinctUntilChanged() |
64 | ) | 66 | ) |
65 | .subscribe(() => this.updatePreviews()) | 67 | .subscribe(() => this.updatePreviews()) |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html index 6c05764df..205f2bc97 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.html +++ b/client/src/app/shared/shared-instance/instance-features-table.component.html | |||
@@ -18,10 +18,9 @@ | |||
18 | </tr> | 18 | </tr> |
19 | 19 | ||
20 | <tr> | 20 | <tr> |
21 | <th i18n class="label" scope="row">User registration allowed</th> | 21 | <th i18n class="label" scope="row">User registration</th> |
22 | <td> | 22 | |
23 | <my-feature-boolean [value]="serverConfig.signup.allowed"></my-feature-boolean> | 23 | <td class="value">{{ buildRegistrationLabel() }}</td> |
24 | </td> | ||
25 | </tr> | 24 | </tr> |
26 | 25 | ||
27 | <tr> | 26 | <tr> |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts index e405c5790..c3df7c594 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.ts +++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts | |||
@@ -56,6 +56,15 @@ export class InstanceFeaturesTableComponent implements OnInit { | |||
56 | if (policy === 'display') return $localize`Displayed` | 56 | if (policy === 'display') return $localize`Displayed` |
57 | } | 57 | } |
58 | 58 | ||
59 | buildRegistrationLabel () { | ||
60 | const config = this.serverConfig.signup | ||
61 | |||
62 | if (config.allowed !== true) return $localize`Disabled` | ||
63 | if (config.requiresApproval === true) return $localize`Requires approval by moderators` | ||
64 | |||
65 | return $localize`Enabled` | ||
66 | } | ||
67 | |||
59 | getServerVersionAndCommit () { | 68 | getServerVersionAndCommit () { |
60 | return this.serverService.getServerVersionAndCommit() | 69 | return this.serverService.getServerVersionAndCommit() |
61 | } | 70 | } |
diff --git a/client/src/app/shared/shared-instance/instance.service.ts b/client/src/app/shared/shared-instance/instance.service.ts index 89f47db24..f5b2e05db 100644 --- a/client/src/app/shared/shared-instance/instance.service.ts +++ b/client/src/app/shared/shared-instance/instance.service.ts | |||
@@ -7,6 +7,11 @@ import { peertubeTranslate } from '@shared/core-utils/i18n' | |||
7 | import { About } from '@shared/models' | 7 | import { About } from '@shared/models' |
8 | import { environment } from '../../../environments/environment' | 8 | import { environment } from '../../../environments/environment' |
9 | 9 | ||
10 | export type AboutHTML = Pick<About['instance'], | ||
11 | 'terms' | 'codeOfConduct' | 'moderationInformation' | 'administrator' | 'creationReason' | | ||
12 | 'maintenanceLifetime' | 'businessModel' | 'hardwareInformation' | ||
13 | > | ||
14 | |||
10 | @Injectable() | 15 | @Injectable() |
11 | export class InstanceService { | 16 | export class InstanceService { |
12 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config' | 17 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config' |
@@ -39,7 +44,7 @@ export class InstanceService { | |||
39 | } | 44 | } |
40 | 45 | ||
41 | async buildHtml (about: About) { | 46 | async buildHtml (about: About) { |
42 | const html = { | 47 | const html: AboutHTML = { |
43 | terms: '', | 48 | terms: '', |
44 | codeOfConduct: '', | 49 | codeOfConduct: '', |
45 | moderationInformation: '', | 50 | moderationInformation: '', |
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts index b80ddb9f5..dd41a5f05 100644 --- a/client/src/app/shared/shared-main/account/index.ts +++ b/client/src/app/shared/shared-main/account/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './account.model' | 1 | export * from './account.model' |
2 | export * from './account.service' | 2 | export * from './account.service' |
3 | export * from './actor.model' | 3 | export * from './actor.model' |
4 | export * from './signup-label.component' | ||
diff --git a/client/src/app/shared/shared-main/account/signup-label.component.html b/client/src/app/shared/shared-main/account/signup-label.component.html new file mode 100644 index 000000000..35d6c5360 --- /dev/null +++ b/client/src/app/shared/shared-main/account/signup-label.component.html | |||
@@ -0,0 +1,2 @@ | |||
1 | <ng-container i18n *ngIf="requiresApproval">Request an account</ng-container> | ||
2 | <ng-container i18n *ngIf="!requiresApproval">Create an account</ng-container> | ||
diff --git a/client/src/app/shared/shared-main/account/signup-label.component.ts b/client/src/app/shared/shared-main/account/signup-label.component.ts new file mode 100644 index 000000000..caacb9c6f --- /dev/null +++ b/client/src/app/shared/shared-main/account/signup-label.component.ts | |||
@@ -0,0 +1,9 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-label', | ||
5 | templateUrl: './signup-label.component.html' | ||
6 | }) | ||
7 | export class SignupLabelComponent { | ||
8 | @Input() requiresApproval: boolean | ||
9 | } | ||
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index c1523bc50..eb1642d97 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -16,7 +16,7 @@ import { | |||
16 | import { LoadingBarModule } from '@ngx-loading-bar/core' | 16 | import { LoadingBarModule } from '@ngx-loading-bar/core' |
17 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' | 17 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' |
18 | import { SharedGlobalIconModule } from '../shared-icons' | 18 | import { SharedGlobalIconModule } from '../shared-icons' |
19 | import { AccountService } from './account' | 19 | import { AccountService, SignupLabelComponent } from './account' |
20 | import { | 20 | import { |
21 | AutofocusDirective, | 21 | AutofocusDirective, |
22 | BytesPipe, | 22 | BytesPipe, |
@@ -113,6 +113,8 @@ import { VideoChannelService } from './video-channel' | |||
113 | UserQuotaComponent, | 113 | UserQuotaComponent, |
114 | UserNotificationsComponent, | 114 | UserNotificationsComponent, |
115 | 115 | ||
116 | SignupLabelComponent, | ||
117 | |||
116 | EmbedComponent, | 118 | EmbedComponent, |
117 | 119 | ||
118 | PluginPlaceholderComponent, | 120 | PluginPlaceholderComponent, |
@@ -171,6 +173,8 @@ import { VideoChannelService } from './video-channel' | |||
171 | UserQuotaComponent, | 173 | UserQuotaComponent, |
172 | UserNotificationsComponent, | 174 | UserNotificationsComponent, |
173 | 175 | ||
176 | SignupLabelComponent, | ||
177 | |||
174 | EmbedComponent, | 178 | EmbedComponent, |
175 | 179 | ||
176 | PluginPlaceholderComponent, | 180 | PluginPlaceholderComponent, |
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index bf8870a79..96e7b4dd0 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts | |||
@@ -83,6 +83,11 @@ export class UserNotification implements UserNotificationServer { | |||
83 | latestVersion: string | 83 | latestVersion: string |
84 | } | 84 | } |
85 | 85 | ||
86 | registration?: { | ||
87 | id: number | ||
88 | username: string | ||
89 | } | ||
90 | |||
86 | createdAt: string | 91 | createdAt: string |
87 | updatedAt: string | 92 | updatedAt: string |
88 | 93 | ||
@@ -97,6 +102,8 @@ export class UserNotification implements UserNotificationServer { | |||
97 | 102 | ||
98 | accountUrl?: string | 103 | accountUrl?: string |
99 | 104 | ||
105 | registrationsUrl?: string | ||
106 | |||
100 | videoImportIdentifier?: string | 107 | videoImportIdentifier?: string |
101 | videoImportUrl?: string | 108 | videoImportUrl?: string |
102 | 109 | ||
@@ -135,6 +142,7 @@ export class UserNotification implements UserNotificationServer { | |||
135 | 142 | ||
136 | this.plugin = hash.plugin | 143 | this.plugin = hash.plugin |
137 | this.peertube = hash.peertube | 144 | this.peertube = hash.peertube |
145 | this.registration = hash.registration | ||
138 | 146 | ||
139 | this.createdAt = hash.createdAt | 147 | this.createdAt = hash.createdAt |
140 | this.updatedAt = hash.updatedAt | 148 | this.updatedAt = hash.updatedAt |
@@ -208,6 +216,10 @@ export class UserNotification implements UserNotificationServer { | |||
208 | this.accountUrl = this.buildAccountUrl(this.account) | 216 | this.accountUrl = this.buildAccountUrl(this.account) |
209 | break | 217 | break |
210 | 218 | ||
219 | case UserNotificationType.NEW_USER_REGISTRATION_REQUEST: | ||
220 | this.registrationsUrl = '/admin/moderation/registrations/list' | ||
221 | break | ||
222 | |||
211 | case UserNotificationType.NEW_FOLLOW: | 223 | case UserNotificationType.NEW_FOLLOW: |
212 | this.accountUrl = this.buildAccountUrl(this.actorFollow.follower) | 224 | this.accountUrl = this.buildAccountUrl(this.actorFollow.follower) |
213 | break | 225 | break |
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html index e7cdb0183..a51e08292 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.html +++ b/client/src/app/shared/shared-main/users/user-notifications.component.html | |||
@@ -215,6 +215,14 @@ | |||
215 | </div> | 215 | </div> |
216 | </ng-container> | 216 | </ng-container> |
217 | 217 | ||
218 | <ng-container *ngSwitchCase="20"> <!-- UserNotificationType.NEW_USER_REGISTRATION_REQUEST --> | ||
219 | <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon> | ||
220 | |||
221 | <div class="message" i18n> | ||
222 | User <a (click)="markAsRead(notification)" [routerLink]="notification.registrationsUrl">{{ notification.registration.username }}</a> wants to register on your instance | ||
223 | </div> | ||
224 | </ng-container> | ||
225 | |||
218 | <ng-container *ngSwitchDefault> | 226 | <ng-container *ngSwitchDefault> |
219 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> | 227 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> |
220 | 228 | ||
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.scss b/client/src/app/shared/shared-moderation/account-blocklist.component.scss index 8b1239d34..00aaf3b9c 100644 --- a/client/src/app/shared/shared-moderation/account-blocklist.component.scss +++ b/client/src/app/shared/shared-moderation/account-blocklist.component.scss | |||
@@ -1,10 +1,6 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | 3 | ||
4 | .chip { | ||
5 | @include chip; | ||
6 | } | ||
7 | |||
8 | .unblock-button { | 4 | .unblock-button { |
9 | @include peertube-button; | 5 | @include peertube-button; |
10 | @include grey-button; | 6 | @include grey-button; |
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.ts b/client/src/app/shared/shared-moderation/account-blocklist.component.ts index 9ed00bc12..38dbbff78 100644 --- a/client/src/app/shared/shared-moderation/account-blocklist.component.ts +++ b/client/src/app/shared/shared-moderation/account-blocklist.component.ts | |||
@@ -48,7 +48,7 @@ export class GenericAccountBlocklistComponent extends RestTable implements OnIni | |||
48 | ) | 48 | ) |
49 | } | 49 | } |
50 | 50 | ||
51 | protected reloadData () { | 51 | protected reloadDataInternal () { |
52 | const operation = this.mode === BlocklistComponentType.Account | 52 | const operation = this.mode === BlocklistComponentType.Account |
53 | ? this.blocklistService.getUserAccountBlocklist({ | 53 | ? this.blocklistService.getUserAccountBlocklist({ |
54 | pagination: this.pagination, | 54 | pagination: this.pagination, |
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss index eaf5a8250..7c1e308cf 100644 --- a/client/src/app/shared/shared-moderation/moderation.scss +++ b/client/src/app/shared/shared-moderation/moderation.scss | |||
@@ -40,10 +40,6 @@ | |||
40 | } | 40 | } |
41 | } | 41 | } |
42 | 42 | ||
43 | .chip { | ||
44 | @include chip; | ||
45 | } | ||
46 | |||
47 | my-action-dropdown.show { | 43 | my-action-dropdown.show { |
48 | ::ng-deep .dropdown-root { | 44 | ::ng-deep .dropdown-root { |
49 | display: block !important; | 45 | display: block !important; |
diff --git a/client/src/app/shared/shared-moderation/server-blocklist.component.scss b/client/src/app/shared/shared-moderation/server-blocklist.component.scss index e29668a23..1a6b0435f 100644 --- a/client/src/app/shared/shared-moderation/server-blocklist.component.scss +++ b/client/src/app/shared/shared-moderation/server-blocklist.component.scss | |||
@@ -24,7 +24,3 @@ a { | |||
24 | .block-button { | 24 | .block-button { |
25 | @include create-button; | 25 | @include create-button; |
26 | } | 26 | } |
27 | |||
28 | .chip { | ||
29 | @include chip; | ||
30 | } | ||
diff --git a/client/src/app/shared/shared-moderation/server-blocklist.component.ts b/client/src/app/shared/shared-moderation/server-blocklist.component.ts index 1ba7a1b4d..f1bcbd561 100644 --- a/client/src/app/shared/shared-moderation/server-blocklist.component.ts +++ b/client/src/app/shared/shared-moderation/server-blocklist.component.ts | |||
@@ -75,7 +75,7 @@ export class GenericServerBlocklistComponent extends RestTable implements OnInit | |||
75 | }) | 75 | }) |
76 | } | 76 | } |
77 | 77 | ||
78 | protected reloadData () { | 78 | protected reloadDataInternal () { |
79 | const operation = this.mode === BlocklistComponentType.Account | 79 | const operation = this.mode === BlocklistComponentType.Account |
80 | ? this.blocklistService.getUserServerBlocklist({ | 80 | ? this.blocklistService.getUserServerBlocklist({ |
81 | pagination: this.pagination, | 81 | pagination: this.pagination, |
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts index c69a45c25..50dccf862 100644 --- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts | |||
@@ -105,7 +105,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
105 | const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) | 105 | const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) |
106 | if (res === false) return | 106 | if (res === false) return |
107 | 107 | ||
108 | this.userAdminService.removeUser(user) | 108 | this.userAdminService.removeUsers(user) |
109 | .subscribe({ | 109 | .subscribe({ |
110 | next: () => { | 110 | next: () => { |
111 | this.notifier.success($localize`User ${user.username} deleted.`) | 111 | this.notifier.success($localize`User ${user.username} deleted.`) |
diff --git a/client/src/app/shared/shared-users/index.ts b/client/src/app/shared/shared-users/index.ts index 20e60486d..95d90e49e 100644 --- a/client/src/app/shared/shared-users/index.ts +++ b/client/src/app/shared/shared-users/index.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | export * from './user-admin.service' | 1 | export * from './user-admin.service' |
2 | export * from './user-signup.service' | ||
3 | export * from './two-factor.service' | 2 | export * from './two-factor.service' |
4 | 3 | ||
5 | export * from './shared-users.module' | 4 | export * from './shared-users.module' |
diff --git a/client/src/app/shared/shared-users/shared-users.module.ts b/client/src/app/shared/shared-users/shared-users.module.ts index 5a1675dc9..efffc6026 100644 --- a/client/src/app/shared/shared-users/shared-users.module.ts +++ b/client/src/app/shared/shared-users/shared-users.module.ts | |||
@@ -1,9 +1,7 @@ | |||
1 | |||
2 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
3 | import { SharedMainModule } from '../shared-main/shared-main.module' | 2 | import { SharedMainModule } from '../shared-main/shared-main.module' |
4 | import { TwoFactorService } from './two-factor.service' | 3 | import { TwoFactorService } from './two-factor.service' |
5 | import { UserAdminService } from './user-admin.service' | 4 | import { UserAdminService } from './user-admin.service' |
6 | import { UserSignupService } from './user-signup.service' | ||
7 | 5 | ||
8 | @NgModule({ | 6 | @NgModule({ |
9 | imports: [ | 7 | imports: [ |
@@ -15,7 +13,6 @@ import { UserSignupService } from './user-signup.service' | |||
15 | exports: [], | 13 | exports: [], |
16 | 14 | ||
17 | providers: [ | 15 | providers: [ |
18 | UserSignupService, | ||
19 | UserAdminService, | 16 | UserAdminService, |
20 | TwoFactorService | 17 | TwoFactorService |
21 | ] | 18 | ] |
diff --git a/client/src/app/shared/shared-users/user-admin.service.ts b/client/src/app/shared/shared-users/user-admin.service.ts index 0b04023a3..6224f0bd5 100644 --- a/client/src/app/shared/shared-users/user-admin.service.ts +++ b/client/src/app/shared/shared-users/user-admin.service.ts | |||
@@ -64,7 +64,7 @@ export class UserAdminService { | |||
64 | ) | 64 | ) |
65 | } | 65 | } |
66 | 66 | ||
67 | removeUser (usersArg: UserServerModel | UserServerModel[]) { | 67 | removeUsers (usersArg: UserServerModel | UserServerModel[]) { |
68 | const users = arrayify(usersArg) | 68 | const users = arrayify(usersArg) |
69 | 69 | ||
70 | return from(users) | 70 | return from(users) |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html index 6fdf24b2d..227c12130 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html | |||
@@ -53,8 +53,8 @@ | |||
53 | <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> | 53 | <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> |
54 | </div> | 54 | </div> |
55 | 55 | ||
56 | <div *ngIf="containedInPlaylists" class="video-contained-in-playlists"> | 56 | <div *ngIf="containedInPlaylists" class="fs-6"> |
57 | <a *ngFor="let playlist of containedInPlaylists" class="chip rectangular bg-secondary text-light" [routerLink]="['/w/p/', playlist.playlistShortUUID]"> | 57 | <a *ngFor="let playlist of containedInPlaylists" class="pt-badge badge-secondary" [routerLink]="['/w/p/', playlist.playlistShortUUID]"> |
58 | {{ playlist.playlistDisplayName }} | 58 | {{ playlist.playlistDisplayName }} |
59 | </a> | 59 | </a> |
60 | </div> | 60 | </div> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss index ba2adfc5a..a397efdca 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss | |||
@@ -4,10 +4,6 @@ | |||
4 | 4 | ||
5 | $more-button-width: 40px; | 5 | $more-button-width: 40px; |
6 | 6 | ||
7 | .chip { | ||
8 | @include chip; | ||
9 | } | ||
10 | |||
11 | .video-miniature { | 7 | .video-miniature { |
12 | font-size: 14px; | 8 | font-size: 14px; |
13 | } | 9 | } |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index 85c63c173..706227e66 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts | |||
@@ -314,6 +314,6 @@ export class VideoMiniatureComponent implements OnInit { | |||
314 | this.cd.markForCheck() | 314 | this.cd.markForCheck() |
315 | }) | 315 | }) |
316 | 316 | ||
317 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 317 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
318 | } | 318 | } |
319 | } | 319 | } |
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts index d5cdd958e..7b832263e 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as debug from 'debug' | 1 | import * as debug from 'debug' |
2 | import { fromEvent, Observable, Subject, Subscription } from 'rxjs' | 2 | import { fromEvent, Observable, Subject, Subscription } from 'rxjs' |
3 | import { debounceTime, switchMap } from 'rxjs/operators' | 3 | import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' |
4 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' | 4 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' |
5 | import { ActivatedRoute } from '@angular/router' | 5 | import { ActivatedRoute } from '@angular/router' |
6 | import { | 6 | import { |
@@ -111,6 +111,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
111 | 111 | ||
112 | private lastQueryLength: number | 112 | private lastQueryLength: number |
113 | 113 | ||
114 | private videoRequests = new Subject<{ reset: boolean, obs: Observable<ResultList<Video>> }>() | ||
115 | |||
114 | constructor ( | 116 | constructor ( |
115 | private notifier: Notifier, | 117 | private notifier: Notifier, |
116 | private authService: AuthService, | 118 | private authService: AuthService, |
@@ -124,6 +126,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
124 | } | 126 | } |
125 | 127 | ||
126 | ngOnInit () { | 128 | ngOnInit () { |
129 | this.subscribeToVideoRequests() | ||
130 | |||
127 | const hiddenFilters = this.hideScopeFilter | 131 | const hiddenFilters = this.hideScopeFilter |
128 | ? [ 'scope' ] | 132 | ? [ 'scope' ] |
129 | : [] | 133 | : [] |
@@ -228,30 +232,12 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
228 | } | 232 | } |
229 | 233 | ||
230 | loadMoreVideos (reset = false) { | 234 | loadMoreVideos (reset = false) { |
231 | if (reset) this.hasDoneFirstQuery = false | 235 | if (reset) { |
232 | 236 | this.hasDoneFirstQuery = false | |
233 | this.getVideosObservableFunction(this.pagination, this.filters) | 237 | this.videos = [] |
234 | .subscribe({ | 238 | } |
235 | next: ({ data }) => { | ||
236 | this.hasDoneFirstQuery = true | ||
237 | this.lastQueryLength = data.length | ||
238 | |||
239 | if (reset) this.videos = [] | ||
240 | this.videos = this.videos.concat(data) | ||
241 | |||
242 | if (this.groupByDate) this.buildGroupedDateLabels() | ||
243 | |||
244 | this.onDataSubject.next(data) | ||
245 | this.videosLoaded.emit(this.videos) | ||
246 | }, | ||
247 | |||
248 | error: err => { | ||
249 | const message = $localize`Cannot load more videos. Try again later.` | ||
250 | 239 | ||
251 | logger.error(message, err) | 240 | this.videoRequests.next({ reset, obs: this.getVideosObservableFunction(this.pagination, this.filters) }) |
252 | this.notifier.error(message) | ||
253 | } | ||
254 | }) | ||
255 | } | 241 | } |
256 | 242 | ||
257 | reloadVideos () { | 243 | reloadVideos () { |
@@ -423,4 +409,30 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
423 | this.onFiltersChanged(true) | 409 | this.onFiltersChanged(true) |
424 | }) | 410 | }) |
425 | } | 411 | } |
412 | |||
413 | private subscribeToVideoRequests () { | ||
414 | this.videoRequests | ||
415 | .pipe(concatMap(({ reset, obs }) => obs.pipe(map(({ data }) => ({ data, reset }))))) | ||
416 | .subscribe({ | ||
417 | next: ({ data, reset }) => { | ||
418 | this.hasDoneFirstQuery = true | ||
419 | this.lastQueryLength = data.length | ||
420 | |||
421 | if (reset) this.videos = [] | ||
422 | this.videos = this.videos.concat(data) | ||
423 | |||
424 | if (this.groupByDate) this.buildGroupedDateLabels() | ||
425 | |||
426 | this.onDataSubject.next(data) | ||
427 | this.videosLoaded.emit(this.videos) | ||
428 | }, | ||
429 | |||
430 | error: err => { | ||
431 | const message = $localize`Cannot load more videos. Try again later.` | ||
432 | |||
433 | logger.error(message, err) | ||
434 | this.notifier.error(message) | ||
435 | } | ||
436 | }) | ||
437 | } | ||
426 | } | 438 | } |
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts index 2fc39fc75..f802416a4 100644 --- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts | |||
@@ -81,7 +81,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
81 | .subscribe(result => { | 81 | .subscribe(result => { |
82 | this.playlistsData = result.data | 82 | this.playlistsData = result.data |
83 | 83 | ||
84 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 84 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
85 | }) | 85 | }) |
86 | 86 | ||
87 | this.videoPlaylistSearchChanged | 87 | this.videoPlaylistSearchChanged |
@@ -129,7 +129,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
129 | .subscribe(playlistsResult => { | 129 | .subscribe(playlistsResult => { |
130 | this.playlistsData = playlistsResult.data | 130 | this.playlistsData = playlistsResult.data |
131 | 131 | ||
132 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 132 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
133 | }) | 133 | }) |
134 | } | 134 | } |
135 | 135 | ||
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts index 330a51f91..bc9fb0d74 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts | |||
@@ -206,7 +206,15 @@ export class VideoPlaylistService { | |||
206 | stopTimestamp: body.stopTimestamp | 206 | stopTimestamp: body.stopTimestamp |
207 | }) | 207 | }) |
208 | 208 | ||
209 | this.runPlaylistCheck(body.videoId) | 209 | this.runVideoExistsInPlaylistCheck(body.videoId) |
210 | |||
211 | if (this.myAccountPlaylistCache) { | ||
212 | const playlist = this.myAccountPlaylistCache.data.find(p => p.id === playlistId) | ||
213 | if (!playlist) return | ||
214 | |||
215 | const otherPlaylists = this.myAccountPlaylistCache.data.filter(p => p !== playlist) | ||
216 | this.myAccountPlaylistCache.data = [ playlist, ...otherPlaylists ] | ||
217 | } | ||
210 | }), | 218 | }), |
211 | catchError(err => this.restExtractor.handleError(err)) | 219 | catchError(err => this.restExtractor.handleError(err)) |
212 | ) | 220 | ) |
@@ -225,7 +233,7 @@ export class VideoPlaylistService { | |||
225 | elem.stopTimestamp = body.stopTimestamp | 233 | elem.stopTimestamp = body.stopTimestamp |
226 | } | 234 | } |
227 | 235 | ||
228 | this.runPlaylistCheck(videoId) | 236 | this.runVideoExistsInPlaylistCheck(videoId) |
229 | }), | 237 | }), |
230 | catchError(err => this.restExtractor.handleError(err)) | 238 | catchError(err => this.restExtractor.handleError(err)) |
231 | ) | 239 | ) |
@@ -242,7 +250,7 @@ export class VideoPlaylistService { | |||
242 | .filter(e => e.playlistElementId !== playlistElementId) | 250 | .filter(e => e.playlistElementId !== playlistElementId) |
243 | } | 251 | } |
244 | 252 | ||
245 | this.runPlaylistCheck(videoId) | 253 | this.runVideoExistsInPlaylistCheck(videoId) |
246 | }), | 254 | }), |
247 | catchError(err => this.restExtractor.handleError(err)) | 255 | catchError(err => this.restExtractor.handleError(err)) |
248 | ) | 256 | ) |
@@ -296,7 +304,7 @@ export class VideoPlaylistService { | |||
296 | return obs | 304 | return obs |
297 | } | 305 | } |
298 | 306 | ||
299 | runPlaylistCheck (videoId: number) { | 307 | runVideoExistsInPlaylistCheck (videoId: number) { |
300 | debugLogger('Running playlist check.') | 308 | debugLogger('Running playlist check.') |
301 | 309 | ||
302 | if (this.videoExistsCache[videoId]) { | 310 | if (this.videoExistsCache[videoId]) { |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 56310c4e9..2781850b9 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -11,6 +11,7 @@ import './shared/control-bar/p2p-info-button' | |||
11 | import './shared/control-bar/peertube-link-button' | 11 | import './shared/control-bar/peertube-link-button' |
12 | import './shared/control-bar/peertube-load-progress-bar' | 12 | import './shared/control-bar/peertube-load-progress-bar' |
13 | import './shared/control-bar/theater-button' | 13 | import './shared/control-bar/theater-button' |
14 | import './shared/control-bar/peertube-live-display' | ||
14 | import './shared/settings/resolution-menu-button' | 15 | import './shared/settings/resolution-menu-button' |
15 | import './shared/settings/resolution-menu-item' | 16 | import './shared/settings/resolution-menu-item' |
16 | import './shared/settings/settings-dialog' | 17 | import './shared/settings/settings-dialog' |
@@ -96,6 +97,10 @@ export class PeertubePlayerManager { | |||
96 | videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { | 97 | videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { |
97 | const player = this | 98 | const player = this |
98 | 99 | ||
100 | if (!isNaN(+options.common.playbackRate)) { | ||
101 | player.playbackRate(+options.common.playbackRate) | ||
102 | } | ||
103 | |||
99 | let alreadyFallback = false | 104 | let alreadyFallback = false |
100 | 105 | ||
101 | const handleError = () => { | 106 | const handleError = () => { |
@@ -118,7 +123,7 @@ export class PeertubePlayerManager { | |||
118 | self.addContextMenu(videojsOptionsBuilder, player, options.common) | 123 | self.addContextMenu(videojsOptionsBuilder, player, options.common) |
119 | 124 | ||
120 | if (isMobile()) player.peertubeMobile() | 125 | if (isMobile()) player.peertubeMobile() |
121 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin() | 126 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin({ isLive: options.common.isLive }) |
122 | if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') | 127 | if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') |
123 | 128 | ||
124 | player.bezels() | 129 | player.bezels() |
diff --git a/client/src/assets/player/shared/control-bar/index.ts b/client/src/assets/player/shared/control-bar/index.ts index db5b8938d..e71e90713 100644 --- a/client/src/assets/player/shared/control-bar/index.ts +++ b/client/src/assets/player/shared/control-bar/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export * from './next-previous-video-button' | 1 | export * from './next-previous-video-button' |
2 | export * from './p2p-info-button' | 2 | export * from './p2p-info-button' |
3 | export * from './peertube-link-button' | 3 | export * from './peertube-link-button' |
4 | export * from './peertube-live-display' | ||
4 | export * from './peertube-load-progress-bar' | 5 | export * from './peertube-load-progress-bar' |
5 | export * from './theater-button' | 6 | export * from './theater-button' |
diff --git a/client/src/assets/player/shared/control-bar/peertube-live-display.ts b/client/src/assets/player/shared/control-bar/peertube-live-display.ts new file mode 100644 index 000000000..649eb0b00 --- /dev/null +++ b/client/src/assets/player/shared/control-bar/peertube-live-display.ts | |||
@@ -0,0 +1,93 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { PeerTubeLinkButtonOptions } from '../../types' | ||
3 | |||
4 | const ClickableComponent = videojs.getComponent('ClickableComponent') | ||
5 | |||
6 | class PeerTubeLiveDisplay extends ClickableComponent { | ||
7 | private interval: any | ||
8 | |||
9 | private contentEl_: any | ||
10 | |||
11 | constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) { | ||
12 | super(player, options as any) | ||
13 | |||
14 | this.interval = this.setInterval(() => this.updateClass(), 1000) | ||
15 | |||
16 | this.show() | ||
17 | this.updateSync(true) | ||
18 | } | ||
19 | |||
20 | dispose () { | ||
21 | if (this.interval) { | ||
22 | this.clearInterval(this.interval) | ||
23 | this.interval = undefined | ||
24 | } | ||
25 | |||
26 | this.contentEl_ = null | ||
27 | |||
28 | super.dispose() | ||
29 | } | ||
30 | |||
31 | createEl () { | ||
32 | const el = super.createEl('div', { | ||
33 | className: 'vjs-live-control vjs-control' | ||
34 | }) | ||
35 | |||
36 | this.contentEl_ = videojs.dom.createEl('div', { | ||
37 | className: 'vjs-live-display' | ||
38 | }, { | ||
39 | 'aria-live': 'off' | ||
40 | }) | ||
41 | |||
42 | this.contentEl_.appendChild(videojs.dom.createEl('span', { | ||
43 | className: 'vjs-control-text', | ||
44 | textContent: `${this.localize('Stream Type')}\u00a0` | ||
45 | })) | ||
46 | |||
47 | this.contentEl_.appendChild(document.createTextNode(this.localize('LIVE'))) | ||
48 | |||
49 | el.appendChild(this.contentEl_) | ||
50 | return el | ||
51 | } | ||
52 | |||
53 | handleClick () { | ||
54 | const hlsjs = this.getHLSJS() | ||
55 | if (!hlsjs) return | ||
56 | |||
57 | this.player().currentTime(hlsjs.liveSyncPosition) | ||
58 | this.player().play() | ||
59 | this.updateSync(true) | ||
60 | } | ||
61 | |||
62 | private updateClass () { | ||
63 | const hlsjs = this.getHLSJS() | ||
64 | if (!hlsjs) return | ||
65 | |||
66 | // Not loaded yet | ||
67 | if (this.player().currentTime() === 0) return | ||
68 | |||
69 | const isSync = Math.abs(this.player().currentTime() - hlsjs.liveSyncPosition) < 10 | ||
70 | this.updateSync(isSync) | ||
71 | } | ||
72 | |||
73 | private updateSync (isSync: boolean) { | ||
74 | if (isSync) { | ||
75 | this.addClass('synced-with-live-edge') | ||
76 | this.removeAttribute('title') | ||
77 | this.disable() | ||
78 | } else { | ||
79 | this.removeClass('synced-with-live-edge') | ||
80 | this.setAttribute('title', this.localize('Go back to the live')) | ||
81 | this.enable() | ||
82 | } | ||
83 | } | ||
84 | |||
85 | private getHLSJS () { | ||
86 | const p2pMediaLoader = this.player()?.p2pMediaLoader | ||
87 | if (!p2pMediaLoader) return undefined | ||
88 | |||
89 | return p2pMediaLoader().getHLSJS() | ||
90 | } | ||
91 | } | ||
92 | |||
93 | videojs.registerComponent('PeerTubeLiveDisplay', PeerTubeLiveDisplay) | ||
diff --git a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts index ec1e1038b..f5b4b3919 100644 --- a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts +++ b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts | |||
@@ -4,6 +4,10 @@ type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardE | |||
4 | 4 | ||
5 | const Plugin = videojs.getPlugin('plugin') | 5 | const Plugin = videojs.getPlugin('plugin') |
6 | 6 | ||
7 | export type HotkeysOptions = { | ||
8 | isLive: boolean | ||
9 | } | ||
10 | |||
7 | class PeerTubeHotkeysPlugin extends Plugin { | 11 | class PeerTubeHotkeysPlugin extends Plugin { |
8 | private static readonly VOLUME_STEP = 0.1 | 12 | private static readonly VOLUME_STEP = 0.1 |
9 | private static readonly SEEK_STEP = 5 | 13 | private static readonly SEEK_STEP = 5 |
@@ -12,9 +16,13 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
12 | 16 | ||
13 | private readonly handlers: KeyHandler[] | 17 | private readonly handlers: KeyHandler[] |
14 | 18 | ||
15 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { | 19 | private readonly isLive: boolean |
20 | |||
21 | constructor (player: videojs.Player, options: videojs.PlayerOptions & HotkeysOptions) { | ||
16 | super(player, options) | 22 | super(player, options) |
17 | 23 | ||
24 | this.isLive = options.isLive | ||
25 | |||
18 | this.handlers = this.buildHandlers() | 26 | this.handlers = this.buildHandlers() |
19 | 27 | ||
20 | this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) | 28 | this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) |
@@ -68,28 +76,6 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
68 | } | 76 | } |
69 | }, | 77 | }, |
70 | 78 | ||
71 | // Rewind | ||
72 | { | ||
73 | accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'), | ||
74 | cb: e => { | ||
75 | e.preventDefault() | ||
76 | |||
77 | const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP) | ||
78 | this.player.currentTime(target) | ||
79 | } | ||
80 | }, | ||
81 | |||
82 | // Forward | ||
83 | { | ||
84 | accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'), | ||
85 | cb: e => { | ||
86 | e.preventDefault() | ||
87 | |||
88 | const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP) | ||
89 | this.player.currentTime(target) | ||
90 | } | ||
91 | }, | ||
92 | |||
93 | // Fullscreen | 79 | // Fullscreen |
94 | { | 80 | { |
95 | // f key or Ctrl + Enter | 81 | // f key or Ctrl + Enter |
@@ -116,6 +102,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
116 | { | 102 | { |
117 | accept: e => e.key === '>', | 103 | accept: e => e.key === '>', |
118 | cb: () => { | 104 | cb: () => { |
105 | if (this.isLive) return | ||
106 | |||
119 | const target = Math.min(this.player.playbackRate() + 0.1, 5) | 107 | const target = Math.min(this.player.playbackRate() + 0.1, 5) |
120 | 108 | ||
121 | this.player.playbackRate(parseFloat(target.toFixed(2))) | 109 | this.player.playbackRate(parseFloat(target.toFixed(2))) |
@@ -126,6 +114,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
126 | { | 114 | { |
127 | accept: e => e.key === '<', | 115 | accept: e => e.key === '<', |
128 | cb: () => { | 116 | cb: () => { |
117 | if (this.isLive) return | ||
118 | |||
129 | const target = Math.max(this.player.playbackRate() - 0.1, 0.10) | 119 | const target = Math.max(this.player.playbackRate() - 0.1, 0.10) |
130 | 120 | ||
131 | this.player.playbackRate(parseFloat(target.toFixed(2))) | 121 | this.player.playbackRate(parseFloat(target.toFixed(2))) |
@@ -136,6 +126,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
136 | { | 126 | { |
137 | accept: e => e.key === ',', | 127 | accept: e => e.key === ',', |
138 | cb: () => { | 128 | cb: () => { |
129 | if (this.isLive) return | ||
130 | |||
139 | this.player.pause() | 131 | this.player.pause() |
140 | 132 | ||
141 | // Calculate movement distance (assuming 30 fps) | 133 | // Calculate movement distance (assuming 30 fps) |
@@ -148,6 +140,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
148 | { | 140 | { |
149 | accept: e => e.key === '.', | 141 | accept: e => e.key === '.', |
150 | cb: () => { | 142 | cb: () => { |
143 | if (this.isLive) return | ||
144 | |||
151 | this.player.pause() | 145 | this.player.pause() |
152 | 146 | ||
153 | // Calculate movement distance (assuming 30 fps) | 147 | // Calculate movement distance (assuming 30 fps) |
@@ -157,11 +151,47 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
157 | } | 151 | } |
158 | ] | 152 | ] |
159 | 153 | ||
154 | if (this.isLive) return handlers | ||
155 | |||
156 | return handlers.concat(this.buildVODHandlers()) | ||
157 | } | ||
158 | |||
159 | private buildVODHandlers () { | ||
160 | const handlers: KeyHandler[] = [ | ||
161 | // Rewind | ||
162 | { | ||
163 | accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'), | ||
164 | cb: e => { | ||
165 | if (this.isLive) return | ||
166 | |||
167 | e.preventDefault() | ||
168 | |||
169 | const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP) | ||
170 | this.player.currentTime(target) | ||
171 | } | ||
172 | }, | ||
173 | |||
174 | // Forward | ||
175 | { | ||
176 | accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'), | ||
177 | cb: e => { | ||
178 | if (this.isLive) return | ||
179 | |||
180 | e.preventDefault() | ||
181 | |||
182 | const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP) | ||
183 | this.player.currentTime(target) | ||
184 | } | ||
185 | } | ||
186 | ] | ||
187 | |||
160 | // 0-9 key handlers | 188 | // 0-9 key handlers |
161 | for (let i = 0; i < 10; i++) { | 189 | for (let i = 0; i < 10; i++) { |
162 | handlers.push({ | 190 | handlers.push({ |
163 | accept: e => this.isNakedOrShift(e, i + ''), | 191 | accept: e => this.isNakedOrShift(e, i + ''), |
164 | cb: e => { | 192 | cb: e => { |
193 | if (this.isLive) return | ||
194 | |||
165 | e.preventDefault() | 195 | e.preventDefault() |
166 | 196 | ||
167 | this.player.currentTime(this.player.duration() * i * 0.1) | 197 | this.player.currentTime(this.player.duration() * i * 0.1) |
diff --git a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts index 27f366732..26f923e92 100644 --- a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts +++ b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts | |||
@@ -30,10 +30,7 @@ export class ControlBarOptionsBuilder { | |||
30 | } | 30 | } |
31 | 31 | ||
32 | Object.assign(children, { | 32 | Object.assign(children, { |
33 | currentTimeDisplay: {}, | 33 | ...this.getTimeControls(), |
34 | timeDivider: {}, | ||
35 | durationDisplay: {}, | ||
36 | liveDisplay: {}, | ||
37 | 34 | ||
38 | flexibleWidthSpacer: {}, | 35 | flexibleWidthSpacer: {}, |
39 | 36 | ||
@@ -74,7 +71,9 @@ export class ControlBarOptionsBuilder { | |||
74 | private getSettingsButton () { | 71 | private getSettingsButton () { |
75 | const settingEntries: string[] = [] | 72 | const settingEntries: string[] = [] |
76 | 73 | ||
77 | settingEntries.push('playbackRateMenuButton') | 74 | if (!this.options.isLive) { |
75 | settingEntries.push('playbackRateMenuButton') | ||
76 | } | ||
78 | 77 | ||
79 | if (this.options.captions === true) settingEntries.push('captionsButton') | 78 | if (this.options.captions === true) settingEntries.push('captionsButton') |
80 | 79 | ||
@@ -90,7 +89,23 @@ export class ControlBarOptionsBuilder { | |||
90 | } | 89 | } |
91 | } | 90 | } |
92 | 91 | ||
92 | private getTimeControls () { | ||
93 | if (this.options.isLive) { | ||
94 | return { | ||
95 | peerTubeLiveDisplay: {} | ||
96 | } | ||
97 | } | ||
98 | |||
99 | return { | ||
100 | currentTimeDisplay: {}, | ||
101 | timeDivider: {}, | ||
102 | durationDisplay: {} | ||
103 | } | ||
104 | } | ||
105 | |||
93 | private getProgressControl () { | 106 | private getProgressControl () { |
107 | if (this.options.isLive) return {} | ||
108 | |||
94 | const loadProgressBar = this.mode === 'webtorrent' | 109 | const loadProgressBar = this.mode === 'webtorrent' |
95 | ? 'peerTubeLoadProgressBar' | 110 | ? 'peerTubeLoadProgressBar' |
96 | : 'loadProgressBar' | 111 | : 'loadProgressBar' |
diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts index a14beb347..7f7d90ab9 100644 --- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts | |||
@@ -281,8 +281,8 @@ class Html5Hlsjs { | |||
281 | if (this.errorCounts[data.type]) this.errorCounts[data.type] += 1 | 281 | if (this.errorCounts[data.type]) this.errorCounts[data.type] += 1 |
282 | else this.errorCounts[data.type] = 1 | 282 | else this.errorCounts[data.type] = 1 |
283 | 283 | ||
284 | if (data.fatal) logger.warn(error.message) | 284 | if (data.fatal) logger.error(error.message, { currentTime: this.player.currentTime(), data }) |
285 | else logger.error(error.message, { data }) | 285 | else logger.warn(error.message) |
286 | 286 | ||
287 | if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { | 287 | if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { |
288 | error.code = 2 | 288 | error.code = 2 |
diff --git a/client/src/assets/player/shared/stats/stats-card.ts b/client/src/assets/player/shared/stats/stats-card.ts index f23ae48be..471a5e46c 100644 --- a/client/src/assets/player/shared/stats/stats-card.ts +++ b/client/src/assets/player/shared/stats/stats-card.ts | |||
@@ -182,7 +182,7 @@ class StatsCard extends Component { | |||
182 | let colorSpace = 'unknown' | 182 | let colorSpace = 'unknown' |
183 | let codecs = 'unknown' | 183 | let codecs = 'unknown' |
184 | 184 | ||
185 | if (metadata?.streams[0]) { | 185 | if (metadata?.streams?.[0]) { |
186 | const stream = metadata.streams[0] | 186 | const stream = metadata.streams[0] |
187 | 187 | ||
188 | colorSpace = stream['color_space'] !== 'unknown' | 188 | colorSpace = stream['color_space'] !== 'unknown' |
@@ -193,7 +193,7 @@ class StatsCard extends Component { | |||
193 | } | 193 | } |
194 | 194 | ||
195 | const resolution = videoFile?.resolution.label + videoFile?.fps | 195 | const resolution = videoFile?.resolution.label + videoFile?.fps |
196 | const buffer = this.timeRangesToString(this.player().buffered()) | 196 | const buffer = this.timeRangesToString(this.player_.buffered()) |
197 | const progress = this.player_.webtorrent().getTorrent()?.progress | 197 | const progress = this.player_.webtorrent().getTorrent()?.progress |
198 | 198 | ||
199 | return { | 199 | return { |
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts index 3057a5adb..3fbcec29c 100644 --- a/client/src/assets/player/types/manager-options.ts +++ b/client/src/assets/player/types/manager-options.ts | |||
@@ -29,6 +29,8 @@ export interface CustomizationOptions { | |||
29 | resume?: string | 29 | resume?: string |
30 | 30 | ||
31 | peertubeLink: boolean | 31 | peertubeLink: boolean |
32 | |||
33 | playbackRate?: number | string | ||
32 | } | 34 | } |
33 | 35 | ||
34 | export interface CommonOptions extends CustomizationOptions { | 36 | export interface CommonOptions extends CustomizationOptions { |
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts index c60154f3b..5674f78cb 100644 --- a/client/src/assets/player/types/peertube-videojs-typings.ts +++ b/client/src/assets/player/types/peertube-videojs-typings.ts | |||
@@ -3,6 +3,7 @@ import videojs from 'video.js' | |||
3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' | 3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' |
4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' | 4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | 5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' |
6 | import { HotkeysOptions } from '../shared/hotkeys/peertube-hotkeys-plugin' | ||
6 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' | 7 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' |
7 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' | 8 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' |
8 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' | 9 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' |
@@ -44,7 +45,7 @@ declare module 'video.js' { | |||
44 | 45 | ||
45 | bezels (): void | 46 | bezels (): void |
46 | peertubeMobile (): void | 47 | peertubeMobile (): void |
47 | peerTubeHotkeysPlugin (): void | 48 | peerTubeHotkeysPlugin (options?: HotkeysOptions): void |
48 | 49 | ||
49 | stats (options?: StatsCardOptions): StatsForNerdsPlugin | 50 | stats (options?: StatsCardOptions): StatsForNerdsPlugin |
50 | 51 | ||
diff --git a/client/src/root-helpers/logger.ts b/client/src/root-helpers/logger.ts index d1fdf73aa..618be62cd 100644 --- a/client/src/root-helpers/logger.ts +++ b/client/src/root-helpers/logger.ts | |||
@@ -27,6 +27,10 @@ class Logger { | |||
27 | warn (message: LoggerMessage, meta?: LoggerMeta) { | 27 | warn (message: LoggerMessage, meta?: LoggerMeta) { |
28 | this.runHooks('warn', message, meta) | 28 | this.runHooks('warn', message, meta) |
29 | 29 | ||
30 | this.clientWarn(message, meta) | ||
31 | } | ||
32 | |||
33 | clientWarn (message: LoggerMessage, meta?: LoggerMeta) { | ||
30 | if (meta) console.warn(message, meta) | 34 | if (meta) console.warn(message, meta) |
31 | else console.warn(message) | 35 | else console.warn(message) |
32 | } | 36 | } |
@@ -34,6 +38,10 @@ class Logger { | |||
34 | error (message: LoggerMessage, meta?: LoggerMeta) { | 38 | error (message: LoggerMessage, meta?: LoggerMeta) { |
35 | this.runHooks('error', message, meta) | 39 | this.runHooks('error', message, meta) |
36 | 40 | ||
41 | this.clientError(message, meta) | ||
42 | } | ||
43 | |||
44 | clientError (message: LoggerMessage, meta?: LoggerMeta) { | ||
37 | if (meta) console.error(message, meta) | 45 | if (meta) console.error(message, meta) |
38 | else console.error(message) | 46 | else console.error(message) |
39 | } | 47 | } |
diff --git a/client/src/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts index 6c64e2b01..e5b06a94c 100644 --- a/client/src/root-helpers/plugins-manager.ts +++ b/client/src/root-helpers/plugins-manager.ts | |||
@@ -3,7 +3,7 @@ import * as debug from 'debug' | |||
3 | import { firstValueFrom, ReplaySubject } from 'rxjs' | 3 | import { firstValueFrom, ReplaySubject } from 'rxjs' |
4 | import { first, shareReplay } from 'rxjs/operators' | 4 | import { first, shareReplay } from 'rxjs/operators' |
5 | import { RegisterClientHelpers } from 'src/types/register-client-option.model' | 5 | import { RegisterClientHelpers } from 'src/types/register-client-option.model' |
6 | import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' | 6 | import { getExternalAuthHref, getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' |
7 | import { | 7 | import { |
8 | ClientHookName, | 8 | ClientHookName, |
9 | clientHookObject, | 9 | clientHookObject, |
@@ -16,7 +16,6 @@ import { | |||
16 | RegisterClientRouteOptions, | 16 | RegisterClientRouteOptions, |
17 | RegisterClientSettingsScriptOptions, | 17 | RegisterClientSettingsScriptOptions, |
18 | RegisterClientVideoFieldOptions, | 18 | RegisterClientVideoFieldOptions, |
19 | RegisteredExternalAuthConfig, | ||
20 | ServerConfigPlugin | 19 | ServerConfigPlugin |
21 | } from '@shared/models' | 20 | } from '@shared/models' |
22 | import { environment } from '../environments/environment' | 21 | import { environment } from '../environments/environment' |
@@ -94,9 +93,13 @@ class PluginsManager { | |||
94 | return isTheme ? '/themes' : '/plugins' | 93 | return isTheme ? '/themes' : '/plugins' |
95 | } | 94 | } |
96 | 95 | ||
97 | static getExternalAuthHref (auth: RegisteredExternalAuthConfig) { | 96 | static getDefaultLoginHref (apiUrl: string, serverConfig: HTMLServerConfig) { |
98 | return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` | 97 | if (!serverConfig || serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined |
99 | 98 | ||
99 | const externalAuths = serverConfig.plugin.registeredExternalAuths | ||
100 | if (externalAuths.length !== 1) return undefined | ||
101 | |||
102 | return getExternalAuthHref(apiUrl, externalAuths[0]) | ||
100 | } | 103 | } |
101 | 104 | ||
102 | loadPluginsList (config: HTMLServerConfig) { | 105 | loadPluginsList (config: HTMLServerConfig) { |
diff --git a/client/src/sass/class-helpers.scss b/client/src/sass/class-helpers.scss index bc965331a..feb3a6de2 100644 --- a/client/src/sass/class-helpers.scss +++ b/client/src/sass/class-helpers.scss | |||
@@ -284,3 +284,9 @@ label + .form-group-description { | |||
284 | border: 2px solid pvar(--mainColorLightest); | 284 | border: 2px solid pvar(--mainColorLightest); |
285 | } | 285 | } |
286 | } | 286 | } |
287 | |||
288 | // --------------------------------------------------------------------------- | ||
289 | |||
290 | .chip { | ||
291 | @include chip; | ||
292 | } | ||
diff --git a/client/src/sass/include/_badges.scss b/client/src/sass/include/_badges.scss index 4bc70d4a9..7efd2fb81 100644 --- a/client/src/sass/include/_badges.scss +++ b/client/src/sass/include/_badges.scss | |||
@@ -9,6 +9,10 @@ | |||
9 | font-weight: $font-semibold; | 9 | font-weight: $font-semibold; |
10 | line-height: 1.1; | 10 | line-height: 1.1; |
11 | 11 | ||
12 | &.badge-fs-normal { | ||
13 | font-size: 100%; | ||
14 | } | ||
15 | |||
12 | &.badge-primary { | 16 | &.badge-primary { |
13 | color: pvar(--mainBackgroundColor); | 17 | color: pvar(--mainBackgroundColor); |
14 | background-color: pvar(--mainColor); | 18 | background-color: pvar(--mainColor); |
diff --git a/client/src/sass/include/_fonts.scss b/client/src/sass/include/_fonts.scss index e5a40af34..514261d01 100644 --- a/client/src/sass/include/_fonts.scss +++ b/client/src/sass/include/_fonts.scss | |||
@@ -15,7 +15,3 @@ | |||
15 | font-display: swap; | 15 | font-display: swap; |
16 | src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2'); | 16 | src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2'); |
17 | } | 17 | } |
18 | |||
19 | @mixin muted { | ||
20 | color: pvar(--greyForegroundColor) !important; | ||
21 | } | ||
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index b5ccb6598..8816437d9 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -36,6 +36,10 @@ | |||
36 | max-height: $font-size * $number-of-lines; | 36 | max-height: $font-size * $number-of-lines; |
37 | } | 37 | } |
38 | 38 | ||
39 | @mixin muted { | ||
40 | color: pvar(--greyForegroundColor) !important; | ||
41 | } | ||
42 | |||
39 | @mixin fade-text ($fade-after, $background-color) { | 43 | @mixin fade-text ($fade-after, $background-color) { |
40 | position: relative; | 44 | position: relative; |
41 | overflow: hidden; | 45 | overflow: hidden; |
@@ -791,51 +795,39 @@ | |||
791 | } | 795 | } |
792 | 796 | ||
793 | @mixin chip { | 797 | @mixin chip { |
794 | --chip-radius: 5rem; | 798 | --avatar-size: 1.2rem; |
795 | --chip-padding: .2rem .4rem; | ||
796 | $avatar-height: 1.2rem; | ||
797 | 799 | ||
798 | align-items: center; | ||
799 | border-radius: var(--chip-radius); | ||
800 | display: inline-flex; | 800 | display: inline-flex; |
801 | font-size: 90%; | ||
802 | color: pvar(--mainForegroundColor); | 801 | color: pvar(--mainForegroundColor); |
803 | height: $avatar-height; | 802 | height: var(--avatar-size); |
804 | line-height: 1rem; | ||
805 | margin: .1rem; | ||
806 | max-width: 320px; | 803 | max-width: 320px; |
807 | overflow: hidden; | 804 | overflow: hidden; |
808 | padding: var(--chip-padding); | ||
809 | text-decoration: none; | 805 | text-decoration: none; |
810 | text-overflow: ellipsis; | 806 | text-overflow: ellipsis; |
811 | vertical-align: middle; | 807 | vertical-align: middle; |
812 | white-space: nowrap; | 808 | white-space: nowrap; |
813 | 809 | ||
814 | &.rectangular { | ||
815 | --chip-radius: .2rem; | ||
816 | --chip-padding: .2rem .3rem; | ||
817 | } | ||
818 | |||
819 | my-actor-avatar { | 810 | my-actor-avatar { |
820 | @include margin-left(-.4rem); | ||
821 | @include margin-right(.2rem); | 811 | @include margin-right(.2rem); |
812 | |||
813 | border-radius: 5rem; | ||
814 | width: var(--avatar-size); | ||
815 | height: var(--avatar-size); | ||
822 | } | 816 | } |
823 | 817 | ||
824 | &.two-lines { | 818 | &.two-lines { |
825 | $avatar-height: 2rem; | 819 | --avatar-size: 2rem; |
826 | 820 | ||
827 | height: $avatar-height; | 821 | font-size: 14px; |
822 | line-height: 1rem; | ||
828 | 823 | ||
829 | my-actor-avatar { | 824 | my-actor-avatar { |
830 | display: inline-block; | 825 | display: inline-block; |
831 | } | 826 | } |
832 | 827 | ||
833 | div { | 828 | > div { |
834 | margin: 0 .1rem; | ||
835 | |||
836 | display: flex; | 829 | display: flex; |
837 | flex-direction: column; | 830 | flex-direction: column; |
838 | height: $avatar-height; | ||
839 | justify-content: center; | 831 | justify-content: center; |
840 | } | 832 | } |
841 | } | 833 | } |
diff --git a/client/src/sass/player/control-bar.scss b/client/src/sass/player/control-bar.scss index 0082378e4..96b3adf66 100644 --- a/client/src/sass/player/control-bar.scss +++ b/client/src/sass/player/control-bar.scss | |||
@@ -153,8 +153,25 @@ | |||
153 | } | 153 | } |
154 | 154 | ||
155 | .vjs-live-control { | 155 | .vjs-live-control { |
156 | line-height: $control-bar-height; | 156 | padding: 5px 7px; |
157 | min-width: 4em; | 157 | border-radius: 3px; |
158 | height: fit-content; | ||
159 | margin: auto 10px; | ||
160 | font-weight: bold; | ||
161 | max-width: fit-content; | ||
162 | opacity: 1 !important; | ||
163 | line-height: normal; | ||
164 | position: relative; | ||
165 | top: -1px; | ||
166 | |||
167 | &.synced-with-live-edge { | ||
168 | background: #d7281c; | ||
169 | } | ||
170 | |||
171 | &:not(.synced-with-live-edge) { | ||
172 | cursor: pointer; | ||
173 | background: #80807f; | ||
174 | } | ||
158 | } | 175 | } |
159 | 176 | ||
160 | .vjs-peertube { | 177 | .vjs-peertube { |
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index 88f6efb6a..ee66a9db3 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss | |||
@@ -294,6 +294,7 @@ body .p-datepicker .p-datepicker-header .p-datepicker-title select:focus { | |||
294 | body .p-datepicker table { | 294 | body .p-datepicker table { |
295 | font-size: 14px; | 295 | font-size: 14px; |
296 | margin: 0.857em 0 0 0; | 296 | margin: 0.857em 0 0 0; |
297 | table-layout: fixed; | ||
297 | } | 298 | } |
298 | body .p-datepicker table th { | 299 | body .p-datepicker table th { |
299 | padding: 0.5em; | 300 | padding: 0.5em; |
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts index b0bdb2dd9..f09c86d14 100644 --- a/client/src/standalone/videos/shared/player-manager-options.ts +++ b/client/src/standalone/videos/shared/player-manager-options.ts | |||
@@ -38,6 +38,7 @@ export class PlayerManagerOptions { | |||
38 | private enableApi = false | 38 | private enableApi = false |
39 | private startTime: number | string = 0 | 39 | private startTime: number | string = 0 |
40 | private stopTime: number | string | 40 | private stopTime: number | string |
41 | private playbackRate: number | string | ||
41 | 42 | ||
42 | private title: boolean | 43 | private title: boolean |
43 | private warningTitle: boolean | 44 | private warningTitle: boolean |
@@ -130,6 +131,7 @@ export class PlayerManagerOptions { | |||
130 | this.subtitle = getParamString(params, 'subtitle') | 131 | this.subtitle = getParamString(params, 'subtitle') |
131 | this.startTime = getParamString(params, 'start') | 132 | this.startTime = getParamString(params, 'start') |
132 | this.stopTime = getParamString(params, 'stop') | 133 | this.stopTime = getParamString(params, 'stop') |
134 | this.playbackRate = getParamString(params, 'playbackRate') | ||
133 | 135 | ||
134 | this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor') | 136 | this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor') |
135 | this.foregroundColor = getParamString(params, 'foregroundColor') | 137 | this.foregroundColor = getParamString(params, 'foregroundColor') |
@@ -210,6 +212,8 @@ export class PlayerManagerOptions { | |||
210 | ? playlistTracker.getCurrentElement().stopTimestamp | 212 | ? playlistTracker.getCurrentElement().stopTimestamp |
211 | : this.stopTime, | 213 | : this.stopTime, |
212 | 214 | ||
215 | playbackRate: this.playbackRate, | ||
216 | |||
213 | videoCaptions, | 217 | videoCaptions, |
214 | inactivityTimeout: 2500, | 218 | inactivityTimeout: 2500, |
215 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), | 219 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), |
diff --git a/client/yarn.lock b/client/yarn.lock index b680bfdfb..1799df7b1 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -302,6 +302,11 @@ | |||
302 | dependencies: | 302 | dependencies: |
303 | tslib "^2.3.0" | 303 | tslib "^2.3.0" |
304 | 304 | ||
305 | "@arr/every@^1.0.0": | ||
306 | version "1.0.1" | ||
307 | resolved "https://registry.yarnpkg.com/@arr/every/-/every-1.0.1.tgz#22fe1f8e6355beca6c7c7bde965eb15cf994387b" | ||
308 | integrity sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg== | ||
309 | |||
305 | "@assemblyscript/loader@^0.10.1": | 310 | "@assemblyscript/loader@^0.10.1": |
306 | version "0.10.1" | 311 | version "0.10.1" |
307 | resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" | 312 | resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" |
@@ -1789,10 +1794,22 @@ | |||
1789 | read-package-json-fast "^2.0.3" | 1794 | read-package-json-fast "^2.0.3" |
1790 | which "^2.0.2" | 1795 | which "^2.0.2" |
1791 | 1796 | ||
1792 | "@peertube/p2p-media-loader-core@^1.0.13", "@peertube/p2p-media-loader-core@^1.0.8": | 1797 | "@peertube/maildev@^1.2.0": |
1793 | version "1.0.13" | 1798 | version "1.2.0" |
1794 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.13.tgz#36744a291b69c001b2562c1a93017979f8534ff8" | 1799 | resolved "https://registry.yarnpkg.com/@peertube/maildev/-/maildev-1.2.0.tgz#f25ee9fa6a45c0a6bc99c5392f63139eaa8eb088" |
1795 | integrity sha512-ArSAaeuxwwBAG0Xd3Gj0TzKObLfJFYzHz9+fREvmUf+GZQEG6qGwWmrdVWL6xjPiEuo6LdFeCOnHSQzAbj/ptg== | 1800 | integrity sha512-VGog0A2gk0P8UnP0ZjCoYQumELiqqQY5i+gt18avTC7NJNJLUxMRMI045NAVSDFVbqt2EJJPsbZf3LFjUWRtmw== |
1801 | dependencies: | ||
1802 | async "^3.1.0" | ||
1803 | commander "^8.3.0" | ||
1804 | mailparser-mit "^1.0.0" | ||
1805 | rimraf "^3.0.2" | ||
1806 | smtp-server "^3.9.0" | ||
1807 | wildstring "1.0.9" | ||
1808 | |||
1809 | "@peertube/p2p-media-loader-core@^1.0.14": | ||
1810 | version "1.0.14" | ||
1811 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.14.tgz#b4442dd343d6b30a51502e1240275eb98ef2c788" | ||
1812 | integrity sha512-tjQv1CNziNY+zYzcL1h4q6AA2WuBUZnBIeVyjWR/EsO1EEC1VMdvPsL02cqYLz9yvIxgycjeTsWCm6XDqNgXRw== | ||
1796 | dependencies: | 1813 | dependencies: |
1797 | bittorrent-tracker "^9.19.0" | 1814 | bittorrent-tracker "^9.19.0" |
1798 | debug "^4.3.4" | 1815 | debug "^4.3.4" |
@@ -1800,12 +1817,12 @@ | |||
1800 | sha.js "^2.4.11" | 1817 | sha.js "^2.4.11" |
1801 | simple-peer "^9.11.1" | 1818 | simple-peer "^9.11.1" |
1802 | 1819 | ||
1803 | "@peertube/p2p-media-loader-hlsjs@^1.0.13": | 1820 | "@peertube/p2p-media-loader-hlsjs@^1.0.14": |
1804 | version "1.0.13" | 1821 | version "1.0.14" |
1805 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.13.tgz#5305e2008041d01850802544d1c49298f79dd67a" | 1822 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.14.tgz#829629a57608b0e30f4b50bc98578e6bee9f8b9b" |
1806 | integrity sha512-2BO2oaRsSHEhLkgi2iw1r4n1Yqq1EnyoOgOZccPDqjmHUsZSV/wNrno8WYr6LsleudrHA26Imu57hVD1jDx7lg== | 1823 | integrity sha512-ySUVgUvAFXCE5E94xxjfywQ8xzk3jy9UGVkgi5Oqq+QeY7uG+o7CZ+LsQ/RjXgWBD70tEnyyfADHtL+9FCnwyQ== |
1807 | dependencies: | 1824 | dependencies: |
1808 | "@peertube/p2p-media-loader-core" "^1.0.8" | 1825 | "@peertube/p2p-media-loader-core" "^1.0.14" |
1809 | debug "^4.3.4" | 1826 | debug "^4.3.4" |
1810 | events "^3.3.0" | 1827 | events "^3.3.0" |
1811 | m3u8-parser "^4.7.1" | 1828 | m3u8-parser "^4.7.1" |
@@ -1830,6 +1847,16 @@ | |||
1830 | tokenizr "^1.6.4" | 1847 | tokenizr "^1.6.4" |
1831 | xmldom "^0.6.0" | 1848 | xmldom "^0.6.0" |
1832 | 1849 | ||
1850 | "@polka/parse@^1.0.0-next.0": | ||
1851 | version "1.0.0-next.0" | ||
1852 | resolved "https://registry.yarnpkg.com/@polka/parse/-/parse-1.0.0-next.0.tgz#3551d792acdf4ad0b053072e57498cbe32e45a94" | ||
1853 | integrity sha512-zcPNrc3PNrRLSCQ7ca8XR7h18VxdPIXhn+yvrYMdUFCHM7mhXGSPw5xBdbcf/dQ1cI4uE8pDfmm5uU+HX+WfFg== | ||
1854 | |||
1855 | "@polka/url@^0.5.0": | ||
1856 | version "0.5.0" | ||
1857 | resolved "https://registry.yarnpkg.com/@polka/url/-/url-0.5.0.tgz#b21510597fd601e5d7c95008b76bf0d254ebfd31" | ||
1858 | integrity sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw== | ||
1859 | |||
1833 | "@polka/url@^1.0.0-next.20": | 1860 | "@polka/url@^1.0.0-next.20": |
1834 | version "1.0.0-next.21" | 1861 | version "1.0.0-next.21" |
1835 | resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" | 1862 | resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" |
@@ -2021,6 +2048,11 @@ | |||
2021 | dependencies: | 2048 | dependencies: |
2022 | "@types/node" "*" | 2049 | "@types/node" "*" |
2023 | 2050 | ||
2051 | "@types/gitconfiglocal@^2.0.1": | ||
2052 | version "2.0.1" | ||
2053 | resolved "https://registry.yarnpkg.com/@types/gitconfiglocal/-/gitconfiglocal-2.0.1.tgz#c134f9fb03d71917afa35c14f3b82085520509a6" | ||
2054 | integrity sha512-AYC38la5dRwIfbrZhPNIvlGHlIbH+kdl2j8A37twoCQyhKPPoRPfVmoBZKajpLIfV7SMboU6MZ6w/RmZLH68IQ== | ||
2055 | |||
2024 | "@types/html-minifier-terser@^6.0.0": | 2056 | "@types/html-minifier-terser@^6.0.0": |
2025 | version "6.1.0" | 2057 | version "6.1.0" |
2026 | resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" | 2058 | resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" |
@@ -2127,9 +2159,9 @@ | |||
2127 | "@types/lodash" "*" | 2159 | "@types/lodash" "*" |
2128 | 2160 | ||
2129 | "@types/lodash@*": | 2161 | "@types/lodash@*": |
2130 | version "4.14.189" | 2162 | version "4.14.191" |
2131 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.189.tgz#975ff8c38da5ae58b751127b19ad5e44b5b7f6d2" | 2163 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" |
2132 | integrity sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA== | 2164 | integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== |
2133 | 2165 | ||
2134 | "@types/magnet-uri@*": | 2166 | "@types/magnet-uri@*": |
2135 | version "5.1.3" | 2167 | version "5.1.3" |
@@ -2162,9 +2194,9 @@ | |||
2162 | integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== | 2194 | integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== |
2163 | 2195 | ||
2164 | "@types/mocha@^10.0.0": | 2196 | "@types/mocha@^10.0.0": |
2165 | version "10.0.0" | 2197 | version "10.0.1" |
2166 | resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.0.tgz#3d9018c575f0e3f7386c1de80ee66cc21fbb7a52" | 2198 | resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b" |
2167 | integrity sha512-rADY+HtTOA52l9VZWtgQfn4p+UDVM2eDVkMZT1I6syp0YKxW2F9v+0pbRZLsvskhQv/vMb6ZfCay81GHbz5SHg== | 2199 | integrity sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q== |
2168 | 2200 | ||
2169 | "@types/mousetrap@^1.6.9": | 2201 | "@types/mousetrap@^1.6.9": |
2170 | version "1.6.11" | 2202 | version "1.6.11" |
@@ -2177,9 +2209,9 @@ | |||
2177 | integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== | 2209 | integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== |
2178 | 2210 | ||
2179 | "@types/node@*", "@types/node@^18.0.0": | 2211 | "@types/node@*", "@types/node@^18.0.0": |
2180 | version "18.11.9" | 2212 | version "18.11.18" |
2181 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" | 2213 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" |
2182 | integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== | 2214 | integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== |
2183 | 2215 | ||
2184 | "@types/node@^17.0.42": | 2216 | "@types/node@^17.0.42": |
2185 | version "17.0.45" | 2217 | version "17.0.45" |
@@ -2380,9 +2412,9 @@ | |||
2380 | integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== | 2412 | integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== |
2381 | 2413 | ||
2382 | "@types/yargs@^17.0.8": | 2414 | "@types/yargs@^17.0.8": |
2383 | version "17.0.13" | 2415 | version "17.0.19" |
2384 | resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" | 2416 | resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.19.tgz#8dbecdc9ab48bee0cb74f6e3327de3fa0d0c98ae" |
2385 | integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg== | 2417 | integrity sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ== |
2386 | dependencies: | 2418 | dependencies: |
2387 | "@types/yargs-parser" "*" | 2419 | "@types/yargs-parser" "*" |
2388 | 2420 | ||
@@ -2509,22 +2541,26 @@ | |||
2509 | is-function "^1.0.1" | 2541 | is-function "^1.0.1" |
2510 | 2542 | ||
2511 | "@wdio/browserstack-service@^7.25.2": | 2543 | "@wdio/browserstack-service@^7.25.2": |
2512 | version "7.26.0" | 2544 | version "7.29.1" |
2513 | resolved "https://registry.yarnpkg.com/@wdio/browserstack-service/-/browserstack-service-7.26.0.tgz#d303c5998e565734bd7f5c23fc9b291a588b7c21" | 2545 | resolved "https://registry.yarnpkg.com/@wdio/browserstack-service/-/browserstack-service-7.29.1.tgz#46282aa07b7c11a51ebac0bff1f12f1badd6e264" |
2514 | integrity sha512-hRKmg4u/DRNZm1EJGaYESAH6GsCPCtBm15fP9ngm/HFUG084thFfrD8Tt09hO+KSNoK4tXl4k1ZHZ4akrOq9KA== | 2546 | integrity sha512-1+MoqlIXIjbh1oEOZcvtemij+Yz/CB6orZjeT3WCoA9oY8Ul8EeIHhfF7GxmE6u0OVofjmC+wfO5NlHYCKgL1w== |
2515 | dependencies: | 2547 | dependencies: |
2516 | "@types/node" "^18.0.0" | 2548 | "@types/gitconfiglocal" "^2.0.1" |
2517 | "@wdio/logger" "7.26.0" | 2549 | "@wdio/logger" "7.26.0" |
2550 | "@wdio/reporter" "7.25.4" | ||
2518 | "@wdio/types" "7.26.0" | 2551 | "@wdio/types" "7.26.0" |
2519 | browserstack-local "^1.4.5" | 2552 | browserstack-local "^1.4.5" |
2520 | form-data "^4.0.0" | 2553 | form-data "^4.0.0" |
2554 | git-repo-info "^2.1.1" | ||
2555 | gitconfiglocal "^2.1.0" | ||
2521 | got "^11.0.2" | 2556 | got "^11.0.2" |
2522 | webdriverio "7.26.0" | 2557 | uuid "^8.3.2" |
2558 | webdriverio "7.29.1" | ||
2523 | 2559 | ||
2524 | "@wdio/cli@^7.25.2": | 2560 | "@wdio/cli@^7.25.2": |
2525 | version "7.26.0" | 2561 | version "7.29.1" |
2526 | resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-7.26.0.tgz#20c690a5ede4a35cb2f84da9041c250a6013bc54" | 2562 | resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-7.29.1.tgz#1b47f5a45f21754d42be814dbae94ff723a6a1a2" |
2527 | integrity sha512-xG+ZIzPqzz/Tvhfrogd8oNvTXzzdE+cbkmTHjMGo1hnmnoAQPeAEcV/QqaX5CHFE9DjaguEeadqjcZikB5U2GQ== | 2563 | integrity sha512-dldHNYlnuFUG10TlENbeL41tujqgYD7S/9nzV1J/szBryCO6AIVz/QWn/AUv3zrsO2sn8TNF8BMEXRvLgCxyeg== |
2528 | dependencies: | 2564 | dependencies: |
2529 | "@types/ejs" "^3.0.5" | 2565 | "@types/ejs" "^3.0.5" |
2530 | "@types/fs-extra" "^9.0.4" | 2566 | "@types/fs-extra" "^9.0.4" |
@@ -2536,7 +2572,7 @@ | |||
2536 | "@types/recursive-readdir" "^2.2.0" | 2572 | "@types/recursive-readdir" "^2.2.0" |
2537 | "@wdio/config" "7.26.0" | 2573 | "@wdio/config" "7.26.0" |
2538 | "@wdio/logger" "7.26.0" | 2574 | "@wdio/logger" "7.26.0" |
2539 | "@wdio/protocols" "7.22.0" | 2575 | "@wdio/protocols" "7.27.0" |
2540 | "@wdio/types" "7.26.0" | 2576 | "@wdio/types" "7.26.0" |
2541 | "@wdio/utils" "7.26.0" | 2577 | "@wdio/utils" "7.26.0" |
2542 | async-exit-hook "^2.0.1" | 2578 | async-exit-hook "^2.0.1" |
@@ -2551,7 +2587,7 @@ | |||
2551 | lodash.union "^4.6.0" | 2587 | lodash.union "^4.6.0" |
2552 | mkdirp "^1.0.4" | 2588 | mkdirp "^1.0.4" |
2553 | recursive-readdir "^2.2.2" | 2589 | recursive-readdir "^2.2.2" |
2554 | webdriverio "7.26.0" | 2590 | webdriverio "7.29.1" |
2555 | yargs "^17.0.0" | 2591 | yargs "^17.0.0" |
2556 | yarn-install "^1.0.0" | 2592 | yarn-install "^1.0.0" |
2557 | 2593 | ||
@@ -2567,14 +2603,14 @@ | |||
2567 | glob "^8.0.3" | 2603 | glob "^8.0.3" |
2568 | 2604 | ||
2569 | "@wdio/local-runner@^7.25.2": | 2605 | "@wdio/local-runner@^7.25.2": |
2570 | version "7.26.0" | 2606 | version "7.29.1" |
2571 | resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-7.26.0.tgz#a056c6e9d73c7f48e54fe3f07ce573a90dae26ab" | 2607 | resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-7.29.1.tgz#f93a2953847b4271b59ba1b9635920e8046f0e55" |
2572 | integrity sha512-GdCP7Y8s8qvoctC0WaSGBSmTSbVw74WEJm6Y3n3DpoCI8ABFNkQlhFlqJH+taQDs3sRVEM65bHGcU4C4FOVWXQ== | 2608 | integrity sha512-4w9Dsp9/4+MEU8yG7M8ynsCqpSP6UbKqZ2M/gWpvkvy57rb3eS9evFdIFfRzuQmbsztG9qeAlGILwlZ4/oaopg== |
2573 | dependencies: | 2609 | dependencies: |
2574 | "@types/stream-buffers" "^3.0.3" | 2610 | "@types/stream-buffers" "^3.0.3" |
2575 | "@wdio/logger" "7.26.0" | 2611 | "@wdio/logger" "7.26.0" |
2576 | "@wdio/repl" "7.26.0" | 2612 | "@wdio/repl" "7.26.0" |
2577 | "@wdio/runner" "7.26.0" | 2613 | "@wdio/runner" "7.29.1" |
2578 | "@wdio/types" "7.26.0" | 2614 | "@wdio/types" "7.26.0" |
2579 | async-exit-hook "^2.0.1" | 2615 | async-exit-hook "^2.0.1" |
2580 | split2 "^4.0.0" | 2616 | split2 "^4.0.0" |
@@ -2602,10 +2638,10 @@ | |||
2602 | expect-webdriverio "^3.0.0" | 2638 | expect-webdriverio "^3.0.0" |
2603 | mocha "^10.0.0" | 2639 | mocha "^10.0.0" |
2604 | 2640 | ||
2605 | "@wdio/protocols@7.22.0": | 2641 | "@wdio/protocols@7.27.0": |
2606 | version "7.22.0" | 2642 | version "7.27.0" |
2607 | resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-7.22.0.tgz#d89faef687cb08981d734bbc5e5dffc6fb5a064c" | 2643 | resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-7.27.0.tgz#8e2663ec877dce7a5f76b021209c18dd0132e853" |
2608 | integrity sha512-8EXRR+Ymdwousm/VGtW3H1hwxZ/1g1H99A1lF0U4GuJ5cFWHCd0IVE5H31Z52i8ZruouW8jueMkGZPSo2IIUSQ== | 2644 | integrity sha512-hT/U22R5i3HhwPjkaKAG0yd59eaOaZB0eibRj2+esCImkb5Y6rg8FirrlYRxIGFVBl0+xZV0jKHzR5+o097nvg== |
2609 | 2645 | ||
2610 | "@wdio/repl@7.26.0": | 2646 | "@wdio/repl@7.26.0": |
2611 | version "7.26.0" | 2647 | version "7.26.0" |
@@ -2614,10 +2650,26 @@ | |||
2614 | dependencies: | 2650 | dependencies: |
2615 | "@wdio/utils" "7.26.0" | 2651 | "@wdio/utils" "7.26.0" |
2616 | 2652 | ||
2617 | "@wdio/reporter@7.26.0": | 2653 | "@wdio/reporter@7.25.4": |
2618 | version "7.26.0" | 2654 | version "7.25.4" |
2619 | resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.26.0.tgz#26c0e7114a4c1e7b29a79e4d178e5312e04d7934" | 2655 | resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.25.4.tgz#b6a69652dd0c4ec131255000af128eac403a18b9" |
2620 | integrity sha512-kEb7i1A4V4E1wJgdyvLsDbap4cEp1fPZslErGtbAbK+9HI8Lt/SlTZCiOpZbvhgzvawEqOV6UqxZT1RsL8wZWw== | 2656 | integrity sha512-M37qzEmF5qNffyZmRQGjDlrXqWW21EFvgW8wsv1b/NtfpZc0c0MoRpeh6BnvX1KcE4nCXfjXgSJPOqV4ZCzUEQ== |
2657 | dependencies: | ||
2658 | "@types/diff" "^5.0.0" | ||
2659 | "@types/node" "^18.0.0" | ||
2660 | "@types/object-inspect" "^1.8.0" | ||
2661 | "@types/supports-color" "^8.1.0" | ||
2662 | "@types/tmp" "^0.2.0" | ||
2663 | "@wdio/types" "7.25.4" | ||
2664 | diff "^5.0.0" | ||
2665 | fs-extra "^10.0.0" | ||
2666 | object-inspect "^1.10.3" | ||
2667 | supports-color "8.1.1" | ||
2668 | |||
2669 | "@wdio/reporter@7.29.1": | ||
2670 | version "7.29.1" | ||
2671 | resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.29.1.tgz#7fc2e3b7aa3843172dcd97221c44257384cbbd27" | ||
2672 | integrity sha512-mpusCpbw7RxnJSDu9qa1qv5IfEMCh7377y1Typ4J2TlMy+78CQzGZ8coEXjBxLcqijTUwcyyoLNI5yRSvbDExw== | ||
2621 | dependencies: | 2673 | dependencies: |
2622 | "@types/diff" "^5.0.0" | 2674 | "@types/diff" "^5.0.0" |
2623 | "@types/node" "^18.0.0" | 2675 | "@types/node" "^18.0.0" |
@@ -2630,10 +2682,10 @@ | |||
2630 | object-inspect "^1.10.3" | 2682 | object-inspect "^1.10.3" |
2631 | supports-color "8.1.1" | 2683 | supports-color "8.1.1" |
2632 | 2684 | ||
2633 | "@wdio/runner@7.26.0": | 2685 | "@wdio/runner@7.29.1": |
2634 | version "7.26.0" | 2686 | version "7.29.1" |
2635 | resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-7.26.0.tgz#c0b2848dc885b655e8690d3e0381dfb0ad221af5" | 2687 | resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-7.29.1.tgz#9fd2fa6dd28b8b130a10d23452eb155e1e887576" |
2636 | integrity sha512-DhQiOs10oPeLlv7/R+997arPg5OY7iEgespGkn6r+kdx2o+awxa6PFegQrjJmRKUmNv3TTuKXHouP34TbR/8sw== | 2688 | integrity sha512-lJEk/HJ5IiuvAJws8zTx9XL5LJuoexvjWIZmOmFJ6Gv8qRpUx6b0n+JM7vhhbTeIqs4QLXOwTQUHlDDRldQlzQ== |
2637 | dependencies: | 2689 | dependencies: |
2638 | "@wdio/config" "7.26.0" | 2690 | "@wdio/config" "7.26.0" |
2639 | "@wdio/logger" "7.26.0" | 2691 | "@wdio/logger" "7.26.0" |
@@ -2641,21 +2693,41 @@ | |||
2641 | "@wdio/utils" "7.26.0" | 2693 | "@wdio/utils" "7.26.0" |
2642 | deepmerge "^4.0.0" | 2694 | deepmerge "^4.0.0" |
2643 | gaze "^1.1.2" | 2695 | gaze "^1.1.2" |
2644 | webdriver "7.26.0" | 2696 | webdriver "7.27.0" |
2645 | webdriverio "7.26.0" | 2697 | webdriverio "7.29.1" |
2698 | |||
2699 | "@wdio/shared-store-service@^7.25.2": | ||
2700 | version "7.29.1" | ||
2701 | resolved "https://registry.yarnpkg.com/@wdio/shared-store-service/-/shared-store-service-7.29.1.tgz#c43a3dbc7d47c8334970bc173e963688977e8a79" | ||
2702 | integrity sha512-13VOxyz956DSs2wloQ8gtyEx42zjAuOg+N8/4tGk1p2igPzHB2qUiY/P0yi6zamxYGb6PKLIumIeUjitWHtyWA== | ||
2703 | dependencies: | ||
2704 | "@polka/parse" "^1.0.0-next.0" | ||
2705 | "@wdio/logger" "7.26.0" | ||
2706 | "@wdio/types" "7.26.0" | ||
2707 | got "^11.0.2" | ||
2708 | polka "^0.5.2" | ||
2709 | webdriverio "7.29.1" | ||
2646 | 2710 | ||
2647 | "@wdio/spec-reporter@^7.25.1": | 2711 | "@wdio/spec-reporter@^7.25.1": |
2648 | version "7.26.0" | 2712 | version "7.29.1" |
2649 | resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-7.26.0.tgz#13eaa5a0fd089684d4c1bcd8ac11dc8646afb5b7" | 2713 | resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-7.29.1.tgz#08e13c02ea0876672226d5a2c326dda7e1a66c8e" |
2650 | integrity sha512-oisyVWn+MRoq0We0qORoDHNk+iKr7CFG4+IE5GCRecR8cgP7dUjVXZcEbn6blgRpry4jOxsAl24frfaPDOsZVA== | 2714 | integrity sha512-bwSGM72QrDedqacY7Wq9Gn86VgRwIGPYzZtcaD7aDnvppCuV8Z/31Wpdfen+CzUk2+whXjXKe66ohPyl9TG5+w== |
2651 | dependencies: | 2715 | dependencies: |
2652 | "@types/easy-table" "^1.2.0" | 2716 | "@types/easy-table" "^1.2.0" |
2653 | "@wdio/reporter" "7.26.0" | 2717 | "@wdio/reporter" "7.29.1" |
2654 | "@wdio/types" "7.26.0" | 2718 | "@wdio/types" "7.26.0" |
2655 | chalk "^4.0.0" | 2719 | chalk "^4.0.0" |
2656 | easy-table "^1.1.1" | 2720 | easy-table "^1.1.1" |
2657 | pretty-ms "^7.0.0" | 2721 | pretty-ms "^7.0.0" |
2658 | 2722 | ||
2723 | "@wdio/types@7.25.4": | ||
2724 | version "7.25.4" | ||
2725 | resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.25.4.tgz#6f8f028e3108dc880de5068264695f1572e65352" | ||
2726 | integrity sha512-muvNmq48QZCvocctnbe0URq2FjJjUPIG4iLoeMmyF0AQgdbjaUkMkw3BHYNHVTbSOU9WMsr2z8alhj/I2H6NRQ== | ||
2727 | dependencies: | ||
2728 | "@types/node" "^18.0.0" | ||
2729 | got "^11.8.1" | ||
2730 | |||
2659 | "@wdio/types@7.26.0": | 2731 | "@wdio/types@7.26.0": |
2660 | version "7.26.0" | 2732 | version "7.26.0" |
2661 | resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.26.0.tgz#70bc879c5dbe316a0eebbac4a46f0f66430b1d84" | 2733 | resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.26.0.tgz#70bc879c5dbe316a0eebbac4a46f0f66430b1d84" |
@@ -2882,6 +2954,11 @@ addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.5.4: | |||
2882 | resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.4.tgz#9542b1c6219fdb8c9ce6cc72c14ee880ab7ddd88" | 2954 | resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.4.tgz#9542b1c6219fdb8c9ce6cc72c14ee880ab7ddd88" |
2883 | integrity sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg== | 2955 | integrity sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg== |
2884 | 2956 | ||
2957 | addressparser@^1.0.1: | ||
2958 | version "1.0.1" | ||
2959 | resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746" | ||
2960 | integrity sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg== | ||
2961 | |||
2885 | adjust-sourcemap-loader@^4.0.0: | 2962 | adjust-sourcemap-loader@^4.0.0: |
2886 | version "4.0.0" | 2963 | version "4.0.0" |
2887 | resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" | 2964 | resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" |
@@ -3057,9 +3134,9 @@ ansi-styles@^5.0.0: | |||
3057 | integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== | 3134 | integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== |
3058 | 3135 | ||
3059 | anymatch@~3.1.2: | 3136 | anymatch@~3.1.2: |
3060 | version "3.1.2" | 3137 | version "3.1.3" |
3061 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" | 3138 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" |
3062 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== | 3139 | integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== |
3063 | dependencies: | 3140 | dependencies: |
3064 | normalize-path "^3.0.0" | 3141 | normalize-path "^3.0.0" |
3065 | picomatch "^2.0.4" | 3142 | picomatch "^2.0.4" |
@@ -3188,7 +3265,7 @@ async-exit-hook@^2.0.1: | |||
3188 | resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" | 3265 | resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" |
3189 | integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== | 3266 | integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== |
3190 | 3267 | ||
3191 | async@^3.2.3: | 3268 | async@^3.1.0, async@^3.2.3: |
3192 | version "3.2.4" | 3269 | version "3.2.4" |
3193 | resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" | 3270 | resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" |
3194 | integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== | 3271 | integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== |
@@ -3327,6 +3404,11 @@ balanced-match@^2.0.0: | |||
3327 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" | 3404 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" |
3328 | integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== | 3405 | integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== |
3329 | 3406 | ||
3407 | base32.js@0.1.0: | ||
3408 | version "0.1.0" | ||
3409 | resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.1.0.tgz#b582dec693c2f11e893cf064ee6ac5b6131a2202" | ||
3410 | integrity sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ== | ||
3411 | |||
3330 | base64-js@^1.2.0, base64-js@^1.3.1: | 3412 | base64-js@^1.2.0, base64-js@^1.3.1: |
3331 | version "1.5.1" | 3413 | version "1.5.1" |
3332 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" | 3414 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" |
@@ -3933,9 +4015,9 @@ chunk-store-stream@^4.3.0: | |||
3933 | readable-stream "^3.6.0" | 4015 | readable-stream "^3.6.0" |
3934 | 4016 | ||
3935 | ci-info@^3.2.0: | 4017 | ci-info@^3.2.0: |
3936 | version "3.6.1" | 4018 | version "3.7.1" |
3937 | resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.1.tgz#7594f1c95cb7fdfddee7af95a13af7dbc67afdcf" | 4019 | resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" |
3938 | integrity sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w== | 4020 | integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== |
3939 | 4021 | ||
3940 | clean-css@5.2.0: | 4022 | clean-css@5.2.0: |
3941 | version "5.2.0" | 4023 | version "5.2.0" |
@@ -4504,16 +4586,18 @@ decompress-response@^6.0.0: | |||
4504 | mimic-response "^3.1.0" | 4586 | mimic-response "^3.1.0" |
4505 | 4587 | ||
4506 | deep-equal@^2.0.5: | 4588 | deep-equal@^2.0.5: |
4507 | version "2.1.0" | 4589 | version "2.2.0" |
4508 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.1.0.tgz#5ba60402cf44ab92c2c07f3f3312c3d857a0e1dd" | 4590 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6" |
4509 | integrity sha512-2pxgvWu3Alv1PoWEyVg7HS8YhGlUFUV7N5oOvfL6d+7xAmLSemMwv/c8Zv/i9KFzxV5Kt5CAvQc70fLwVuf4UA== | 4591 | integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== |
4510 | dependencies: | 4592 | dependencies: |
4511 | call-bind "^1.0.2" | 4593 | call-bind "^1.0.2" |
4512 | es-get-iterator "^1.1.2" | 4594 | es-get-iterator "^1.1.2" |
4513 | get-intrinsic "^1.1.3" | 4595 | get-intrinsic "^1.1.3" |
4514 | is-arguments "^1.1.1" | 4596 | is-arguments "^1.1.1" |
4597 | is-array-buffer "^3.0.1" | ||
4515 | is-date-object "^1.0.5" | 4598 | is-date-object "^1.0.5" |
4516 | is-regex "^1.1.4" | 4599 | is-regex "^1.1.4" |
4600 | is-shared-array-buffer "^1.0.2" | ||
4517 | isarray "^2.0.5" | 4601 | isarray "^2.0.5" |
4518 | object-is "^1.1.5" | 4602 | object-is "^1.1.5" |
4519 | object-keys "^1.1.1" | 4603 | object-keys "^1.1.1" |
@@ -4522,7 +4606,7 @@ deep-equal@^2.0.5: | |||
4522 | side-channel "^1.0.4" | 4606 | side-channel "^1.0.4" |
4523 | which-boxed-primitive "^1.0.2" | 4607 | which-boxed-primitive "^1.0.2" |
4524 | which-collection "^1.0.1" | 4608 | which-collection "^1.0.1" |
4525 | which-typed-array "^1.1.8" | 4609 | which-typed-array "^1.1.9" |
4526 | 4610 | ||
4527 | deep-is@^0.1.3: | 4611 | deep-is@^0.1.3: |
4528 | version "0.1.4" | 4612 | version "0.1.4" |
@@ -4606,21 +4690,21 @@ devtools-protocol@0.0.981744: | |||
4606 | resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf" | 4690 | resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf" |
4607 | integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg== | 4691 | integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg== |
4608 | 4692 | ||
4609 | devtools-protocol@^0.0.1069585: | 4693 | devtools-protocol@^0.0.1085790: |
4610 | version "0.0.1069585" | 4694 | version "0.0.1085790" |
4611 | resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1069585.tgz#c9a9f330462aabf054d581f254b13774297b84f2" | 4695 | resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1085790.tgz#315e4700eb960cf111cc908b9be2caca2257cb13" |
4612 | integrity sha512-sHmkZB6immWQWU4Wx3ogXwxjQUvQc92MmUDL52+q1z2hQmvpOcvDmbsjwX7QZOPTA32dMV7fgT6zUytcpPzy4A== | 4696 | integrity sha512-f5kfwdOTxPqX5v8ZfAAl9xBgoEVazBYtIONDWIRqYbb7yjOIcnk6vpzCgBCQvav5AuBRLzyUGG0V74OAx93LoA== |
4613 | 4697 | ||
4614 | devtools@7.26.0: | 4698 | devtools@7.28.1: |
4615 | version "7.26.0" | 4699 | version "7.28.1" |
4616 | resolved "https://registry.yarnpkg.com/devtools/-/devtools-7.26.0.tgz#3d568aea2238d190ad0cd71c00483c07c707124a" | 4700 | resolved "https://registry.yarnpkg.com/devtools/-/devtools-7.28.1.tgz#9699e0ca41c9a3adfa351d8afac2928f8e1d381c" |
4617 | integrity sha512-+8HNbNpzgo4Sn+WcrvXuwsHW9XPJfLo4bs9lgs6DPJHIIDXYJXQGsd7940wMX0Rp0D2vHXA4ibK0oTI5rogM3Q== | 4701 | integrity sha512-sDoszzrXDMLiBQqsg9A5gDqDBwhH4sjYzJIW15lQinB8qgNs0y4o1zdfNlqiKs4HstCA2uFixQeibbDCyMa7hQ== |
4618 | dependencies: | 4702 | dependencies: |
4619 | "@types/node" "^18.0.0" | 4703 | "@types/node" "^18.0.0" |
4620 | "@types/ua-parser-js" "^0.7.33" | 4704 | "@types/ua-parser-js" "^0.7.33" |
4621 | "@wdio/config" "7.26.0" | 4705 | "@wdio/config" "7.26.0" |
4622 | "@wdio/logger" "7.26.0" | 4706 | "@wdio/logger" "7.26.0" |
4623 | "@wdio/protocols" "7.22.0" | 4707 | "@wdio/protocols" "7.27.0" |
4624 | "@wdio/types" "7.26.0" | 4708 | "@wdio/types" "7.26.0" |
4625 | "@wdio/utils" "7.26.0" | 4709 | "@wdio/utils" "7.26.0" |
4626 | chrome-launcher "^0.15.0" | 4710 | chrome-launcher "^0.15.0" |
@@ -4923,18 +5007,19 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: | |||
4923 | unbox-primitive "^1.0.2" | 5007 | unbox-primitive "^1.0.2" |
4924 | 5008 | ||
4925 | es-get-iterator@^1.1.2: | 5009 | es-get-iterator@^1.1.2: |
4926 | version "1.1.2" | 5010 | version "1.1.3" |
4927 | resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" | 5011 | resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" |
4928 | integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ== | 5012 | integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== |
4929 | dependencies: | 5013 | dependencies: |
4930 | call-bind "^1.0.2" | 5014 | call-bind "^1.0.2" |
4931 | get-intrinsic "^1.1.0" | 5015 | get-intrinsic "^1.1.3" |
4932 | has-symbols "^1.0.1" | 5016 | has-symbols "^1.0.3" |
4933 | is-arguments "^1.1.0" | 5017 | is-arguments "^1.1.1" |
4934 | is-map "^2.0.2" | 5018 | is-map "^2.0.2" |
4935 | is-set "^2.0.2" | 5019 | is-set "^2.0.2" |
4936 | is-string "^1.0.5" | 5020 | is-string "^1.0.7" |
4937 | isarray "^2.0.5" | 5021 | isarray "^2.0.5" |
5022 | stop-iteration-iterator "^1.0.0" | ||
4938 | 5023 | ||
4939 | es-module-lexer@^0.9.0: | 5024 | es-module-lexer@^0.9.0: |
4940 | version "0.9.3" | 5025 | version "0.9.3" |
@@ -5104,7 +5189,7 @@ escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: | |||
5104 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" | 5189 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" |
5105 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== | 5190 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== |
5106 | 5191 | ||
5107 | escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: | 5192 | escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5, escape-string-regexp@~1.0.5: |
5108 | version "1.0.5" | 5193 | version "1.0.5" |
5109 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" | 5194 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" |
5110 | integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== | 5195 | integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== |
@@ -5404,7 +5489,7 @@ express@^4.17.3: | |||
5404 | utils-merge "1.0.1" | 5489 | utils-merge "1.0.1" |
5405 | vary "~1.1.2" | 5490 | vary "~1.1.2" |
5406 | 5491 | ||
5407 | extend@~3.0.2: | 5492 | extend@~3.0.0, extend@~3.0.2: |
5408 | version "3.0.2" | 5493 | version "3.0.2" |
5409 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" | 5494 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" |
5410 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== | 5495 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== |
@@ -5856,6 +5941,18 @@ getpass@^0.1.1: | |||
5856 | dependencies: | 5941 | dependencies: |
5857 | assert-plus "^1.0.0" | 5942 | assert-plus "^1.0.0" |
5858 | 5943 | ||
5944 | git-repo-info@^2.1.1: | ||
5945 | version "2.1.1" | ||
5946 | resolved "https://registry.yarnpkg.com/git-repo-info/-/git-repo-info-2.1.1.tgz#220ffed8cbae74ef8a80e3052f2ccb5179aed058" | ||
5947 | integrity sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg== | ||
5948 | |||
5949 | gitconfiglocal@^2.1.0: | ||
5950 | version "2.1.0" | ||
5951 | resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-2.1.0.tgz#07c28685c55cc5338b27b5acbcfe34aeb92e43d1" | ||
5952 | integrity sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg== | ||
5953 | dependencies: | ||
5954 | ini "^1.3.2" | ||
5955 | |||
5859 | glob-parent@^5.1.2, glob-parent@~5.1.2: | 5956 | glob-parent@^5.1.2, glob-parent@~5.1.2: |
5860 | version "5.1.2" | 5957 | version "5.1.2" |
5861 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" | 5958 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" |
@@ -5887,7 +5984,7 @@ glob@7.2.0: | |||
5887 | once "^1.3.0" | 5984 | once "^1.3.0" |
5888 | path-is-absolute "^1.0.0" | 5985 | path-is-absolute "^1.0.0" |
5889 | 5986 | ||
5890 | glob@8.0.3, glob@^8.0.1, glob@^8.0.3: | 5987 | glob@8.0.3, glob@^8.0.1: |
5891 | version "8.0.3" | 5988 | version "8.0.3" |
5892 | resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" | 5989 | resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" |
5893 | integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== | 5990 | integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== |
@@ -5910,6 +6007,17 @@ glob@^7.0.5, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: | |||
5910 | once "^1.3.0" | 6007 | once "^1.3.0" |
5911 | path-is-absolute "^1.0.0" | 6008 | path-is-absolute "^1.0.0" |
5912 | 6009 | ||
6010 | glob@^8.0.3: | ||
6011 | version "8.1.0" | ||
6012 | resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" | ||
6013 | integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== | ||
6014 | dependencies: | ||
6015 | fs.realpath "^1.0.0" | ||
6016 | inflight "^1.0.4" | ||
6017 | inherits "2" | ||
6018 | minimatch "^5.0.1" | ||
6019 | once "^1.3.0" | ||
6020 | |||
5913 | glob@~7.1.1: | 6021 | glob@~7.1.1: |
5914 | version "7.1.7" | 6022 | version "7.1.7" |
5915 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" | 6023 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" |
@@ -6002,7 +6110,7 @@ gopd@^1.0.1: | |||
6002 | dependencies: | 6110 | dependencies: |
6003 | get-intrinsic "^1.1.3" | 6111 | get-intrinsic "^1.1.3" |
6004 | 6112 | ||
6005 | got@11.8.5, got@^11.0.2, got@^11.8.1: | 6113 | got@11.8.5: |
6006 | version "11.8.5" | 6114 | version "11.8.5" |
6007 | resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" | 6115 | resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" |
6008 | integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== | 6116 | integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== |
@@ -6019,6 +6127,23 @@ got@11.8.5, got@^11.0.2, got@^11.8.1: | |||
6019 | p-cancelable "^2.0.0" | 6127 | p-cancelable "^2.0.0" |
6020 | responselike "^2.0.0" | 6128 | responselike "^2.0.0" |
6021 | 6129 | ||
6130 | got@^11.0.2, got@^11.8.1: | ||
6131 | version "11.8.6" | ||
6132 | resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" | ||
6133 | integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== | ||
6134 | dependencies: | ||
6135 | "@sindresorhus/is" "^4.0.0" | ||
6136 | "@szmarczak/http-timer" "^4.0.5" | ||
6137 | "@types/cacheable-request" "^6.0.1" | ||
6138 | "@types/responselike" "^1.0.0" | ||
6139 | cacheable-lookup "^5.0.3" | ||
6140 | cacheable-request "^7.0.2" | ||
6141 | decompress-response "^6.0.0" | ||
6142 | http2-wrapper "^1.0.0-beta.5.2" | ||
6143 | lowercase-keys "^2.0.0" | ||
6144 | p-cancelable "^2.0.0" | ||
6145 | responselike "^2.0.0" | ||
6146 | |||
6022 | graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: | 6147 | graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: |
6023 | version "4.2.10" | 6148 | version "4.2.10" |
6024 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" | 6149 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" |
@@ -6093,7 +6218,7 @@ has-property-descriptors@^1.0.0: | |||
6093 | dependencies: | 6218 | dependencies: |
6094 | get-intrinsic "^1.1.1" | 6219 | get-intrinsic "^1.1.1" |
6095 | 6220 | ||
6096 | has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: | 6221 | has-symbols@^1.0.2, has-symbols@^1.0.3: |
6097 | version "1.0.3" | 6222 | version "1.0.3" |
6098 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" | 6223 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" |
6099 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== | 6224 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== |
@@ -6347,7 +6472,7 @@ humanize-ms@^1.2.1: | |||
6347 | dependencies: | 6472 | dependencies: |
6348 | ms "^2.0.0" | 6473 | ms "^2.0.0" |
6349 | 6474 | ||
6350 | iconv-lite@0.4.24, iconv-lite@^0.4.24: | 6475 | iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.24: |
6351 | version "0.4.24" | 6476 | version "0.4.24" |
6352 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" | 6477 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" |
6353 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== | 6478 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== |
@@ -6469,7 +6594,7 @@ ini@3.0.0: | |||
6469 | resolved "https://registry.yarnpkg.com/ini/-/ini-3.0.0.tgz#2f6de95006923aa75feed8894f5686165adc08f1" | 6594 | resolved "https://registry.yarnpkg.com/ini/-/ini-3.0.0.tgz#2f6de95006923aa75feed8894f5686165adc08f1" |
6470 | integrity sha512-TxYQaeNW/N8ymDvwAxPyRbhMBtnEwuvaTYpOQkFx1nSeusgezHniEc/l35Vo4iCq/mMiTJbpD7oYxN98hFlfmw== | 6595 | integrity sha512-TxYQaeNW/N8ymDvwAxPyRbhMBtnEwuvaTYpOQkFx1nSeusgezHniEc/l35Vo4iCq/mMiTJbpD7oYxN98hFlfmw== |
6471 | 6596 | ||
6472 | ini@^1.3.5: | 6597 | ini@^1.3.2, ini@^1.3.5: |
6473 | version "1.3.8" | 6598 | version "1.3.8" |
6474 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" | 6599 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" |
6475 | integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== | 6600 | integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== |
@@ -6504,6 +6629,15 @@ internal-slot@^1.0.3: | |||
6504 | has "^1.0.3" | 6629 | has "^1.0.3" |
6505 | side-channel "^1.0.4" | 6630 | side-channel "^1.0.4" |
6506 | 6631 | ||
6632 | internal-slot@^1.0.4: | ||
6633 | version "1.0.4" | ||
6634 | resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" | ||
6635 | integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== | ||
6636 | dependencies: | ||
6637 | get-intrinsic "^1.1.3" | ||
6638 | has "^1.0.3" | ||
6639 | side-channel "^1.0.4" | ||
6640 | |||
6507 | interpret@^2.2.0: | 6641 | interpret@^2.2.0: |
6508 | version "2.2.0" | 6642 | version "2.2.0" |
6509 | resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" | 6643 | resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" |
@@ -6556,7 +6690,12 @@ ipaddr.js@1.9.1: | |||
6556 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" | 6690 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" |
6557 | integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== | 6691 | integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== |
6558 | 6692 | ||
6559 | is-arguments@^1.1.0, is-arguments@^1.1.1: | 6693 | ipv6-normalize@1.0.1: |
6694 | version "1.0.1" | ||
6695 | resolved "https://registry.yarnpkg.com/ipv6-normalize/-/ipv6-normalize-1.0.1.tgz#1b3258290d365fa83239e89907dde4592e7620a8" | ||
6696 | integrity sha512-Bm6H79i01DjgGTCWjUuCjJ6QDo1HB96PT/xCYuyJUP9WFbVDrLSbG4EZCvOCun2rNswZb0c3e4Jt/ws795esHA== | ||
6697 | |||
6698 | is-arguments@^1.1.1: | ||
6560 | version "1.1.1" | 6699 | version "1.1.1" |
6561 | resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" | 6700 | resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" |
6562 | integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== | 6701 | integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== |
@@ -6564,6 +6703,15 @@ is-arguments@^1.1.0, is-arguments@^1.1.1: | |||
6564 | call-bind "^1.0.2" | 6703 | call-bind "^1.0.2" |
6565 | has-tostringtag "^1.0.0" | 6704 | has-tostringtag "^1.0.0" |
6566 | 6705 | ||
6706 | is-array-buffer@^3.0.1: | ||
6707 | version "3.0.1" | ||
6708 | resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" | ||
6709 | integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== | ||
6710 | dependencies: | ||
6711 | call-bind "^1.0.2" | ||
6712 | get-intrinsic "^1.1.3" | ||
6713 | is-typed-array "^1.1.10" | ||
6714 | |||
6567 | is-arrayish@^0.2.1: | 6715 | is-arrayish@^0.2.1: |
6568 | version "0.2.1" | 6716 | version "0.2.1" |
6569 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" | 6717 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" |
@@ -7508,6 +7656,16 @@ magnet-uri@^6.2.0: | |||
7508 | bep53-range "^1.1.0" | 7656 | bep53-range "^1.1.0" |
7509 | thirty-two "^1.0.2" | 7657 | thirty-two "^1.0.2" |
7510 | 7658 | ||
7659 | mailparser-mit@^1.0.0: | ||
7660 | version "1.0.0" | ||
7661 | resolved "https://registry.yarnpkg.com/mailparser-mit/-/mailparser-mit-1.0.0.tgz#19df8436c2a02e1d34a03ec518a2eb065e0a94a4" | ||
7662 | integrity sha512-sckRITNb3VCT1sQ275g47MAN786pQ5lU20bLY5f794dF/ARGzuVATQ64gO13FOw8jayjFT10e5ttsripKGGXcw== | ||
7663 | dependencies: | ||
7664 | addressparser "^1.0.1" | ||
7665 | iconv-lite "~0.4.24" | ||
7666 | mime "^1.6.0" | ||
7667 | uue "^3.1.0" | ||
7668 | |||
7511 | make-dir@^2.1.0: | 7669 | make-dir@^2.1.0: |
7512 | version "2.1.0" | 7670 | version "2.1.0" |
7513 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" | 7671 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" |
@@ -7576,6 +7734,13 @@ marky@^1.2.2: | |||
7576 | resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" | 7734 | resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" |
7577 | integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q== | 7735 | integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q== |
7578 | 7736 | ||
7737 | matchit@^1.0.0: | ||
7738 | version "1.1.0" | ||
7739 | resolved "https://registry.yarnpkg.com/matchit/-/matchit-1.1.0.tgz#c4ccf17d9c824cc1301edbcffde9b75a61d10a7c" | ||
7740 | integrity sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA== | ||
7741 | dependencies: | ||
7742 | "@arr/every" "^1.0.0" | ||
7743 | |||
7579 | mathml-tag-names@^2.1.3: | 7744 | mathml-tag-names@^2.1.3: |
7580 | version "2.1.3" | 7745 | version "2.1.3" |
7581 | resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" | 7746 | resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" |
@@ -7679,7 +7844,7 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, | |||
7679 | dependencies: | 7844 | dependencies: |
7680 | mime-db "1.52.0" | 7845 | mime-db "1.52.0" |
7681 | 7846 | ||
7682 | mime@1.6.0, mime@^1.4.1: | 7847 | mime@1.6.0, mime@^1.4.1, mime@^1.6.0: |
7683 | version "1.6.0" | 7848 | version "1.6.0" |
7684 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" | 7849 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" |
7685 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== | 7850 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== |
@@ -7740,7 +7905,7 @@ minimatch@5.0.1: | |||
7740 | dependencies: | 7905 | dependencies: |
7741 | brace-expansion "^2.0.1" | 7906 | brace-expansion "^2.0.1" |
7742 | 7907 | ||
7743 | minimatch@5.1.0, minimatch@^5.0.0, minimatch@^5.0.1, minimatch@^5.1.0: | 7908 | minimatch@5.1.0: |
7744 | version "5.1.0" | 7909 | version "5.1.0" |
7745 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" | 7910 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" |
7746 | integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== | 7911 | integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== |
@@ -7754,6 +7919,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: | |||
7754 | dependencies: | 7919 | dependencies: |
7755 | brace-expansion "^1.1.7" | 7920 | brace-expansion "^1.1.7" |
7756 | 7921 | ||
7922 | minimatch@^5.0.0, minimatch@^5.0.1, minimatch@^5.1.0: | ||
7923 | version "5.1.6" | ||
7924 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" | ||
7925 | integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== | ||
7926 | dependencies: | ||
7927 | brace-expansion "^2.0.1" | ||
7928 | |||
7757 | minimatch@~3.0.2: | 7929 | minimatch@~3.0.2: |
7758 | version "3.0.8" | 7930 | version "3.0.8" |
7759 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" | 7931 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" |
@@ -7848,9 +8020,9 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: | |||
7848 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== | 8020 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== |
7849 | 8021 | ||
7850 | mocha@^10.0.0: | 8022 | mocha@^10.0.0: |
7851 | version "10.1.0" | 8023 | version "10.2.0" |
7852 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a" | 8024 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" |
7853 | integrity sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg== | 8025 | integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== |
7854 | dependencies: | 8026 | dependencies: |
7855 | ansi-colors "4.1.1" | 8027 | ansi-colors "4.1.1" |
7856 | browser-stdout "1.3.1" | 8028 | browser-stdout "1.3.1" |
@@ -8080,6 +8252,11 @@ node-releases@^2.0.6: | |||
8080 | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" | 8252 | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" |
8081 | integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== | 8253 | integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== |
8082 | 8254 | ||
8255 | nodemailer@6.7.3: | ||
8256 | version "6.7.3" | ||
8257 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.3.tgz#b73f9a81b9c8fa8acb4ea14b608f5e725ea8e018" | ||
8258 | integrity sha512-KUdDsspqx89sD4UUyUKzdlUOper3hRkDVkrKh/89G+d9WKsU5ox51NWS4tB1XR5dPUdR4SP0E3molyEfOvSa3g== | ||
8259 | |||
8083 | nopt@^6.0.0: | 8260 | nopt@^6.0.0: |
8084 | version "6.0.0" | 8261 | version "6.0.0" |
8085 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" | 8262 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" |
@@ -8267,7 +8444,12 @@ oauth-sign@~0.9.0: | |||
8267 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" | 8444 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" |
8268 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== | 8445 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== |
8269 | 8446 | ||
8270 | object-inspect@^1.10.3, object-inspect@^1.12.2, object-inspect@^1.9.0: | 8447 | object-inspect@^1.10.3, object-inspect@^1.9.0: |
8448 | version "1.12.3" | ||
8449 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" | ||
8450 | integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== | ||
8451 | |||
8452 | object-inspect@^1.12.2: | ||
8271 | version "1.12.2" | 8453 | version "1.12.2" |
8272 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" | 8454 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" |
8273 | integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== | 8455 | integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== |
@@ -8775,6 +8957,14 @@ pngjs@^5.0.0: | |||
8775 | resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" | 8957 | resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" |
8776 | integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== | 8958 | integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== |
8777 | 8959 | ||
8960 | polka@^0.5.2: | ||
8961 | version "0.5.2" | ||
8962 | resolved "https://registry.yarnpkg.com/polka/-/polka-0.5.2.tgz#588bee0c5806dbc6c64958de3a1251860e9f2e26" | ||
8963 | integrity sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw== | ||
8964 | dependencies: | ||
8965 | "@polka/url" "^0.5.0" | ||
8966 | trouter "^2.0.1" | ||
8967 | |||
8778 | postcss-attribute-case-insensitive@^5.0.2: | 8968 | postcss-attribute-case-insensitive@^5.0.2: |
8779 | version "5.0.2" | 8969 | version "5.0.2" |
8780 | resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" | 8970 | resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" |
@@ -9285,9 +9475,9 @@ qs@~6.5.2: | |||
9285 | integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== | 9475 | integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== |
9286 | 9476 | ||
9287 | query-selector-shadow-dom@^1.0.0: | 9477 | query-selector-shadow-dom@^1.0.0: |
9288 | version "1.0.0" | 9478 | version "1.0.1" |
9289 | resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.0.tgz#8fa7459a4620f094457640e74e953a9dbe61a38e" | 9479 | resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349" |
9290 | integrity sha512-bK0/0cCI+R8ZmOF1QjT7HupDUYCxbf/9TJgAmSXQxZpftXmTAeil9DRoCnTDkWbvOyZzhcMBwKpptWcdkGFIMg== | 9480 | integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw== |
9291 | 9481 | ||
9292 | querystring@0.2.0: | 9482 | querystring@0.2.0: |
9293 | version "0.2.0" | 9483 | version "0.2.0" |
@@ -9736,9 +9926,9 @@ responselike@^2.0.0: | |||
9736 | lowercase-keys "^2.0.0" | 9926 | lowercase-keys "^2.0.0" |
9737 | 9927 | ||
9738 | resq@^1.9.1: | 9928 | resq@^1.9.1: |
9739 | version "1.10.2" | 9929 | version "1.11.0" |
9740 | resolved "https://registry.yarnpkg.com/resq/-/resq-1.10.2.tgz#cedf4f20d53f6e574b1e12afbda446ad9576c193" | 9930 | resolved "https://registry.yarnpkg.com/resq/-/resq-1.11.0.tgz#edec8c58be9af800fd628118c0ca8815283de196" |
9741 | integrity sha512-HmgVS3j+FLrEDBTDYysPdPVF9/hioDMJ/otOiQDKqk77YfZeeLOj0qi34yObumcud1gBpk+wpBTEg4kMicD++A== | 9931 | integrity sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw== |
9742 | dependencies: | 9932 | dependencies: |
9743 | fast-deep-equal "^2.0.1" | 9933 | fast-deep-equal "^2.0.1" |
9744 | 9934 | ||
@@ -9835,13 +10025,20 @@ rxjs@6.6.7: | |||
9835 | dependencies: | 10025 | dependencies: |
9836 | tslib "^1.9.0" | 10026 | tslib "^1.9.0" |
9837 | 10027 | ||
9838 | rxjs@^7.3.0, rxjs@^7.4.0, rxjs@^7.5.5: | 10028 | rxjs@^7.3.0, rxjs@^7.4.0: |
9839 | version "7.5.7" | 10029 | version "7.5.7" |
9840 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" | 10030 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" |
9841 | integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== | 10031 | integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== |
9842 | dependencies: | 10032 | dependencies: |
9843 | tslib "^2.1.0" | 10033 | tslib "^2.1.0" |
9844 | 10034 | ||
10035 | rxjs@^7.5.5: | ||
10036 | version "7.8.0" | ||
10037 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" | ||
10038 | integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== | ||
10039 | dependencies: | ||
10040 | tslib "^2.1.0" | ||
10041 | |||
9845 | safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: | 10042 | safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: |
9846 | version "5.1.2" | 10043 | version "5.1.2" |
9847 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" | 10044 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" |
@@ -10191,6 +10388,15 @@ smart-buffer@^4.2.0: | |||
10191 | resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" | 10388 | resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" |
10192 | integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== | 10389 | integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== |
10193 | 10390 | ||
10391 | smtp-server@^3.9.0: | ||
10392 | version "3.11.0" | ||
10393 | resolved "https://registry.yarnpkg.com/smtp-server/-/smtp-server-3.11.0.tgz#8820c191124fab37a8f16c8325a7f1fd38092c4f" | ||
10394 | integrity sha512-j/W6mEKeMNKuiM9oCAAjm87agPEN1O3IU4cFLT4ZOCyyq3UXN7HiIXF+q7izxJcYSar15B/JaSxcijoPCR8Tag== | ||
10395 | dependencies: | ||
10396 | base32.js "0.1.0" | ||
10397 | ipv6-normalize "1.0.1" | ||
10398 | nodemailer "6.7.3" | ||
10399 | |||
10194 | socket.io-client@^4.5.4: | 10400 | socket.io-client@^4.5.4: |
10195 | version "4.5.4" | 10401 | version "4.5.4" |
10196 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.4.tgz#d3cde8a06a6250041ba7390f08d2468ccebc5ac9" | 10402 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.4.tgz#d3cde8a06a6250041ba7390f08d2468ccebc5ac9" |
@@ -10420,6 +10626,13 @@ statuses@2.0.1: | |||
10420 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" | 10626 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" |
10421 | integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== | 10627 | integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== |
10422 | 10628 | ||
10629 | stop-iteration-iterator@^1.0.0: | ||
10630 | version "1.0.0" | ||
10631 | resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" | ||
10632 | integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== | ||
10633 | dependencies: | ||
10634 | internal-slot "^1.0.4" | ||
10635 | |||
10423 | stream-browserify@^3.0.0: | 10636 | stream-browserify@^3.0.0: |
10424 | version "3.0.0" | 10637 | version "3.0.0" |
10425 | resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" | 10638 | resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" |
@@ -10980,6 +11193,13 @@ trim-newlines@^3.0.0: | |||
10980 | resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" | 11193 | resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" |
10981 | integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== | 11194 | integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== |
10982 | 11195 | ||
11196 | trouter@^2.0.1: | ||
11197 | version "2.0.1" | ||
11198 | resolved "https://registry.yarnpkg.com/trouter/-/trouter-2.0.1.tgz#2726a5f8558e090d24c3a393f09eaab1df232df6" | ||
11199 | integrity sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ== | ||
11200 | dependencies: | ||
11201 | matchit "^1.0.0" | ||
11202 | |||
10983 | ts-loader@^9.3.0: | 11203 | ts-loader@^9.3.0: |
10984 | version "9.4.1" | 11204 | version "9.4.1" |
10985 | resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.1.tgz#b6f3d82db0eac5a8295994f8cb5e4940ff6b1060" | 11205 | resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.1.tgz#b6f3d82db0eac5a8295994f8cb5e4940ff6b1060" |
@@ -11280,6 +11500,14 @@ utp-native@^2.5.3: | |||
11280 | timeout-refresh "^1.0.0" | 11500 | timeout-refresh "^1.0.0" |
11281 | unordered-set "^2.0.1" | 11501 | unordered-set "^2.0.1" |
11282 | 11502 | ||
11503 | uue@^3.1.0: | ||
11504 | version "3.1.2" | ||
11505 | resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2" | ||
11506 | integrity sha512-axKLXVqwtdI/czrjG0X8hyV1KLgeWx8F4KvSbvVCnS+RUvsQMGRjx0kfuZDXXqj0LYvVJmx3B9kWlKtEdRrJLg== | ||
11507 | dependencies: | ||
11508 | escape-string-regexp "~1.0.5" | ||
11509 | extend "~3.0.0" | ||
11510 | |||
11283 | uuid@8.3.2, uuid@^8.3.2: | 11511 | uuid@8.3.2, uuid@^8.3.2: |
11284 | version "8.3.2" | 11512 | version "8.3.2" |
11285 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" | 11513 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" |
@@ -11415,31 +11643,31 @@ wdio-geckodriver-service@^3.0.2: | |||
11415 | split2 "^4.1.0" | 11643 | split2 "^4.1.0" |
11416 | tcp-port-used "^1.0.2" | 11644 | tcp-port-used "^1.0.2" |
11417 | 11645 | ||
11418 | webdriver@7.26.0: | 11646 | webdriver@7.27.0: |
11419 | version "7.26.0" | 11647 | version "7.27.0" |
11420 | resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-7.26.0.tgz#cc20640ee9906c0126044449dfe9562b6277d14e" | 11648 | resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-7.27.0.tgz#41d23a6c38bd79ea868f0b9fb9c9e3d4b6e4f8bd" |
11421 | integrity sha512-T21T31wq29D/rmpFHcAahhdrvfsfXsLs/LBe2su7wL725ptOEoSssuDXjXMkwjf9MSUIXnTcUIz8oJGbKRUMwQ== | 11649 | integrity sha512-870uIBnrGJ86g3DdYjM+PHhqdWf6NxysSme1KIs6irWxK+LqcaWKWhN75PldE+04xJB2mVWt1tKn0NBBFTWeMg== |
11422 | dependencies: | 11650 | dependencies: |
11423 | "@types/node" "^18.0.0" | 11651 | "@types/node" "^18.0.0" |
11424 | "@wdio/config" "7.26.0" | 11652 | "@wdio/config" "7.26.0" |
11425 | "@wdio/logger" "7.26.0" | 11653 | "@wdio/logger" "7.26.0" |
11426 | "@wdio/protocols" "7.22.0" | 11654 | "@wdio/protocols" "7.27.0" |
11427 | "@wdio/types" "7.26.0" | 11655 | "@wdio/types" "7.26.0" |
11428 | "@wdio/utils" "7.26.0" | 11656 | "@wdio/utils" "7.26.0" |
11429 | got "^11.0.2" | 11657 | got "^11.0.2" |
11430 | ky "0.30.0" | 11658 | ky "0.30.0" |
11431 | lodash.merge "^4.6.1" | 11659 | lodash.merge "^4.6.1" |
11432 | 11660 | ||
11433 | webdriverio@7.26.0: | 11661 | webdriverio@7.29.1: |
11434 | version "7.26.0" | 11662 | version "7.29.1" |
11435 | resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-7.26.0.tgz#d6036d950ef96fb6cc29c6c5c9cfc452fcafa59a" | 11663 | resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-7.29.1.tgz#f71c9de317326cff36d22f6277669477e5340c6f" |
11436 | integrity sha512-7m9TeP871aYxZYKBI4GDh5aQZLN9Fd/PASu5K/jEIT65J4OBB6g5ZaycGFOmfNHCfjWKjwPXZuKiN1f2mcrcRg== | 11664 | integrity sha512-2xhoaZvV0tzOgnj8H/B4Yol8LTcIrWdfTdfe01d+ERtdzKCoqimmPNP4vpr2lVRVKL/TW4rfoBTBNvDUaJHe2g== |
11437 | dependencies: | 11665 | dependencies: |
11438 | "@types/aria-query" "^5.0.0" | 11666 | "@types/aria-query" "^5.0.0" |
11439 | "@types/node" "^18.0.0" | 11667 | "@types/node" "^18.0.0" |
11440 | "@wdio/config" "7.26.0" | 11668 | "@wdio/config" "7.26.0" |
11441 | "@wdio/logger" "7.26.0" | 11669 | "@wdio/logger" "7.26.0" |
11442 | "@wdio/protocols" "7.22.0" | 11670 | "@wdio/protocols" "7.27.0" |
11443 | "@wdio/repl" "7.26.0" | 11671 | "@wdio/repl" "7.26.0" |
11444 | "@wdio/types" "7.26.0" | 11672 | "@wdio/types" "7.26.0" |
11445 | "@wdio/utils" "7.26.0" | 11673 | "@wdio/utils" "7.26.0" |
@@ -11447,8 +11675,8 @@ webdriverio@7.26.0: | |||
11447 | aria-query "^5.0.0" | 11675 | aria-query "^5.0.0" |
11448 | css-shorthand-properties "^1.1.1" | 11676 | css-shorthand-properties "^1.1.1" |
11449 | css-value "^0.0.1" | 11677 | css-value "^0.0.1" |
11450 | devtools "7.26.0" | 11678 | devtools "7.28.1" |
11451 | devtools-protocol "^0.0.1069585" | 11679 | devtools-protocol "^0.0.1085790" |
11452 | fs-extra "^10.0.0" | 11680 | fs-extra "^10.0.0" |
11453 | grapheme-splitter "^1.0.2" | 11681 | grapheme-splitter "^1.0.2" |
11454 | lodash.clonedeep "^4.5.0" | 11682 | lodash.clonedeep "^4.5.0" |
@@ -11461,7 +11689,7 @@ webdriverio@7.26.0: | |||
11461 | resq "^1.9.1" | 11689 | resq "^1.9.1" |
11462 | rgb2hex "0.2.5" | 11690 | rgb2hex "0.2.5" |
11463 | serialize-error "^8.0.0" | 11691 | serialize-error "^8.0.0" |
11464 | webdriver "7.26.0" | 11692 | webdriver "7.27.0" |
11465 | 11693 | ||
11466 | webidl-conversions@^3.0.0: | 11694 | webidl-conversions@^3.0.0: |
11467 | version "3.0.1" | 11695 | version "3.0.1" |
@@ -11732,7 +11960,7 @@ which-module@^2.0.0: | |||
11732 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" | 11960 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" |
11733 | integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== | 11961 | integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== |
11734 | 11962 | ||
11735 | which-typed-array@^1.1.8: | 11963 | which-typed-array@^1.1.9: |
11736 | version "1.1.9" | 11964 | version "1.1.9" |
11737 | resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" | 11965 | resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" |
11738 | integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== | 11966 | integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== |
@@ -11770,6 +11998,11 @@ wildcard@^2.0.0: | |||
11770 | resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" | 11998 | resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" |
11771 | integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== | 11999 | integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== |
11772 | 12000 | ||
12001 | wildstring@1.0.9: | ||
12002 | version "1.0.9" | ||
12003 | resolved "https://registry.yarnpkg.com/wildstring/-/wildstring-1.0.9.tgz#82a696d5653c7d4ec9ba716859b6b53aba2761c5" | ||
12004 | integrity sha512-XBNxKIMLO6uVHf1Xvo++HGWAZZoiVCHmEMCmZJzJ82vQsuUJCLw13Gzq0mRCATk7a3+ZcgeOKSDioavuYqtlfA== | ||
12005 | |||
11773 | word-wrap@^1.2.3: | 12006 | word-wrap@^1.2.3: |
11774 | version "1.2.3" | 12007 | version "1.2.3" |
11775 | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" | 12008 | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" |
diff --git a/config/default.yaml b/config/default.yaml index 20094ae8f..37059e9e0 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -37,6 +37,11 @@ rates_limit: | |||
37 | window: 10 minutes | 37 | window: 10 minutes |
38 | max: 10 | 38 | max: 10 |
39 | 39 | ||
40 | oauth2: | ||
41 | token_lifetime: | ||
42 | access_token: '1 day' | ||
43 | refresh_token: '2 weeks' | ||
44 | |||
40 | # Proxies to trust to get real client IP | 45 | # Proxies to trust to get real client IP |
41 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' | 46 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' |
42 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) | 47 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) |
@@ -377,9 +382,15 @@ contact_form: | |||
377 | 382 | ||
378 | signup: | 383 | signup: |
379 | enabled: false | 384 | enabled: false |
385 | |||
380 | limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited | 386 | limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited |
387 | |||
381 | minimum_age: 16 # Used to configure the signup form | 388 | minimum_age: 16 # Used to configure the signup form |
389 | |||
390 | # Users fill a form to register so moderators can accept/reject the registration | ||
391 | requires_approval: true | ||
382 | requires_email_verification: false | 392 | requires_email_verification: false |
393 | |||
383 | filters: | 394 | filters: |
384 | cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist | 395 | cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist |
385 | whitelist: [] | 396 | whitelist: [] |
diff --git a/config/dev.yaml b/config/dev.yaml index ef93afc19..44856fb6a 100644 --- a/config/dev.yaml +++ b/config/dev.yaml | |||
@@ -8,6 +8,11 @@ webserver: | |||
8 | secrets: | 8 | secrets: |
9 | peertube: 'my super dev secret' | 9 | peertube: 'my super dev secret' |
10 | 10 | ||
11 | rates_limit: | ||
12 | signup: | ||
13 | window: 5 minutes | ||
14 | max: 200 | ||
15 | |||
11 | database: | 16 | database: |
12 | hostname: 'localhost' | 17 | hostname: 'localhost' |
13 | port: 5432 | 18 | port: 5432 |
diff --git a/config/production.yaml.example b/config/production.yaml.example index e8b354d01..906fb7e1f 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -35,6 +35,11 @@ rates_limit: | |||
35 | window: 10 minutes | 35 | window: 10 minutes |
36 | max: 10 | 36 | max: 10 |
37 | 37 | ||
38 | oauth2: | ||
39 | token_lifetime: | ||
40 | access_token: '1 day' | ||
41 | refresh_token: '2 weeks' | ||
42 | |||
38 | # Proxies to trust to get real client IP | 43 | # Proxies to trust to get real client IP |
39 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' | 44 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' |
40 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) | 45 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) |
@@ -387,9 +392,15 @@ contact_form: | |||
387 | 392 | ||
388 | signup: | 393 | signup: |
389 | enabled: false | 394 | enabled: false |
395 | |||
390 | limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited | 396 | limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited |
397 | |||
391 | minimum_age: 16 # Used to configure the signup form | 398 | minimum_age: 16 # Used to configure the signup form |
399 | |||
400 | # Users fill a form to register so moderators can accept/reject the registration | ||
401 | requires_approval: true | ||
392 | requires_email_verification: false | 402 | requires_email_verification: false |
403 | |||
393 | filters: | 404 | filters: |
394 | cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist | 405 | cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist |
395 | whitelist: [] | 406 | whitelist: [] |
diff --git a/config/test.yaml b/config/test.yaml index 878d68cb9..94d74ffa5 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -74,6 +74,7 @@ cache: | |||
74 | 74 | ||
75 | signup: | 75 | signup: |
76 | enabled: true | 76 | enabled: true |
77 | requires_approval: false | ||
77 | requires_email_verification: false | 78 | requires_email_verification: false |
78 | 79 | ||
79 | transcoding: | 80 | transcoding: |
diff --git a/package.json b/package.json index d7d19afc2..b48f65bbd 100644 --- a/package.json +++ b/package.json | |||
@@ -225,7 +225,7 @@ | |||
225 | "swagger-cli": "^4.0.2", | 225 | "swagger-cli": "^4.0.2", |
226 | "ts-node": "^10.8.1", | 226 | "ts-node": "^10.8.1", |
227 | "tsc-watch": "^5.0.3", | 227 | "tsc-watch": "^5.0.3", |
228 | "typescript": "^4.0.5" | 228 | "typescript": "~4.8" |
229 | }, | 229 | }, |
230 | "bundlewatch": { | 230 | "bundlewatch": { |
231 | "files": [ | 231 | "files": [ |
diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts index bcd7fe2a2..eca2fe09d 100755 --- a/scripts/i18n/create-custom-files.ts +++ b/scripts/i18n/create-custom-files.ts | |||
@@ -41,6 +41,7 @@ const playerKeys = { | |||
41 | 'Volume': 'Volume', | 41 | 'Volume': 'Volume', |
42 | 'Codecs': 'Codecs', | 42 | 'Codecs': 'Codecs', |
43 | 'Color': 'Color', | 43 | 'Color': 'Color', |
44 | 'Go back to the live': 'Go back to the live', | ||
44 | 'Connection Speed': 'Connection Speed', | 45 | 'Connection Speed': 'Connection Speed', |
45 | 'Network Activity': 'Network Activity', | 46 | 'Network Activity': 'Network Activity', |
46 | 'Total Transfered': 'Total Transfered', | 47 | 'Total Transfered': 'Total Transfered', |
@@ -89,7 +90,6 @@ Object.values(VIDEO_CATEGORIES) | |||
89 | 90 | ||
90 | // More keys | 91 | // More keys |
91 | Object.assign(serverKeys, { | 92 | Object.assign(serverKeys, { |
92 | Misc: 'Misc', | ||
93 | Unknown: 'Unknown' | 93 | Unknown: 'Unknown' |
94 | }) | 94 | }) |
95 | 95 | ||
@@ -279,7 +279,7 @@ app.use((err, _req, res: express.Response, _next) => { | |||
279 | }) | 279 | }) |
280 | }) | 280 | }) |
281 | 281 | ||
282 | const server = createWebsocketTrackerServer(app) | 282 | const { server, trackerServer } = createWebsocketTrackerServer(app) |
283 | 283 | ||
284 | // ----------- Run ----------- | 284 | // ----------- Run ----------- |
285 | 285 | ||
@@ -328,7 +328,8 @@ async function startApplication () { | |||
328 | VideoChannelSyncLatestScheduler.Instance.enable() | 328 | VideoChannelSyncLatestScheduler.Instance.enable() |
329 | VideoViewsBufferScheduler.Instance.enable() | 329 | VideoViewsBufferScheduler.Instance.enable() |
330 | GeoIPUpdateScheduler.Instance.enable() | 330 | GeoIPUpdateScheduler.Instance.enable() |
331 | OpenTelemetryMetrics.Instance.registerMetrics() | 331 | |
332 | OpenTelemetryMetrics.Instance.registerMetrics({ trackerServer }) | ||
332 | 333 | ||
333 | PluginManager.Instance.init(server) | 334 | PluginManager.Instance.init(server) |
334 | // Before PeerTubeSocket init | 335 | // Before PeerTubeSocket init |
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 8e064fb5b..def320730 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -309,7 +309,7 @@ async function videoCommentsController (req: express.Request, res: express.Respo | |||
309 | if (redirectIfNotOwned(video.url, res)) return | 309 | if (redirectIfNotOwned(video.url, res)) return |
310 | 310 | ||
311 | const handler = async (start: number, count: number) => { | 311 | const handler = async (start: number, count: number) => { |
312 | const result = await VideoCommentModel.listAndCountByVideoForAP(video, start, count) | 312 | const result = await VideoCommentModel.listAndCountByVideoForAP({ video, start, count }) |
313 | 313 | ||
314 | return { | 314 | return { |
315 | total: result.total, | 315 | total: result.total, |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index f0fb43071..86434f382 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -193,6 +193,7 @@ function customConfig (): CustomConfig { | |||
193 | signup: { | 193 | signup: { |
194 | enabled: CONFIG.SIGNUP.ENABLED, | 194 | enabled: CONFIG.SIGNUP.ENABLED, |
195 | limit: CONFIG.SIGNUP.LIMIT, | 195 | limit: CONFIG.SIGNUP.LIMIT, |
196 | requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL, | ||
196 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION, | 197 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION, |
197 | minimumAge: CONFIG.SIGNUP.MINIMUM_AGE | 198 | minimumAge: CONFIG.SIGNUP.MINIMUM_AGE |
198 | }, | 199 | }, |
diff --git a/server/controllers/api/users/email-verification.ts b/server/controllers/api/users/email-verification.ts new file mode 100644 index 000000000..230aaa9af --- /dev/null +++ b/server/controllers/api/users/email-verification.ts | |||
@@ -0,0 +1,72 @@ | |||
1 | import express from 'express' | ||
2 | import { HttpStatusCode } from '@shared/models' | ||
3 | import { CONFIG } from '../../../initializers/config' | ||
4 | import { sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user' | ||
5 | import { asyncMiddleware, buildRateLimiter } from '../../../middlewares' | ||
6 | import { | ||
7 | registrationVerifyEmailValidator, | ||
8 | usersAskSendVerifyEmailValidator, | ||
9 | usersVerifyEmailValidator | ||
10 | } from '../../../middlewares/validators' | ||
11 | |||
12 | const askSendEmailLimiter = buildRateLimiter({ | ||
13 | windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, | ||
14 | max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX | ||
15 | }) | ||
16 | |||
17 | const emailVerificationRouter = express.Router() | ||
18 | |||
19 | emailVerificationRouter.post([ '/ask-send-verify-email', '/registrations/ask-send-verify-email' ], | ||
20 | askSendEmailLimiter, | ||
21 | asyncMiddleware(usersAskSendVerifyEmailValidator), | ||
22 | asyncMiddleware(reSendVerifyUserEmail) | ||
23 | ) | ||
24 | |||
25 | emailVerificationRouter.post('/:id/verify-email', | ||
26 | asyncMiddleware(usersVerifyEmailValidator), | ||
27 | asyncMiddleware(verifyUserEmail) | ||
28 | ) | ||
29 | |||
30 | emailVerificationRouter.post('/registrations/:registrationId/verify-email', | ||
31 | asyncMiddleware(registrationVerifyEmailValidator), | ||
32 | asyncMiddleware(verifyRegistrationEmail) | ||
33 | ) | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | export { | ||
38 | emailVerificationRouter | ||
39 | } | ||
40 | |||
41 | async function reSendVerifyUserEmail (req: express.Request, res: express.Response) { | ||
42 | const user = res.locals.user | ||
43 | const registration = res.locals.userRegistration | ||
44 | |||
45 | if (user) await sendVerifyUserEmail(user) | ||
46 | else if (registration) await sendVerifyRegistrationEmail(registration) | ||
47 | |||
48 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
49 | } | ||
50 | |||
51 | async function verifyUserEmail (req: express.Request, res: express.Response) { | ||
52 | const user = res.locals.user | ||
53 | user.emailVerified = true | ||
54 | |||
55 | if (req.body.isPendingEmail === true) { | ||
56 | user.email = user.pendingEmail | ||
57 | user.pendingEmail = null | ||
58 | } | ||
59 | |||
60 | await user.save() | ||
61 | |||
62 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
63 | } | ||
64 | |||
65 | async function verifyRegistrationEmail (req: express.Request, res: express.Response) { | ||
66 | const registration = res.locals.userRegistration | ||
67 | registration.emailVerified = true | ||
68 | |||
69 | await registration.save() | ||
70 | |||
71 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
72 | } | ||
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index a8677a1d3..5a5a12e82 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -4,26 +4,21 @@ import { Hooks } from '@server/lib/plugins/hooks' | |||
4 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 4 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
5 | import { MUserAccountDefault } from '@server/types/models' | 5 | import { MUserAccountDefault } from '@server/types/models' |
6 | import { pick } from '@shared/core-utils' | 6 | import { pick } from '@shared/core-utils' |
7 | import { HttpStatusCode, UserCreate, UserCreateResult, UserRegister, UserRight, UserUpdate } from '@shared/models' | 7 | import { HttpStatusCode, UserCreate, UserCreateResult, UserRight, UserUpdate } from '@shared/models' |
8 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' | 8 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' |
9 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
10 | import { generateRandomString, getFormattedObjects } from '../../../helpers/utils' | 10 | import { generateRandomString, getFormattedObjects } from '../../../helpers/utils' |
11 | import { CONFIG } from '../../../initializers/config' | ||
12 | import { WEBSERVER } from '../../../initializers/constants' | 11 | import { WEBSERVER } from '../../../initializers/constants' |
13 | import { sequelizeTypescript } from '../../../initializers/database' | 12 | import { sequelizeTypescript } from '../../../initializers/database' |
14 | import { Emailer } from '../../../lib/emailer' | 13 | import { Emailer } from '../../../lib/emailer' |
15 | import { Notifier } from '../../../lib/notifier' | ||
16 | import { Redis } from '../../../lib/redis' | 14 | import { Redis } from '../../../lib/redis' |
17 | import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' | 15 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../../../lib/user' |
18 | import { | 16 | import { |
19 | adminUsersSortValidator, | 17 | adminUsersSortValidator, |
20 | asyncMiddleware, | 18 | asyncMiddleware, |
21 | asyncRetryTransactionMiddleware, | 19 | asyncRetryTransactionMiddleware, |
22 | authenticate, | 20 | authenticate, |
23 | buildRateLimiter, | ||
24 | ensureUserHasRight, | 21 | ensureUserHasRight, |
25 | ensureUserRegistrationAllowed, | ||
26 | ensureUserRegistrationAllowedForIP, | ||
27 | paginationValidator, | 22 | paginationValidator, |
28 | setDefaultPagination, | 23 | setDefaultPagination, |
29 | setDefaultSort, | 24 | setDefaultSort, |
@@ -31,19 +26,17 @@ import { | |||
31 | usersAddValidator, | 26 | usersAddValidator, |
32 | usersGetValidator, | 27 | usersGetValidator, |
33 | usersListValidator, | 28 | usersListValidator, |
34 | usersRegisterValidator, | ||
35 | usersRemoveValidator, | 29 | usersRemoveValidator, |
36 | usersUpdateValidator | 30 | usersUpdateValidator |
37 | } from '../../../middlewares' | 31 | } from '../../../middlewares' |
38 | import { | 32 | import { |
39 | ensureCanModerateUser, | 33 | ensureCanModerateUser, |
40 | usersAskResetPasswordValidator, | 34 | usersAskResetPasswordValidator, |
41 | usersAskSendVerifyEmailValidator, | ||
42 | usersBlockingValidator, | 35 | usersBlockingValidator, |
43 | usersResetPasswordValidator, | 36 | usersResetPasswordValidator |
44 | usersVerifyEmailValidator | ||
45 | } from '../../../middlewares/validators' | 37 | } from '../../../middlewares/validators' |
46 | import { UserModel } from '../../../models/user/user' | 38 | import { UserModel } from '../../../models/user/user' |
39 | import { emailVerificationRouter } from './email-verification' | ||
47 | import { meRouter } from './me' | 40 | import { meRouter } from './me' |
48 | import { myAbusesRouter } from './my-abuses' | 41 | import { myAbusesRouter } from './my-abuses' |
49 | import { myBlocklistRouter } from './my-blocklist' | 42 | import { myBlocklistRouter } from './my-blocklist' |
@@ -51,22 +44,14 @@ import { myVideosHistoryRouter } from './my-history' | |||
51 | import { myNotificationsRouter } from './my-notifications' | 44 | import { myNotificationsRouter } from './my-notifications' |
52 | import { mySubscriptionsRouter } from './my-subscriptions' | 45 | import { mySubscriptionsRouter } from './my-subscriptions' |
53 | import { myVideoPlaylistsRouter } from './my-video-playlists' | 46 | import { myVideoPlaylistsRouter } from './my-video-playlists' |
47 | import { registrationsRouter } from './registrations' | ||
54 | import { twoFactorRouter } from './two-factor' | 48 | import { twoFactorRouter } from './two-factor' |
55 | 49 | ||
56 | const auditLogger = auditLoggerFactory('users') | 50 | const auditLogger = auditLoggerFactory('users') |
57 | 51 | ||
58 | const signupRateLimiter = buildRateLimiter({ | ||
59 | windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, | ||
60 | max: CONFIG.RATES_LIMIT.SIGNUP.MAX, | ||
61 | skipFailedRequests: true | ||
62 | }) | ||
63 | |||
64 | const askSendEmailLimiter = buildRateLimiter({ | ||
65 | windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, | ||
66 | max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX | ||
67 | }) | ||
68 | |||
69 | const usersRouter = express.Router() | 52 | const usersRouter = express.Router() |
53 | usersRouter.use('/', emailVerificationRouter) | ||
54 | usersRouter.use('/', registrationsRouter) | ||
70 | usersRouter.use('/', twoFactorRouter) | 55 | usersRouter.use('/', twoFactorRouter) |
71 | usersRouter.use('/', tokensRouter) | 56 | usersRouter.use('/', tokensRouter) |
72 | usersRouter.use('/', myNotificationsRouter) | 57 | usersRouter.use('/', myNotificationsRouter) |
@@ -122,14 +107,6 @@ usersRouter.post('/', | |||
122 | asyncRetryTransactionMiddleware(createUser) | 107 | asyncRetryTransactionMiddleware(createUser) |
123 | ) | 108 | ) |
124 | 109 | ||
125 | usersRouter.post('/register', | ||
126 | signupRateLimiter, | ||
127 | asyncMiddleware(ensureUserRegistrationAllowed), | ||
128 | ensureUserRegistrationAllowedForIP, | ||
129 | asyncMiddleware(usersRegisterValidator), | ||
130 | asyncRetryTransactionMiddleware(registerUser) | ||
131 | ) | ||
132 | |||
133 | usersRouter.put('/:id', | 110 | usersRouter.put('/:id', |
134 | authenticate, | 111 | authenticate, |
135 | ensureUserHasRight(UserRight.MANAGE_USERS), | 112 | ensureUserHasRight(UserRight.MANAGE_USERS), |
@@ -156,17 +133,6 @@ usersRouter.post('/:id/reset-password', | |||
156 | asyncMiddleware(resetUserPassword) | 133 | asyncMiddleware(resetUserPassword) |
157 | ) | 134 | ) |
158 | 135 | ||
159 | usersRouter.post('/ask-send-verify-email', | ||
160 | askSendEmailLimiter, | ||
161 | asyncMiddleware(usersAskSendVerifyEmailValidator), | ||
162 | asyncMiddleware(reSendVerifyUserEmail) | ||
163 | ) | ||
164 | |||
165 | usersRouter.post('/:id/verify-email', | ||
166 | asyncMiddleware(usersVerifyEmailValidator), | ||
167 | asyncMiddleware(verifyUserEmail) | ||
168 | ) | ||
169 | |||
170 | // --------------------------------------------------------------------------- | 136 | // --------------------------------------------------------------------------- |
171 | 137 | ||
172 | export { | 138 | export { |
@@ -218,35 +184,6 @@ async function createUser (req: express.Request, res: express.Response) { | |||
218 | }) | 184 | }) |
219 | } | 185 | } |
220 | 186 | ||
221 | async function registerUser (req: express.Request, res: express.Response) { | ||
222 | const body: UserRegister = req.body | ||
223 | |||
224 | const userToCreate = buildUser({ | ||
225 | ...pick(body, [ 'username', 'password', 'email' ]), | ||
226 | |||
227 | emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null | ||
228 | }) | ||
229 | |||
230 | const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({ | ||
231 | userToCreate, | ||
232 | userDisplayName: body.displayName || undefined, | ||
233 | channelNames: body.channel | ||
234 | }) | ||
235 | |||
236 | auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) | ||
237 | logger.info('User %s with its channel and account registered.', body.username) | ||
238 | |||
239 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { | ||
240 | await sendVerifyUserEmail(user) | ||
241 | } | ||
242 | |||
243 | Notifier.Instance.notifyOnNewUserRegistration(user) | ||
244 | |||
245 | Hooks.runAction('action:api.user.registered', { body, user, account, videoChannel, req, res }) | ||
246 | |||
247 | return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() | ||
248 | } | ||
249 | |||
250 | async function unblockUser (req: express.Request, res: express.Response) { | 187 | async function unblockUser (req: express.Request, res: express.Response) { |
251 | const user = res.locals.user | 188 | const user = res.locals.user |
252 | 189 | ||
@@ -360,28 +297,6 @@ async function resetUserPassword (req: express.Request, res: express.Response) { | |||
360 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | 297 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
361 | } | 298 | } |
362 | 299 | ||
363 | async function reSendVerifyUserEmail (req: express.Request, res: express.Response) { | ||
364 | const user = res.locals.user | ||
365 | |||
366 | await sendVerifyUserEmail(user) | ||
367 | |||
368 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
369 | } | ||
370 | |||
371 | async function verifyUserEmail (req: express.Request, res: express.Response) { | ||
372 | const user = res.locals.user | ||
373 | user.emailVerified = true | ||
374 | |||
375 | if (req.body.isPendingEmail === true) { | ||
376 | user.email = user.pendingEmail | ||
377 | user.pendingEmail = null | ||
378 | } | ||
379 | |||
380 | await user.save() | ||
381 | |||
382 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
383 | } | ||
384 | |||
385 | async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) { | 300 | async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) { |
386 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) | 301 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) |
387 | 302 | ||
diff --git a/server/controllers/api/users/registrations.ts b/server/controllers/api/users/registrations.ts new file mode 100644 index 000000000..5e213d6cc --- /dev/null +++ b/server/controllers/api/users/registrations.ts | |||
@@ -0,0 +1,249 @@ | |||
1 | import express from 'express' | ||
2 | import { Emailer } from '@server/lib/emailer' | ||
3 | import { Hooks } from '@server/lib/plugins/hooks' | ||
4 | import { UserRegistrationModel } from '@server/models/user/user-registration' | ||
5 | import { pick } from '@shared/core-utils' | ||
6 | import { | ||
7 | HttpStatusCode, | ||
8 | UserRegister, | ||
9 | UserRegistrationRequest, | ||
10 | UserRegistrationState, | ||
11 | UserRegistrationUpdateState, | ||
12 | UserRight | ||
13 | } from '@shared/models' | ||
14 | import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' | ||
15 | import { logger } from '../../../helpers/logger' | ||
16 | import { CONFIG } from '../../../initializers/config' | ||
17 | import { Notifier } from '../../../lib/notifier' | ||
18 | import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user' | ||
19 | import { | ||
20 | acceptOrRejectRegistrationValidator, | ||
21 | asyncMiddleware, | ||
22 | asyncRetryTransactionMiddleware, | ||
23 | authenticate, | ||
24 | buildRateLimiter, | ||
25 | ensureUserHasRight, | ||
26 | ensureUserRegistrationAllowedFactory, | ||
27 | ensureUserRegistrationAllowedForIP, | ||
28 | getRegistrationValidator, | ||
29 | listRegistrationsValidator, | ||
30 | paginationValidator, | ||
31 | setDefaultPagination, | ||
32 | setDefaultSort, | ||
33 | userRegistrationsSortValidator, | ||
34 | usersDirectRegistrationValidator, | ||
35 | usersRequestRegistrationValidator | ||
36 | } from '../../../middlewares' | ||
37 | |||
38 | const auditLogger = auditLoggerFactory('users') | ||
39 | |||
40 | const registrationRateLimiter = buildRateLimiter({ | ||
41 | windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, | ||
42 | max: CONFIG.RATES_LIMIT.SIGNUP.MAX, | ||
43 | skipFailedRequests: true | ||
44 | }) | ||
45 | |||
46 | const registrationsRouter = express.Router() | ||
47 | |||
48 | registrationsRouter.post('/registrations/request', | ||
49 | registrationRateLimiter, | ||
50 | asyncMiddleware(ensureUserRegistrationAllowedFactory('request-registration')), | ||
51 | ensureUserRegistrationAllowedForIP, | ||
52 | asyncMiddleware(usersRequestRegistrationValidator), | ||
53 | asyncRetryTransactionMiddleware(requestRegistration) | ||
54 | ) | ||
55 | |||
56 | registrationsRouter.post('/registrations/:registrationId/accept', | ||
57 | authenticate, | ||
58 | ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS), | ||
59 | asyncMiddleware(acceptOrRejectRegistrationValidator), | ||
60 | asyncRetryTransactionMiddleware(acceptRegistration) | ||
61 | ) | ||
62 | registrationsRouter.post('/registrations/:registrationId/reject', | ||
63 | authenticate, | ||
64 | ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS), | ||
65 | asyncMiddleware(acceptOrRejectRegistrationValidator), | ||
66 | asyncRetryTransactionMiddleware(rejectRegistration) | ||
67 | ) | ||
68 | |||
69 | registrationsRouter.delete('/registrations/:registrationId', | ||
70 | authenticate, | ||
71 | ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS), | ||
72 | asyncMiddleware(getRegistrationValidator), | ||
73 | asyncRetryTransactionMiddleware(deleteRegistration) | ||
74 | ) | ||
75 | |||
76 | registrationsRouter.get('/registrations', | ||
77 | authenticate, | ||
78 | ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS), | ||
79 | paginationValidator, | ||
80 | userRegistrationsSortValidator, | ||
81 | setDefaultSort, | ||
82 | setDefaultPagination, | ||
83 | listRegistrationsValidator, | ||
84 | asyncMiddleware(listRegistrations) | ||
85 | ) | ||
86 | |||
87 | registrationsRouter.post('/register', | ||
88 | registrationRateLimiter, | ||
89 | asyncMiddleware(ensureUserRegistrationAllowedFactory('direct-registration')), | ||
90 | ensureUserRegistrationAllowedForIP, | ||
91 | asyncMiddleware(usersDirectRegistrationValidator), | ||
92 | asyncRetryTransactionMiddleware(registerUser) | ||
93 | ) | ||
94 | |||
95 | // --------------------------------------------------------------------------- | ||
96 | |||
97 | export { | ||
98 | registrationsRouter | ||
99 | } | ||
100 | |||
101 | // --------------------------------------------------------------------------- | ||
102 | |||
103 | async function requestRegistration (req: express.Request, res: express.Response) { | ||
104 | const body: UserRegistrationRequest = req.body | ||
105 | |||
106 | const registration = new UserRegistrationModel({ | ||
107 | ...pick(body, [ 'username', 'password', 'email', 'registrationReason' ]), | ||
108 | |||
109 | accountDisplayName: body.displayName, | ||
110 | channelDisplayName: body.channel?.displayName, | ||
111 | channelHandle: body.channel?.name, | ||
112 | |||
113 | state: UserRegistrationState.PENDING, | ||
114 | |||
115 | emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null | ||
116 | }) | ||
117 | |||
118 | await registration.save() | ||
119 | |||
120 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { | ||
121 | await sendVerifyRegistrationEmail(registration) | ||
122 | } | ||
123 | |||
124 | Notifier.Instance.notifyOnNewRegistrationRequest(registration) | ||
125 | |||
126 | Hooks.runAction('action:api.user.requested-registration', { body, registration, req, res }) | ||
127 | |||
128 | return res.json(registration.toFormattedJSON()) | ||
129 | } | ||
130 | |||
131 | // --------------------------------------------------------------------------- | ||
132 | |||
133 | async function acceptRegistration (req: express.Request, res: express.Response) { | ||
134 | const registration = res.locals.userRegistration | ||
135 | const body: UserRegistrationUpdateState = req.body | ||
136 | |||
137 | const userToCreate = buildUser({ | ||
138 | username: registration.username, | ||
139 | password: registration.password, | ||
140 | email: registration.email, | ||
141 | emailVerified: registration.emailVerified | ||
142 | }) | ||
143 | // We already encrypted password in registration model | ||
144 | userToCreate.skipPasswordEncryption = true | ||
145 | |||
146 | // TODO: handle conflicts if someone else created a channel handle/user handle/user email between registration and approval | ||
147 | |||
148 | const { user } = await createUserAccountAndChannelAndPlaylist({ | ||
149 | userToCreate, | ||
150 | userDisplayName: registration.accountDisplayName, | ||
151 | channelNames: registration.channelHandle && registration.channelDisplayName | ||
152 | ? { | ||
153 | name: registration.channelHandle, | ||
154 | displayName: registration.channelDisplayName | ||
155 | } | ||
156 | : undefined | ||
157 | }) | ||
158 | |||
159 | registration.userId = user.id | ||
160 | registration.state = UserRegistrationState.ACCEPTED | ||
161 | registration.moderationResponse = body.moderationResponse | ||
162 | |||
163 | await registration.save() | ||
164 | |||
165 | logger.info('Registration of %s accepted', registration.username) | ||
166 | |||
167 | if (body.preventEmailDelivery !== true) { | ||
168 | Emailer.Instance.addUserRegistrationRequestProcessedJob(registration) | ||
169 | } | ||
170 | |||
171 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
172 | } | ||
173 | |||
174 | async function rejectRegistration (req: express.Request, res: express.Response) { | ||
175 | const registration = res.locals.userRegistration | ||
176 | const body: UserRegistrationUpdateState = req.body | ||
177 | |||
178 | registration.state = UserRegistrationState.REJECTED | ||
179 | registration.moderationResponse = body.moderationResponse | ||
180 | |||
181 | await registration.save() | ||
182 | |||
183 | if (body.preventEmailDelivery !== true) { | ||
184 | Emailer.Instance.addUserRegistrationRequestProcessedJob(registration) | ||
185 | } | ||
186 | |||
187 | logger.info('Registration of %s rejected', registration.username) | ||
188 | |||
189 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
190 | } | ||
191 | |||
192 | // --------------------------------------------------------------------------- | ||
193 | |||
194 | async function deleteRegistration (req: express.Request, res: express.Response) { | ||
195 | const registration = res.locals.userRegistration | ||
196 | |||
197 | await registration.destroy() | ||
198 | |||
199 | logger.info('Registration of %s deleted', registration.username) | ||
200 | |||
201 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
202 | } | ||
203 | |||
204 | // --------------------------------------------------------------------------- | ||
205 | |||
206 | async function listRegistrations (req: express.Request, res: express.Response) { | ||
207 | const resultList = await UserRegistrationModel.listForApi({ | ||
208 | start: req.query.start, | ||
209 | count: req.query.count, | ||
210 | sort: req.query.sort, | ||
211 | search: req.query.search | ||
212 | }) | ||
213 | |||
214 | return res.json({ | ||
215 | total: resultList.total, | ||
216 | data: resultList.data.map(d => d.toFormattedJSON()) | ||
217 | }) | ||
218 | } | ||
219 | |||
220 | // --------------------------------------------------------------------------- | ||
221 | |||
222 | async function registerUser (req: express.Request, res: express.Response) { | ||
223 | const body: UserRegister = req.body | ||
224 | |||
225 | const userToCreate = buildUser({ | ||
226 | ...pick(body, [ 'username', 'password', 'email' ]), | ||
227 | |||
228 | emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null | ||
229 | }) | ||
230 | |||
231 | const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({ | ||
232 | userToCreate, | ||
233 | userDisplayName: body.displayName || undefined, | ||
234 | channelNames: body.channel | ||
235 | }) | ||
236 | |||
237 | auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) | ||
238 | logger.info('User %s with its channel and account registered.', body.username) | ||
239 | |||
240 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { | ||
241 | await sendVerifyUserEmail(user) | ||
242 | } | ||
243 | |||
244 | Notifier.Instance.notifyOnNewDirectRegistration(user) | ||
245 | |||
246 | Hooks.runAction('action:api.user.registered', { body, user, account, videoChannel, req, res }) | ||
247 | |||
248 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
249 | } | ||
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index f8a607170..947f7ca77 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts | |||
@@ -15,7 +15,7 @@ import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/vid | |||
15 | import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model' | 15 | import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model' |
16 | import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model' | 16 | import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model' |
17 | import { resetSequelizeInstance } from '../../helpers/database-utils' | 17 | import { resetSequelizeInstance } from '../../helpers/database-utils' |
18 | import { buildNSFWFilter, createReqFiles } from '../../helpers/express-utils' | 18 | import { createReqFiles } from '../../helpers/express-utils' |
19 | import { logger } from '../../helpers/logger' | 19 | import { logger } from '../../helpers/logger' |
20 | import { getFormattedObjects } from '../../helpers/utils' | 20 | import { getFormattedObjects } from '../../helpers/utils' |
21 | import { CONFIG } from '../../initializers/config' | 21 | import { CONFIG } from '../../initializers/config' |
@@ -474,10 +474,7 @@ async function getVideoPlaylistVideos (req: express.Request, res: express.Respon | |||
474 | 'filter:api.video-playlist.videos.list.result' | 474 | 'filter:api.video-playlist.videos.list.result' |
475 | ) | 475 | ) |
476 | 476 | ||
477 | const options = { | 477 | const options = { accountId: user?.Account?.id } |
478 | displayNSFW: buildNSFWFilter(res, req.query.nsfw), | ||
479 | accountId: user ? user.Account.id : undefined | ||
480 | } | ||
481 | return res.json(getFormattedObjects(resultList.data, resultList.total, options)) | 478 | return res.json(getFormattedObjects(resultList.data, resultList.total, options)) |
482 | } | 479 | } |
483 | 480 | ||
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index 44d64776c..70ca21500 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { MCommentFormattable } from '@server/types/models' | ||
1 | import express from 'express' | 2 | import express from 'express' |
3 | |||
2 | import { ResultList, ThreadsResultList, UserRight, VideoCommentCreate } from '../../../../shared/models' | 4 | import { ResultList, ThreadsResultList, UserRight, VideoCommentCreate } from '../../../../shared/models' |
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | 5 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' |
4 | import { VideoCommentThreads } from '../../../../shared/models/videos/comment/video-comment.model' | 6 | import { VideoCommentThreads } from '../../../../shared/models/videos/comment/video-comment.model' |
@@ -109,7 +111,7 @@ async function listVideoThreads (req: express.Request, res: express.Response) { | |||
109 | const video = res.locals.onlyVideo | 111 | const video = res.locals.onlyVideo |
110 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined | 112 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined |
111 | 113 | ||
112 | let resultList: ThreadsResultList<VideoCommentModel> | 114 | let resultList: ThreadsResultList<MCommentFormattable> |
113 | 115 | ||
114 | if (video.commentsEnabled === true) { | 116 | if (video.commentsEnabled === true) { |
115 | const apiOptions = await Hooks.wrapObject({ | 117 | const apiOptions = await Hooks.wrapObject({ |
@@ -144,12 +146,11 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo | |||
144 | const video = res.locals.onlyVideo | 146 | const video = res.locals.onlyVideo |
145 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined | 147 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined |
146 | 148 | ||
147 | let resultList: ResultList<VideoCommentModel> | 149 | let resultList: ResultList<MCommentFormattable> |
148 | 150 | ||
149 | if (video.commentsEnabled === true) { | 151 | if (video.commentsEnabled === true) { |
150 | const apiOptions = await Hooks.wrapObject({ | 152 | const apiOptions = await Hooks.wrapObject({ |
151 | videoId: video.id, | 153 | videoId: video.id, |
152 | isVideoOwned: video.isOwned(), | ||
153 | threadId: res.locals.videoCommentThread.id, | 154 | threadId: res.locals.videoCommentThread.id, |
154 | user | 155 | user |
155 | }, 'filter:api.video-thread-comments.list.params') | 156 | }, 'filter:api.video-thread-comments.list.params') |
diff --git a/server/controllers/api/videos/token.ts b/server/controllers/api/videos/token.ts index 009b6dfb6..22387c3e8 100644 --- a/server/controllers/api/videos/token.ts +++ b/server/controllers/api/videos/token.ts | |||
@@ -22,7 +22,7 @@ export { | |||
22 | function generateToken (req: express.Request, res: express.Response) { | 22 | function generateToken (req: express.Request, res: express.Response) { |
23 | const video = res.locals.onlyVideo | 23 | const video = res.locals.onlyVideo |
24 | 24 | ||
25 | const { token, expires } = VideoTokensManager.Instance.create(video.uuid) | 25 | const { token, expires } = VideoTokensManager.Instance.create({ videoUUID: video.uuid, user: res.locals.oauth.token.User }) |
26 | 26 | ||
27 | return res.json({ | 27 | return res.json({ |
28 | files: { | 28 | files: { |
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 772fe734d..ef810a842 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -285,8 +285,8 @@ function addVideosToFeed (feed: Feed, videos: VideoModel[]) { | |||
285 | content: toSafeHtml(video.description), | 285 | content: toSafeHtml(video.description), |
286 | author: [ | 286 | author: [ |
287 | { | 287 | { |
288 | name: video.VideoChannel.Account.getDisplayName(), | 288 | name: video.VideoChannel.getDisplayName(), |
289 | link: video.VideoChannel.Account.Actor.url | 289 | link: video.VideoChannel.Actor.url |
290 | } | 290 | } |
291 | ], | 291 | ], |
292 | date: video.publishedAt, | 292 | date: video.publishedAt, |
diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts index 19a8b2bc9..c4f3a8889 100644 --- a/server/controllers/tracker.ts +++ b/server/controllers/tracker.ts | |||
@@ -1,17 +1,22 @@ | |||
1 | import { Server as TrackerServer } from 'bittorrent-tracker' | 1 | import { Server as TrackerServer } from 'bittorrent-tracker' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { createServer } from 'http' | 3 | import { createServer } from 'http' |
4 | import LRUCache from 'lru-cache' | ||
4 | import proxyAddr from 'proxy-addr' | 5 | import proxyAddr from 'proxy-addr' |
5 | import { WebSocketServer } from 'ws' | 6 | import { WebSocketServer } from 'ws' |
6 | import { Redis } from '@server/lib/redis' | ||
7 | import { logger } from '../helpers/logger' | 7 | import { logger } from '../helpers/logger' |
8 | import { CONFIG } from '../initializers/config' | 8 | import { CONFIG } from '../initializers/config' |
9 | import { TRACKER_RATE_LIMITS } from '../initializers/constants' | 9 | import { LRU_CACHE, TRACKER_RATE_LIMITS } from '../initializers/constants' |
10 | import { VideoFileModel } from '../models/video/video-file' | 10 | import { VideoFileModel } from '../models/video/video-file' |
11 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 11 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
12 | 12 | ||
13 | const trackerRouter = express.Router() | 13 | const trackerRouter = express.Router() |
14 | 14 | ||
15 | const blockedIPs = new LRUCache<string, boolean>({ | ||
16 | max: LRU_CACHE.TRACKER_IPS.MAX_SIZE, | ||
17 | ttl: TRACKER_RATE_LIMITS.BLOCK_IP_LIFETIME | ||
18 | }) | ||
19 | |||
15 | let peersIps = {} | 20 | let peersIps = {} |
16 | let peersIpInfoHash = {} | 21 | let peersIpInfoHash = {} |
17 | runPeersChecker() | 22 | runPeersChecker() |
@@ -55,8 +60,7 @@ const trackerServer = new TrackerServer({ | |||
55 | 60 | ||
56 | // Close socket connection and block IP for a few time | 61 | // Close socket connection and block IP for a few time |
57 | if (params.type === 'ws') { | 62 | if (params.type === 'ws') { |
58 | Redis.Instance.setTrackerBlockIP(ip) | 63 | blockedIPs.set(ip, true) |
59 | .catch(err => logger.error('Cannot set tracker block ip.', { err })) | ||
60 | 64 | ||
61 | // setTimeout to wait filter response | 65 | // setTimeout to wait filter response |
62 | setTimeout(() => params.socket.close(), 0) | 66 | setTimeout(() => params.socket.close(), 0) |
@@ -102,26 +106,22 @@ function createWebsocketTrackerServer (app: express.Application) { | |||
102 | if (request.url === '/tracker/socket') { | 106 | if (request.url === '/tracker/socket') { |
103 | const ip = proxyAddr(request, CONFIG.TRUST_PROXY) | 107 | const ip = proxyAddr(request, CONFIG.TRUST_PROXY) |
104 | 108 | ||
105 | Redis.Instance.doesTrackerBlockIPExist(ip) | 109 | if (blockedIPs.has(ip)) { |
106 | .then(result => { | 110 | logger.debug('Blocking IP %s from tracker.', ip) |
107 | if (result === true) { | ||
108 | logger.debug('Blocking IP %s from tracker.', ip) | ||
109 | 111 | ||
110 | socket.write('HTTP/1.1 403 Forbidden\r\n\r\n') | 112 | socket.write('HTTP/1.1 403 Forbidden\r\n\r\n') |
111 | socket.destroy() | 113 | socket.destroy() |
112 | return | 114 | return |
113 | } | 115 | } |
114 | 116 | ||
115 | // FIXME: typings | 117 | // FIXME: typings |
116 | return wss.handleUpgrade(request, socket as any, head, ws => wss.emit('connection', ws, request)) | 118 | return wss.handleUpgrade(request, socket as any, head, ws => wss.emit('connection', ws, request)) |
117 | }) | ||
118 | .catch(err => logger.error('Cannot check if tracker block ip exists.', { err })) | ||
119 | } | 119 | } |
120 | 120 | ||
121 | // Don't destroy socket, we have Socket.IO too | 121 | // Don't destroy socket, we have Socket.IO too |
122 | }) | 122 | }) |
123 | 123 | ||
124 | return server | 124 | return { server, trackerServer } |
125 | } | 125 | } |
126 | 126 | ||
127 | // --------------------------------------------------------------------------- | 127 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 3dc5504e3..b3ab3ac64 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -103,7 +103,13 @@ function checkMimetypeRegex (fileMimeType: string, mimeTypeRegex: string) { | |||
103 | // --------------------------------------------------------------------------- | 103 | // --------------------------------------------------------------------------- |
104 | 104 | ||
105 | function toCompleteUUID (value: string) { | 105 | function toCompleteUUID (value: string) { |
106 | if (isShortUUID(value)) return shortToUUID(value) | 106 | if (isShortUUID(value)) { |
107 | try { | ||
108 | return shortToUUID(value) | ||
109 | } catch { | ||
110 | return null | ||
111 | } | ||
112 | } | ||
107 | 113 | ||
108 | return value | 114 | return value |
109 | } | 115 | } |
diff --git a/server/helpers/custom-validators/user-registration.ts b/server/helpers/custom-validators/user-registration.ts new file mode 100644 index 000000000..9da0bb08a --- /dev/null +++ b/server/helpers/custom-validators/user-registration.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import validator from 'validator' | ||
2 | import { CONSTRAINTS_FIELDS, USER_REGISTRATION_STATES } from '../../initializers/constants' | ||
3 | import { exists } from './misc' | ||
4 | |||
5 | const USER_REGISTRATIONS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USER_REGISTRATIONS | ||
6 | |||
7 | function isRegistrationStateValid (value: string) { | ||
8 | return exists(value) && USER_REGISTRATION_STATES[value] !== undefined | ||
9 | } | ||
10 | |||
11 | function isRegistrationModerationResponseValid (value: string) { | ||
12 | return exists(value) && validator.isLength(value, USER_REGISTRATIONS_CONSTRAINTS_FIELDS.MODERATOR_MESSAGE) | ||
13 | } | ||
14 | |||
15 | function isRegistrationReasonValid (value: string) { | ||
16 | return exists(value) && validator.isLength(value, USER_REGISTRATIONS_CONSTRAINTS_FIELDS.REASON_MESSAGE) | ||
17 | } | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | export { | ||
22 | isRegistrationStateValid, | ||
23 | isRegistrationModerationResponseValid, | ||
24 | isRegistrationReasonValid | ||
25 | } | ||
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts index 59ba005fe..d5b09ea03 100644 --- a/server/helpers/custom-validators/video-captions.ts +++ b/server/helpers/custom-validators/video-captions.ts | |||
@@ -8,10 +8,11 @@ function isVideoCaptionLanguageValid (value: any) { | |||
8 | return exists(value) && VIDEO_LANGUAGES[value] !== undefined | 8 | return exists(value) && VIDEO_LANGUAGES[value] !== undefined |
9 | } | 9 | } |
10 | 10 | ||
11 | const videoCaptionTypesRegex = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT) | 11 | // MacOS sends application/octet-stream |
12 | .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream | 12 | const videoCaptionTypesRegex = [ ...Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT), 'application/octet-stream' ] |
13 | .map(m => `(${m})`) | 13 | .map(m => `(${m})`) |
14 | .join('|') | 14 | .join('|') |
15 | |||
15 | function isVideoCaptionFile (files: UploadFilesForCheck, field: string) { | 16 | function isVideoCaptionFile (files: UploadFilesForCheck, field: string) { |
16 | return isFileValid({ | 17 | return isFileValid({ |
17 | files, | 18 | files, |
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts index af93aea56..da8962cb6 100644 --- a/server/helpers/custom-validators/video-imports.ts +++ b/server/helpers/custom-validators/video-imports.ts | |||
@@ -22,10 +22,11 @@ function isVideoImportStateValid (value: any) { | |||
22 | return exists(value) && VIDEO_IMPORT_STATES[value] !== undefined | 22 | return exists(value) && VIDEO_IMPORT_STATES[value] !== undefined |
23 | } | 23 | } |
24 | 24 | ||
25 | const videoTorrentImportRegex = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT) | 25 | // MacOS sends application/octet-stream |
26 | .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream | 26 | const videoTorrentImportRegex = [ ...Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT), 'application/octet-stream' ] |
27 | .map(m => `(${m})`) | 27 | .map(m => `(${m})`) |
28 | .join('|') | 28 | .join('|') |
29 | |||
29 | function isVideoImportTorrentFile (files: UploadFilesForCheck) { | 30 | function isVideoImportTorrentFile (files: UploadFilesForCheck) { |
30 | return isFileValid({ | 31 | return isFileValid({ |
31 | files, | 32 | files, |
diff --git a/server/helpers/decache.ts b/server/helpers/decache.ts index e31973b7a..08ab545e4 100644 --- a/server/helpers/decache.ts +++ b/server/helpers/decache.ts | |||
@@ -68,7 +68,7 @@ function searchCache (moduleName: string, callback: (current: NodeModule) => voi | |||
68 | }; | 68 | }; |
69 | 69 | ||
70 | function removeCachedPath (pluginPath: string) { | 70 | function removeCachedPath (pluginPath: string) { |
71 | const pathCache = (module.constructor as any)._pathCache | 71 | const pathCache = (module.constructor as any)._pathCache as { [ id: string ]: string[] } |
72 | 72 | ||
73 | Object.keys(pathCache).forEach(function (cacheKey) { | 73 | Object.keys(pathCache).forEach(function (cacheKey) { |
74 | if (cacheKey.includes(pluginPath)) { | 74 | if (cacheKey.includes(pluginPath)) { |
diff --git a/server/helpers/memoize.ts b/server/helpers/memoize.ts new file mode 100644 index 000000000..aa20e7d73 --- /dev/null +++ b/server/helpers/memoize.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import memoizee from 'memoizee' | ||
2 | |||
3 | export function Memoize (config?: memoizee.Options<any>) { | ||
4 | return function (_target, _key, descriptor: PropertyDescriptor) { | ||
5 | const oldFunction = descriptor.value | ||
6 | const newFunction = memoizee(oldFunction, config) | ||
7 | |||
8 | descriptor.value = function () { | ||
9 | return newFunction.apply(this, arguments) | ||
10 | } | ||
11 | } | ||
12 | } | ||
diff --git a/server/helpers/youtube-dl/youtube-dl-cli.ts b/server/helpers/youtube-dl/youtube-dl-cli.ts index a2f630953..765038cea 100644 --- a/server/helpers/youtube-dl/youtube-dl-cli.ts +++ b/server/helpers/youtube-dl/youtube-dl-cli.ts | |||
@@ -6,6 +6,7 @@ import { VideoResolution } from '@shared/models' | |||
6 | import { logger, loggerTagsFactory } from '../logger' | 6 | import { logger, loggerTagsFactory } from '../logger' |
7 | import { getProxy, isProxyEnabled } from '../proxy' | 7 | import { getProxy, isProxyEnabled } from '../proxy' |
8 | import { isBinaryResponse, peertubeGot } from '../requests' | 8 | import { isBinaryResponse, peertubeGot } from '../requests' |
9 | import { OptionsOfBufferResponseBody } from 'got/dist/source' | ||
9 | 10 | ||
10 | const lTags = loggerTagsFactory('youtube-dl') | 11 | const lTags = loggerTagsFactory('youtube-dl') |
11 | 12 | ||
@@ -28,7 +29,16 @@ export class YoutubeDLCLI { | |||
28 | 29 | ||
29 | logger.info('Updating youtubeDL binary from %s.', url, lTags()) | 30 | logger.info('Updating youtubeDL binary from %s.', url, lTags()) |
30 | 31 | ||
31 | const gotOptions = { context: { bodyKBLimit: 20_000 }, responseType: 'buffer' as 'buffer' } | 32 | const gotOptions: OptionsOfBufferResponseBody = { |
33 | context: { bodyKBLimit: 20_000 }, | ||
34 | responseType: 'buffer' as 'buffer' | ||
35 | } | ||
36 | |||
37 | if (process.env.YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN) { | ||
38 | gotOptions.headers = { | ||
39 | authorization: 'Bearer ' + process.env.YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN | ||
40 | } | ||
41 | } | ||
32 | 42 | ||
33 | try { | 43 | try { |
34 | let gotResult = await peertubeGot(url, gotOptions) | 44 | let gotResult = await peertubeGot(url, gotOptions) |
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index c83fef425..0df7414be 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts | |||
@@ -4,7 +4,7 @@ import { getFFmpegVersion } from '@server/helpers/ffmpeg' | |||
4 | import { uniqify } from '@shared/core-utils' | 4 | import { uniqify } from '@shared/core-utils' |
5 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' | 5 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' |
6 | import { RecentlyAddedStrategy } from '../../shared/models/redundancy' | 6 | import { RecentlyAddedStrategy } from '../../shared/models/redundancy' |
7 | import { isProdInstance, parseSemVersion } from '../helpers/core-utils' | 7 | import { isProdInstance, parseBytes, parseSemVersion } from '../helpers/core-utils' |
8 | import { isArray } from '../helpers/custom-validators/misc' | 8 | import { isArray } from '../helpers/custom-validators/misc' |
9 | import { logger } from '../helpers/logger' | 9 | import { logger } from '../helpers/logger' |
10 | import { ApplicationModel, getServerActor } from '../models/application/application' | 10 | import { ApplicationModel, getServerActor } from '../models/application/application' |
@@ -116,6 +116,11 @@ function checkEmailConfig () { | |||
116 | throw new Error('Emailer is disabled but you require signup email verification.') | 116 | throw new Error('Emailer is disabled but you require signup email verification.') |
117 | } | 117 | } |
118 | 118 | ||
119 | if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_APPROVAL) { | ||
120 | // eslint-disable-next-line max-len | ||
121 | logger.warn('Emailer is disabled but signup approval is enabled: PeerTube will not be able to send an email to the user upon acceptance/rejection of the registration request') | ||
122 | } | ||
123 | |||
119 | if (CONFIG.CONTACT_FORM.ENABLED) { | 124 | if (CONFIG.CONTACT_FORM.ENABLED) { |
120 | logger.warn('Emailer is disabled so the contact form will not work.') | 125 | logger.warn('Emailer is disabled so the contact form will not work.') |
121 | } | 126 | } |
@@ -174,7 +179,8 @@ function checkRemoteRedundancyConfig () { | |||
174 | function checkStorageConfig () { | 179 | function checkStorageConfig () { |
175 | // Check storage directory locations | 180 | // Check storage directory locations |
176 | if (isProdInstance()) { | 181 | if (isProdInstance()) { |
177 | const configStorage = config.get('storage') | 182 | const configStorage = config.get<{ [ name: string ]: string }>('storage') |
183 | |||
178 | for (const key of Object.keys(configStorage)) { | 184 | for (const key of Object.keys(configStorage)) { |
179 | if (configStorage[key].startsWith('storage/')) { | 185 | if (configStorage[key].startsWith('storage/')) { |
180 | logger.warn( | 186 | logger.warn( |
@@ -278,6 +284,11 @@ function checkObjectStorageConfig () { | |||
278 | 'Object storage bucket prefixes should be set to different values when the same bucket is used for both types of video.' | 284 | 'Object storage bucket prefixes should be set to different values when the same bucket is used for both types of video.' |
279 | ) | 285 | ) |
280 | } | 286 | } |
287 | |||
288 | if (CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART > parseBytes('250MB')) { | ||
289 | // eslint-disable-next-line max-len | ||
290 | logger.warn(`Object storage max upload part seems to have a big value (${CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART} bytes). Consider using a lower one (like 100MB).`) | ||
291 | } | ||
281 | } | 292 | } |
282 | } | 293 | } |
283 | 294 | ||
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 39713a266..8b4d49180 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -13,6 +13,7 @@ function checkMissedConfig () { | |||
13 | 'webserver.https', 'webserver.hostname', 'webserver.port', | 13 | 'webserver.https', 'webserver.hostname', 'webserver.port', |
14 | 'secrets.peertube', | 14 | 'secrets.peertube', |
15 | 'trust_proxy', | 15 | 'trust_proxy', |
16 | 'oauth2.token_lifetime.access_token', 'oauth2.token_lifetime.refresh_token', | ||
16 | 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', | 17 | 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', |
17 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', | 18 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', |
18 | 'email.body.signature', 'email.subject.prefix', | 19 | 'email.body.signature', 'email.subject.prefix', |
@@ -27,7 +28,7 @@ function checkMissedConfig () { | |||
27 | 'csp.enabled', 'csp.report_only', 'csp.report_uri', | 28 | 'csp.enabled', 'csp.report_only', 'csp.report_uri', |
28 | 'security.frameguard.enabled', | 29 | 'security.frameguard.enabled', |
29 | 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled', | 30 | 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled', |
30 | 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', 'signup.minimum_age', | 31 | 'signup.enabled', 'signup.limit', 'signup.requires_approval', 'signup.requires_email_verification', 'signup.minimum_age', |
31 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', | 32 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', |
32 | 'redundancy.videos.strategies', 'redundancy.videos.check_interval', | 33 | 'redundancy.videos.strategies', 'redundancy.videos.check_interval', |
33 | 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled', | 34 | 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled', |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index c2f8b19fd..9685e7bfc 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -149,6 +149,12 @@ const CONFIG = { | |||
149 | HOSTNAME: config.get<string>('webserver.hostname'), | 149 | HOSTNAME: config.get<string>('webserver.hostname'), |
150 | PORT: config.get<number>('webserver.port') | 150 | PORT: config.get<number>('webserver.port') |
151 | }, | 151 | }, |
152 | OAUTH2: { | ||
153 | TOKEN_LIFETIME: { | ||
154 | ACCESS_TOKEN: parseDurationToMs(config.get<string>('oauth2.token_lifetime.access_token')), | ||
155 | REFRESH_TOKEN: parseDurationToMs(config.get<string>('oauth2.token_lifetime.refresh_token')) | ||
156 | } | ||
157 | }, | ||
152 | RATES_LIMIT: { | 158 | RATES_LIMIT: { |
153 | API: { | 159 | API: { |
154 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')), | 160 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')), |
@@ -299,6 +305,7 @@ const CONFIG = { | |||
299 | }, | 305 | }, |
300 | SIGNUP: { | 306 | SIGNUP: { |
301 | get ENABLED () { return config.get<boolean>('signup.enabled') }, | 307 | get ENABLED () { return config.get<boolean>('signup.enabled') }, |
308 | get REQUIRES_APPROVAL () { return config.get<boolean>('signup.requires_approval') }, | ||
302 | get LIMIT () { return config.get<number>('signup.limit') }, | 309 | get LIMIT () { return config.get<number>('signup.limit') }, |
303 | get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') }, | 310 | get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') }, |
304 | get MINIMUM_AGE () { return config.get<number>('signup.minimum_age') }, | 311 | get MINIMUM_AGE () { return config.get<number>('signup.minimum_age') }, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 0e56f0c9f..992c86ed2 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -6,6 +6,7 @@ import { randomInt, root } from '@shared/core-utils' | |||
6 | import { | 6 | import { |
7 | AbuseState, | 7 | AbuseState, |
8 | JobType, | 8 | JobType, |
9 | UserRegistrationState, | ||
9 | VideoChannelSyncState, | 10 | VideoChannelSyncState, |
10 | VideoImportState, | 11 | VideoImportState, |
11 | VideoPrivacy, | 12 | VideoPrivacy, |
@@ -25,7 +26,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
25 | 26 | ||
26 | // --------------------------------------------------------------------------- | 27 | // --------------------------------------------------------------------------- |
27 | 28 | ||
28 | const LAST_MIGRATION_VERSION = 745 | 29 | const LAST_MIGRATION_VERSION = 755 |
29 | 30 | ||
30 | // --------------------------------------------------------------------------- | 31 | // --------------------------------------------------------------------------- |
31 | 32 | ||
@@ -78,6 +79,8 @@ const SORTABLE_COLUMNS = { | |||
78 | ACCOUNT_FOLLOWERS: [ 'createdAt' ], | 79 | ACCOUNT_FOLLOWERS: [ 'createdAt' ], |
79 | CHANNEL_FOLLOWERS: [ 'createdAt' ], | 80 | CHANNEL_FOLLOWERS: [ 'createdAt' ], |
80 | 81 | ||
82 | USER_REGISTRATIONS: [ 'createdAt', 'state' ], | ||
83 | |||
81 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ], | 84 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ], |
82 | 85 | ||
83 | // Don't forget to update peertube-search-index with the same values | 86 | // Don't forget to update peertube-search-index with the same values |
@@ -101,11 +104,6 @@ const SORTABLE_COLUMNS = { | |||
101 | VIDEO_REDUNDANCIES: [ 'name' ] | 104 | VIDEO_REDUNDANCIES: [ 'name' ] |
102 | } | 105 | } |
103 | 106 | ||
104 | const OAUTH_LIFETIME = { | ||
105 | ACCESS_TOKEN: 3600 * 24, // 1 day, for upload | ||
106 | REFRESH_TOKEN: 1209600 // 2 weeks | ||
107 | } | ||
108 | |||
109 | const ROUTE_CACHE_LIFETIME = { | 107 | const ROUTE_CACHE_LIFETIME = { |
110 | FEEDS: '15 minutes', | 108 | FEEDS: '15 minutes', |
111 | ROBOTS: '2 hours', | 109 | ROBOTS: '2 hours', |
@@ -295,6 +293,10 @@ const CONSTRAINTS_FIELDS = { | |||
295 | ABUSE_MESSAGES: { | 293 | ABUSE_MESSAGES: { |
296 | MESSAGE: { min: 2, max: 3000 } // Length | 294 | MESSAGE: { min: 2, max: 3000 } // Length |
297 | }, | 295 | }, |
296 | USER_REGISTRATIONS: { | ||
297 | REASON_MESSAGE: { min: 2, max: 3000 }, // Length | ||
298 | MODERATOR_MESSAGE: { min: 2, max: 3000 } // Length | ||
299 | }, | ||
298 | VIDEO_BLACKLIST: { | 300 | VIDEO_BLACKLIST: { |
299 | REASON: { min: 2, max: 300 } // Length | 301 | REASON: { min: 2, max: 300 } // Length |
300 | }, | 302 | }, |
@@ -521,6 +523,12 @@ const ABUSE_STATES: { [ id in AbuseState ]: string } = { | |||
521 | [AbuseState.ACCEPTED]: 'Accepted' | 523 | [AbuseState.ACCEPTED]: 'Accepted' |
522 | } | 524 | } |
523 | 525 | ||
526 | const USER_REGISTRATION_STATES: { [ id in UserRegistrationState ]: string } = { | ||
527 | [UserRegistrationState.PENDING]: 'Pending', | ||
528 | [UserRegistrationState.REJECTED]: 'Rejected', | ||
529 | [UserRegistrationState.ACCEPTED]: 'Accepted' | ||
530 | } | ||
531 | |||
524 | const VIDEO_PLAYLIST_PRIVACIES: { [ id in VideoPlaylistPrivacy ]: string } = { | 532 | const VIDEO_PLAYLIST_PRIVACIES: { [ id in VideoPlaylistPrivacy ]: string } = { |
525 | [VideoPlaylistPrivacy.PUBLIC]: 'Public', | 533 | [VideoPlaylistPrivacy.PUBLIC]: 'Public', |
526 | [VideoPlaylistPrivacy.UNLISTED]: 'Unlisted', | 534 | [VideoPlaylistPrivacy.UNLISTED]: 'Unlisted', |
@@ -665,7 +673,7 @@ const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days | |||
665 | 673 | ||
666 | const TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME = 60000 * 10 // 10 minutes | 674 | const TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME = 60000 * 10 // 10 minutes |
667 | 675 | ||
668 | const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes | 676 | const EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes |
669 | 677 | ||
670 | const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = { | 678 | const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = { |
671 | DO_NOT_LIST: 'do_not_list', | 679 | DO_NOT_LIST: 'do_not_list', |
@@ -781,6 +789,9 @@ const LRU_CACHE = { | |||
781 | VIDEO_TOKENS: { | 789 | VIDEO_TOKENS: { |
782 | MAX_SIZE: 100_000, | 790 | MAX_SIZE: 100_000, |
783 | TTL: parseDurationToMs('8 hours') | 791 | TTL: parseDurationToMs('8 hours') |
792 | }, | ||
793 | TRACKER_IPS: { | ||
794 | MAX_SIZE: 100_000 | ||
784 | } | 795 | } |
785 | } | 796 | } |
786 | 797 | ||
@@ -884,7 +895,7 @@ const TRACKER_RATE_LIMITS = { | |||
884 | INTERVAL: 60000 * 5, // 5 minutes | 895 | INTERVAL: 60000 * 5, // 5 minutes |
885 | ANNOUNCES_PER_IP_PER_INFOHASH: 15, // maximum announces per torrent in the interval | 896 | ANNOUNCES_PER_IP_PER_INFOHASH: 15, // maximum announces per torrent in the interval |
886 | ANNOUNCES_PER_IP: 30, // maximum announces for all our torrents in the interval | 897 | ANNOUNCES_PER_IP: 30, // maximum announces for all our torrents in the interval |
887 | BLOCK_IP_LIFETIME: 60000 * 3 // 3 minutes | 898 | BLOCK_IP_LIFETIME: parseDurationToMs('3 minutes') |
888 | } | 899 | } |
889 | 900 | ||
890 | const P2P_MEDIA_LOADER_PEER_VERSION = 2 | 901 | const P2P_MEDIA_LOADER_PEER_VERSION = 2 |
@@ -1030,7 +1041,6 @@ export { | |||
1030 | JOB_ATTEMPTS, | 1041 | JOB_ATTEMPTS, |
1031 | AP_CLEANER, | 1042 | AP_CLEANER, |
1032 | LAST_MIGRATION_VERSION, | 1043 | LAST_MIGRATION_VERSION, |
1033 | OAUTH_LIFETIME, | ||
1034 | CUSTOM_HTML_TAG_COMMENTS, | 1044 | CUSTOM_HTML_TAG_COMMENTS, |
1035 | STATS_TIMESERIE, | 1045 | STATS_TIMESERIE, |
1036 | BROADCAST_CONCURRENCY, | 1046 | BROADCAST_CONCURRENCY, |
@@ -1072,13 +1082,14 @@ export { | |||
1072 | VIDEO_TRANSCODING_FPS, | 1082 | VIDEO_TRANSCODING_FPS, |
1073 | FFMPEG_NICE, | 1083 | FFMPEG_NICE, |
1074 | ABUSE_STATES, | 1084 | ABUSE_STATES, |
1085 | USER_REGISTRATION_STATES, | ||
1075 | LRU_CACHE, | 1086 | LRU_CACHE, |
1076 | REQUEST_TIMEOUTS, | 1087 | REQUEST_TIMEOUTS, |
1077 | MAX_LOCAL_VIEWER_WATCH_SECTIONS, | 1088 | MAX_LOCAL_VIEWER_WATCH_SECTIONS, |
1078 | USER_PASSWORD_RESET_LIFETIME, | 1089 | USER_PASSWORD_RESET_LIFETIME, |
1079 | USER_PASSWORD_CREATE_LIFETIME, | 1090 | USER_PASSWORD_CREATE_LIFETIME, |
1080 | MEMOIZE_TTL, | 1091 | MEMOIZE_TTL, |
1081 | USER_EMAIL_VERIFY_LIFETIME, | 1092 | EMAIL_VERIFY_LIFETIME, |
1082 | OVERVIEWS, | 1093 | OVERVIEWS, |
1083 | SCHEDULER_INTERVALS_MS, | 1094 | SCHEDULER_INTERVALS_MS, |
1084 | REPEAT_JOBS, | 1095 | REPEAT_JOBS, |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index f55f40df0..96145f489 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -5,7 +5,9 @@ import { TrackerModel } from '@server/models/server/tracker' | |||
5 | import { VideoTrackerModel } from '@server/models/server/video-tracker' | 5 | import { VideoTrackerModel } from '@server/models/server/video-tracker' |
6 | import { UserModel } from '@server/models/user/user' | 6 | import { UserModel } from '@server/models/user/user' |
7 | import { UserNotificationModel } from '@server/models/user/user-notification' | 7 | import { UserNotificationModel } from '@server/models/user/user-notification' |
8 | import { UserRegistrationModel } from '@server/models/user/user-registration' | ||
8 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | 9 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' |
10 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' | ||
9 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | 11 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' |
10 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' | 12 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' |
11 | import { VideoSourceModel } from '@server/models/video/video-source' | 13 | import { VideoSourceModel } from '@server/models/video/video-source' |
@@ -50,7 +52,6 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla | |||
50 | import { VideoTagModel } from '../models/video/video-tag' | 52 | import { VideoTagModel } from '../models/video/video-tag' |
51 | import { VideoViewModel } from '../models/view/video-view' | 53 | import { VideoViewModel } from '../models/view/video-view' |
52 | import { CONFIG } from './config' | 54 | import { CONFIG } from './config' |
53 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' | ||
54 | 55 | ||
55 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 56 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
56 | 57 | ||
@@ -155,7 +156,8 @@ async function initDatabaseModels (silent: boolean) { | |||
155 | PluginModel, | 156 | PluginModel, |
156 | ActorCustomPageModel, | 157 | ActorCustomPageModel, |
157 | VideoJobInfoModel, | 158 | VideoJobInfoModel, |
158 | VideoChannelSyncModel | 159 | VideoChannelSyncModel, |
160 | UserRegistrationModel | ||
159 | ]) | 161 | ]) |
160 | 162 | ||
161 | // Check extensions exist in the database | 163 | // Check extensions exist in the database |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index f5d8eedf1..f48f348a7 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -51,8 +51,7 @@ function removeCacheAndTmpDirectories () { | |||
51 | const tasks: Promise<any>[] = [] | 51 | const tasks: Promise<any>[] = [] |
52 | 52 | ||
53 | // Cache directories | 53 | // Cache directories |
54 | for (const key of Object.keys(cacheDirectories)) { | 54 | for (const dir of cacheDirectories) { |
55 | const dir = cacheDirectories[key] | ||
56 | tasks.push(removeDirectoryOrContent(dir)) | 55 | tasks.push(removeDirectoryOrContent(dir)) |
57 | } | 56 | } |
58 | 57 | ||
@@ -87,8 +86,7 @@ function createDirectoriesIfNotExist () { | |||
87 | } | 86 | } |
88 | 87 | ||
89 | // Cache directories | 88 | // Cache directories |
90 | for (const key of Object.keys(cacheDirectories)) { | 89 | for (const dir of cacheDirectories) { |
91 | const dir = cacheDirectories[key] | ||
92 | tasks.push(ensureDir(dir)) | 90 | tasks.push(ensureDir(dir)) |
93 | } | 91 | } |
94 | 92 | ||
diff --git a/server/initializers/migrations/0750-user-registration.ts b/server/initializers/migrations/0750-user-registration.ts new file mode 100644 index 000000000..15bbfd3fd --- /dev/null +++ b/server/initializers/migrations/0750-user-registration.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | |||
2 | import * as Sequelize from 'sequelize' | ||
3 | |||
4 | async function up (utils: { | ||
5 | transaction: Sequelize.Transaction | ||
6 | queryInterface: Sequelize.QueryInterface | ||
7 | sequelize: Sequelize.Sequelize | ||
8 | db: any | ||
9 | }): Promise<void> { | ||
10 | { | ||
11 | const query = ` | ||
12 | CREATE TABLE IF NOT EXISTS "userRegistration" ( | ||
13 | "id" serial, | ||
14 | "state" integer NOT NULL, | ||
15 | "registrationReason" text NOT NULL, | ||
16 | "moderationResponse" text, | ||
17 | "password" varchar(255), | ||
18 | "username" varchar(255) NOT NULL, | ||
19 | "email" varchar(400) NOT NULL, | ||
20 | "emailVerified" boolean, | ||
21 | "accountDisplayName" varchar(255), | ||
22 | "channelHandle" varchar(255), | ||
23 | "channelDisplayName" varchar(255), | ||
24 | "userId" integer REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE CASCADE, | ||
25 | "createdAt" timestamp with time zone NOT NULL, | ||
26 | "updatedAt" timestamp with time zone NOT NULL, | ||
27 | PRIMARY KEY ("id") | ||
28 | ); | ||
29 | ` | ||
30 | await utils.sequelize.query(query, { transaction: utils.transaction }) | ||
31 | } | ||
32 | |||
33 | { | ||
34 | await utils.queryInterface.addColumn('userNotification', 'userRegistrationId', { | ||
35 | type: Sequelize.INTEGER, | ||
36 | defaultValue: null, | ||
37 | allowNull: true, | ||
38 | references: { | ||
39 | model: 'userRegistration', | ||
40 | key: 'id' | ||
41 | }, | ||
42 | onUpdate: 'CASCADE', | ||
43 | onDelete: 'SET NULL' | ||
44 | }, { transaction: utils.transaction }) | ||
45 | } | ||
46 | } | ||
47 | |||
48 | async function down (utils: { | ||
49 | queryInterface: Sequelize.QueryInterface | ||
50 | transaction: Sequelize.Transaction | ||
51 | }) { | ||
52 | await utils.queryInterface.dropTable('videoChannelSync', { transaction: utils.transaction }) | ||
53 | } | ||
54 | |||
55 | export { | ||
56 | up, | ||
57 | down | ||
58 | } | ||
diff --git a/server/initializers/migrations/0755-unique-viewer-url.ts b/server/initializers/migrations/0755-unique-viewer-url.ts new file mode 100644 index 000000000..b3dff9258 --- /dev/null +++ b/server/initializers/migrations/0755-unique-viewer-url.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | const { transaction } = utils | ||
10 | |||
11 | const query = 'DELETE FROM "localVideoViewer" t1 ' + | ||
12 | 'USING (SELECT MIN(id) as id, "url" FROM "localVideoViewer" GROUP BY "url" HAVING COUNT(*) > 1) t2 ' + | ||
13 | 'WHERE t1."url" = t2."url" AND t1.id <> t2.id' | ||
14 | |||
15 | await utils.sequelize.query(query, { transaction }) | ||
16 | } | ||
17 | |||
18 | async function down (utils: { | ||
19 | queryInterface: Sequelize.QueryInterface | ||
20 | transaction: Sequelize.Transaction | ||
21 | }) { | ||
22 | } | ||
23 | |||
24 | export { | ||
25 | up, | ||
26 | down | ||
27 | } | ||
diff --git a/server/lib/auth/external-auth.ts b/server/lib/auth/external-auth.ts index 053112801..bc5b74257 100644 --- a/server/lib/auth/external-auth.ts +++ b/server/lib/auth/external-auth.ts | |||
@@ -1,26 +1,35 @@ | |||
1 | 1 | ||
2 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' | 2 | import { |
3 | isUserAdminFlagsValid, | ||
4 | isUserDisplayNameValid, | ||
5 | isUserRoleValid, | ||
6 | isUserUsernameValid, | ||
7 | isUserVideoQuotaDailyValid, | ||
8 | isUserVideoQuotaValid | ||
9 | } from '@server/helpers/custom-validators/users' | ||
3 | import { logger } from '@server/helpers/logger' | 10 | import { logger } from '@server/helpers/logger' |
4 | import { generateRandomString } from '@server/helpers/utils' | 11 | import { generateRandomString } from '@server/helpers/utils' |
5 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' | 12 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' |
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 13 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 14 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
15 | import { MUser } from '@server/types/models' | ||
8 | import { | 16 | import { |
9 | RegisterServerAuthenticatedResult, | 17 | RegisterServerAuthenticatedResult, |
10 | RegisterServerAuthPassOptions, | 18 | RegisterServerAuthPassOptions, |
11 | RegisterServerExternalAuthenticatedResult | 19 | RegisterServerExternalAuthenticatedResult |
12 | } from '@server/types/plugins/register-server-auth.model' | 20 | } from '@server/types/plugins/register-server-auth.model' |
13 | import { UserRole } from '@shared/models' | 21 | import { UserAdminFlag, UserRole } from '@shared/models' |
22 | import { BypassLogin } from './oauth-model' | ||
23 | |||
24 | export type ExternalUser = | ||
25 | Pick<MUser, 'username' | 'email' | 'role' | 'adminFlags' | 'videoQuotaDaily' | 'videoQuota'> & | ||
26 | { displayName: string } | ||
14 | 27 | ||
15 | // Token is the key, expiration date is the value | 28 | // Token is the key, expiration date is the value |
16 | const authBypassTokens = new Map<string, { | 29 | const authBypassTokens = new Map<string, { |
17 | expires: Date | 30 | expires: Date |
18 | user: { | 31 | user: ExternalUser |
19 | username: string | 32 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] |
20 | email: string | ||
21 | displayName: string | ||
22 | role: UserRole | ||
23 | } | ||
24 | authName: string | 33 | authName: string |
25 | npmName: string | 34 | npmName: string |
26 | }>() | 35 | }>() |
@@ -56,7 +65,8 @@ async function onExternalUserAuthenticated (options: { | |||
56 | expires, | 65 | expires, |
57 | user, | 66 | user, |
58 | npmName, | 67 | npmName, |
59 | authName | 68 | authName, |
69 | userUpdater: authResult.userUpdater | ||
60 | }) | 70 | }) |
61 | 71 | ||
62 | // Cleanup expired tokens | 72 | // Cleanup expired tokens |
@@ -78,7 +88,7 @@ async function getAuthNameFromRefreshGrant (refreshToken?: string) { | |||
78 | return tokenModel?.authName | 88 | return tokenModel?.authName |
79 | } | 89 | } |
80 | 90 | ||
81 | async function getBypassFromPasswordGrant (username: string, password: string) { | 91 | async function getBypassFromPasswordGrant (username: string, password: string): Promise<BypassLogin> { |
82 | const plugins = PluginManager.Instance.getIdAndPassAuths() | 92 | const plugins = PluginManager.Instance.getIdAndPassAuths() |
83 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | 93 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] |
84 | 94 | ||
@@ -133,7 +143,8 @@ async function getBypassFromPasswordGrant (username: string, password: string) { | |||
133 | bypass: true, | 143 | bypass: true, |
134 | pluginName: pluginAuth.npmName, | 144 | pluginName: pluginAuth.npmName, |
135 | authName: authOptions.authName, | 145 | authName: authOptions.authName, |
136 | user: buildUserResult(loginResult) | 146 | user: buildUserResult(loginResult), |
147 | userUpdater: loginResult.userUpdater | ||
137 | } | 148 | } |
138 | } catch (err) { | 149 | } catch (err) { |
139 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | 150 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) |
@@ -143,7 +154,7 @@ async function getBypassFromPasswordGrant (username: string, password: string) { | |||
143 | return undefined | 154 | return undefined |
144 | } | 155 | } |
145 | 156 | ||
146 | function getBypassFromExternalAuth (username: string, externalAuthToken: string) { | 157 | function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin { |
147 | const obj = authBypassTokens.get(externalAuthToken) | 158 | const obj = authBypassTokens.get(externalAuthToken) |
148 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') | 159 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') |
149 | 160 | ||
@@ -167,33 +178,29 @@ function getBypassFromExternalAuth (username: string, externalAuthToken: string) | |||
167 | bypass: true, | 178 | bypass: true, |
168 | pluginName: npmName, | 179 | pluginName: npmName, |
169 | authName, | 180 | authName, |
181 | userUpdater: obj.userUpdater, | ||
170 | user | 182 | user |
171 | } | 183 | } |
172 | } | 184 | } |
173 | 185 | ||
174 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { | 186 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { |
175 | if (!isUserUsernameValid(result.username)) { | 187 | const returnError = (field: string) => { |
176 | logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { username: result.username }) | 188 | logger.error('Auth method %s of plugin %s did not provide a valid %s.', authName, npmName, field, { [field]: result[field] }) |
177 | return false | 189 | return false |
178 | } | 190 | } |
179 | 191 | ||
180 | if (!result.email) { | 192 | if (!isUserUsernameValid(result.username)) return returnError('username') |
181 | logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email }) | 193 | if (!result.email) return returnError('email') |
182 | return false | ||
183 | } | ||
184 | 194 | ||
185 | // role is optional | 195 | // Following fields are optional |
186 | if (result.role && !isUserRoleValid(result.role)) { | 196 | if (result.role && !isUserRoleValid(result.role)) return returnError('role') |
187 | logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { role: result.role }) | 197 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) return returnError('displayName') |
188 | return false | 198 | if (result.adminFlags && !isUserAdminFlagsValid(result.adminFlags)) return returnError('adminFlags') |
189 | } | 199 | if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota') |
200 | if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily') | ||
190 | 201 | ||
191 | // display name is optional | 202 | if (result.userUpdater && typeof result.userUpdater !== 'function') { |
192 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) { | 203 | logger.error('Auth method %s of plugin %s did not provide a valid user updater function.', authName, npmName) |
193 | logger.error( | ||
194 | 'Auth method %s of plugin %s did not provide a valid display name.', | ||
195 | authName, npmName, { displayName: result.displayName } | ||
196 | ) | ||
197 | return false | 204 | return false |
198 | } | 205 | } |
199 | 206 | ||
@@ -205,7 +212,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | |||
205 | username: pluginResult.username, | 212 | username: pluginResult.username, |
206 | email: pluginResult.email, | 213 | email: pluginResult.email, |
207 | role: pluginResult.role ?? UserRole.USER, | 214 | role: pluginResult.role ?? UserRole.USER, |
208 | displayName: pluginResult.displayName || pluginResult.username | 215 | displayName: pluginResult.displayName || pluginResult.username, |
216 | |||
217 | adminFlags: pluginResult.adminFlags ?? UserAdminFlag.NONE, | ||
218 | |||
219 | videoQuota: pluginResult.videoQuota, | ||
220 | videoQuotaDaily: pluginResult.videoQuotaDaily | ||
209 | } | 221 | } |
210 | } | 222 | } |
211 | 223 | ||
diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts index 322b69e3a..43909284f 100644 --- a/server/lib/auth/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts | |||
@@ -1,11 +1,13 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' | 2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' |
3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
4 | import { AccountModel } from '@server/models/account/account' | ||
5 | import { AuthenticatedResultUpdaterFieldName, RegisterServerAuthenticatedResult } from '@server/types' | ||
4 | import { MOAuthClient } from '@server/types/models' | 6 | import { MOAuthClient } from '@server/types/models' |
5 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 7 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
6 | import { MUser } from '@server/types/models/user/user' | 8 | import { MUser, MUserDefault } from '@server/types/models/user/user' |
7 | import { pick } from '@shared/core-utils' | 9 | import { pick } from '@shared/core-utils' |
8 | import { UserRole } from '@shared/models/users/user-role' | 10 | import { AttributesOnly } from '@shared/typescript-utils' |
9 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
10 | import { CONFIG } from '../../initializers/config' | 12 | import { CONFIG } from '../../initializers/config' |
11 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | 13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
@@ -13,6 +15,7 @@ import { OAuthTokenModel } from '../../models/oauth/oauth-token' | |||
13 | import { UserModel } from '../../models/user/user' | 15 | import { UserModel } from '../../models/user/user' |
14 | import { findAvailableLocalActorName } from '../local-actor' | 16 | import { findAvailableLocalActorName } from '../local-actor' |
15 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' | 17 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' |
18 | import { ExternalUser } from './external-auth' | ||
16 | import { TokensCache } from './tokens-cache' | 19 | import { TokensCache } from './tokens-cache' |
17 | 20 | ||
18 | type TokenInfo = { | 21 | type TokenInfo = { |
@@ -26,12 +29,8 @@ export type BypassLogin = { | |||
26 | bypass: boolean | 29 | bypass: boolean |
27 | pluginName: string | 30 | pluginName: string |
28 | authName?: string | 31 | authName?: string |
29 | user: { | 32 | user: ExternalUser |
30 | username: string | 33 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] |
31 | email: string | ||
32 | displayName: string | ||
33 | role: UserRole | ||
34 | } | ||
35 | } | 34 | } |
36 | 35 | ||
37 | async function getAccessToken (bearerToken: string) { | 36 | async function getAccessToken (bearerToken: string) { |
@@ -89,7 +88,9 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin | |||
89 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) | 88 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) |
90 | 89 | ||
91 | let user = await UserModel.loadByEmail(bypassLogin.user.email) | 90 | let user = await UserModel.loadByEmail(bypassLogin.user.email) |
91 | |||
92 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) | 92 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) |
93 | else user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater) | ||
93 | 94 | ||
94 | // Cannot create a user | 95 | // Cannot create a user |
95 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') | 96 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') |
@@ -219,16 +220,11 @@ export { | |||
219 | 220 | ||
220 | // --------------------------------------------------------------------------- | 221 | // --------------------------------------------------------------------------- |
221 | 222 | ||
222 | async function createUserFromExternal (pluginAuth: string, options: { | 223 | async function createUserFromExternal (pluginAuth: string, userOptions: ExternalUser) { |
223 | username: string | 224 | const username = await findAvailableLocalActorName(userOptions.username) |
224 | email: string | ||
225 | role: UserRole | ||
226 | displayName: string | ||
227 | }) { | ||
228 | const username = await findAvailableLocalActorName(options.username) | ||
229 | 225 | ||
230 | const userToCreate = buildUser({ | 226 | const userToCreate = buildUser({ |
231 | ...pick(options, [ 'email', 'role' ]), | 227 | ...pick(userOptions, [ 'email', 'role', 'adminFlags', 'videoQuota', 'videoQuotaDaily' ]), |
232 | 228 | ||
233 | username, | 229 | username, |
234 | emailVerified: null, | 230 | emailVerified: null, |
@@ -238,12 +234,57 @@ async function createUserFromExternal (pluginAuth: string, options: { | |||
238 | 234 | ||
239 | const { user } = await createUserAccountAndChannelAndPlaylist({ | 235 | const { user } = await createUserAccountAndChannelAndPlaylist({ |
240 | userToCreate, | 236 | userToCreate, |
241 | userDisplayName: options.displayName | 237 | userDisplayName: userOptions.displayName |
242 | }) | 238 | }) |
243 | 239 | ||
244 | return user | 240 | return user |
245 | } | 241 | } |
246 | 242 | ||
243 | async function updateUserFromExternal ( | ||
244 | user: MUserDefault, | ||
245 | userOptions: ExternalUser, | ||
246 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] | ||
247 | ) { | ||
248 | if (!userUpdater) return user | ||
249 | |||
250 | { | ||
251 | type UserAttributeKeys = keyof AttributesOnly<UserModel> | ||
252 | const mappingKeys: { [ id in UserAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = { | ||
253 | role: 'role', | ||
254 | adminFlags: 'adminFlags', | ||
255 | videoQuota: 'videoQuota', | ||
256 | videoQuotaDaily: 'videoQuotaDaily' | ||
257 | } | ||
258 | |||
259 | for (const modelKey of Object.keys(mappingKeys)) { | ||
260 | const pluginOptionKey = mappingKeys[modelKey] | ||
261 | |||
262 | const newValue = userUpdater({ fieldName: pluginOptionKey, currentValue: user[modelKey], newValue: userOptions[pluginOptionKey] }) | ||
263 | user.set(modelKey, newValue) | ||
264 | } | ||
265 | } | ||
266 | |||
267 | { | ||
268 | type AccountAttributeKeys = keyof Partial<AttributesOnly<AccountModel>> | ||
269 | const mappingKeys: { [ id in AccountAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = { | ||
270 | name: 'displayName' | ||
271 | } | ||
272 | |||
273 | for (const modelKey of Object.keys(mappingKeys)) { | ||
274 | const optionKey = mappingKeys[modelKey] | ||
275 | |||
276 | const newValue = userUpdater({ fieldName: optionKey, currentValue: user.Account[modelKey], newValue: userOptions[optionKey] }) | ||
277 | user.Account.set(modelKey, newValue) | ||
278 | } | ||
279 | } | ||
280 | |||
281 | logger.debug('Updated user %s with plugin userUpdated function.', user.email, { user, userOptions }) | ||
282 | |||
283 | user.Account = await user.Account.save() | ||
284 | |||
285 | return user.save() | ||
286 | } | ||
287 | |||
247 | function checkUserValidityOrThrow (user: MUser) { | 288 | function checkUserValidityOrThrow (user: MUser) { |
248 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 289 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
249 | } | 290 | } |
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts index bc0d4301f..887c4f7c9 100644 --- a/server/lib/auth/oauth.ts +++ b/server/lib/auth/oauth.ts | |||
@@ -10,20 +10,32 @@ import OAuth2Server, { | |||
10 | } from '@node-oauth/oauth2-server' | 10 | } from '@node-oauth/oauth2-server' |
11 | import { randomBytesPromise } from '@server/helpers/core-utils' | 11 | import { randomBytesPromise } from '@server/helpers/core-utils' |
12 | import { isOTPValid } from '@server/helpers/otp' | 12 | import { isOTPValid } from '@server/helpers/otp' |
13 | import { CONFIG } from '@server/initializers/config' | ||
14 | import { UserRegistrationModel } from '@server/models/user/user-registration' | ||
13 | import { MOAuthClient } from '@server/types/models' | 15 | import { MOAuthClient } from '@server/types/models' |
14 | import { sha1 } from '@shared/extra-utils' | 16 | import { sha1 } from '@shared/extra-utils' |
15 | import { HttpStatusCode } from '@shared/models' | 17 | import { HttpStatusCode, ServerErrorCode, UserRegistrationState } from '@shared/models' |
16 | import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' | 18 | import { OTP } from '../../initializers/constants' |
17 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | 19 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' |
18 | 20 | ||
19 | class MissingTwoFactorError extends Error { | 21 | class MissingTwoFactorError extends Error { |
20 | code = HttpStatusCode.UNAUTHORIZED_401 | 22 | code = HttpStatusCode.UNAUTHORIZED_401 |
21 | name = 'missing_two_factor' | 23 | name = ServerErrorCode.MISSING_TWO_FACTOR |
22 | } | 24 | } |
23 | 25 | ||
24 | class InvalidTwoFactorError extends Error { | 26 | class InvalidTwoFactorError extends Error { |
25 | code = HttpStatusCode.BAD_REQUEST_400 | 27 | code = HttpStatusCode.BAD_REQUEST_400 |
26 | name = 'invalid_two_factor' | 28 | name = ServerErrorCode.INVALID_TWO_FACTOR |
29 | } | ||
30 | |||
31 | class RegistrationWaitingForApproval extends Error { | ||
32 | code = HttpStatusCode.BAD_REQUEST_400 | ||
33 | name = ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL | ||
34 | } | ||
35 | |||
36 | class RegistrationApprovalRejected extends Error { | ||
37 | code = HttpStatusCode.BAD_REQUEST_400 | ||
38 | name = ServerErrorCode.ACCOUNT_APPROVAL_REJECTED | ||
27 | } | 39 | } |
28 | 40 | ||
29 | /** | 41 | /** |
@@ -32,8 +44,9 @@ class InvalidTwoFactorError extends Error { | |||
32 | * | 44 | * |
33 | */ | 45 | */ |
34 | const oAuthServer = new OAuth2Server({ | 46 | const oAuthServer = new OAuth2Server({ |
35 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | 47 | // Wants seconds |
36 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | 48 | accessTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN / 1000, |
49 | refreshTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN / 1000, | ||
37 | 50 | ||
38 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | 51 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications |
39 | model: require('./oauth-model') | 52 | model: require('./oauth-model') |
@@ -126,7 +139,17 @@ async function handlePasswordGrant (options: { | |||
126 | } | 139 | } |
127 | 140 | ||
128 | const user = await getUser(request.body.username, request.body.password, bypassLogin) | 141 | const user = await getUser(request.body.username, request.body.password, bypassLogin) |
129 | if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid') | 142 | if (!user) { |
143 | const registration = await UserRegistrationModel.loadByEmailOrUsername(request.body.username) | ||
144 | |||
145 | if (registration?.state === UserRegistrationState.REJECTED) { | ||
146 | throw new RegistrationApprovalRejected('Registration approval for this account has been rejected') | ||
147 | } else if (registration?.state === UserRegistrationState.PENDING) { | ||
148 | throw new RegistrationWaitingForApproval('Registration for this account is awaiting approval') | ||
149 | } | ||
150 | |||
151 | throw new InvalidGrantError('Invalid grant: user credentials are invalid') | ||
152 | } | ||
130 | 153 | ||
131 | if (user.otpSecret) { | 154 | if (user.otpSecret) { |
132 | if (!request.headers[OTP.HEADER_NAME]) { | 155 | if (!request.headers[OTP.HEADER_NAME]) { |
@@ -182,10 +205,10 @@ function generateRandomToken () { | |||
182 | 205 | ||
183 | function getTokenExpiresAt (type: 'access' | 'refresh') { | 206 | function getTokenExpiresAt (type: 'access' | 'refresh') { |
184 | const lifetime = type === 'access' | 207 | const lifetime = type === 'access' |
185 | ? OAUTH_LIFETIME.ACCESS_TOKEN | 208 | ? CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN |
186 | : OAUTH_LIFETIME.REFRESH_TOKEN | 209 | : CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN |
187 | 210 | ||
188 | return new Date(Date.now() + lifetime * 1000) | 211 | return new Date(Date.now() + lifetime) |
189 | } | 212 | } |
190 | 213 | ||
191 | async function buildToken () { | 214 | async function buildToken () { |
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts index 410708a35..43efc7d02 100644 --- a/server/lib/auth/tokens-cache.ts +++ b/server/lib/auth/tokens-cache.ts | |||
@@ -36,8 +36,8 @@ export class TokensCache { | |||
36 | const token = this.userHavingToken.get(userId) | 36 | const token = this.userHavingToken.get(userId) |
37 | 37 | ||
38 | if (token !== undefined) { | 38 | if (token !== undefined) { |
39 | this.accessTokenCache.del(token) | 39 | this.accessTokenCache.delete(token) |
40 | this.userHavingToken.del(userId) | 40 | this.userHavingToken.delete(userId) |
41 | } | 41 | } |
42 | } | 42 | } |
43 | 43 | ||
@@ -45,8 +45,8 @@ export class TokensCache { | |||
45 | const tokenModel = this.accessTokenCache.get(token) | 45 | const tokenModel = this.accessTokenCache.get(token) |
46 | 46 | ||
47 | if (tokenModel !== undefined) { | 47 | if (tokenModel !== undefined) { |
48 | this.userHavingToken.del(tokenModel.userId) | 48 | this.userHavingToken.delete(tokenModel.userId) |
49 | this.accessTokenCache.del(token) | 49 | this.accessTokenCache.delete(token) |
50 | } | 50 | } |
51 | } | 51 | } |
52 | } | 52 | } |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 39b662eb2..f5c3e4745 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -3,13 +3,13 @@ import { merge } from 'lodash' | |||
3 | import { createTransport, Transporter } from 'nodemailer' | 3 | import { createTransport, Transporter } from 'nodemailer' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { arrayify, root } from '@shared/core-utils' | 5 | import { arrayify, root } from '@shared/core-utils' |
6 | import { EmailPayload } from '@shared/models' | 6 | import { EmailPayload, UserRegistrationState } from '@shared/models' |
7 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' | 7 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' |
8 | import { isTestOrDevInstance } from '../helpers/core-utils' | 8 | import { isTestOrDevInstance } from '../helpers/core-utils' |
9 | import { bunyanLogger, logger } from '../helpers/logger' | 9 | import { bunyanLogger, logger } from '../helpers/logger' |
10 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 10 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
11 | import { WEBSERVER } from '../initializers/constants' | 11 | import { WEBSERVER } from '../initializers/constants' |
12 | import { MUser } from '../types/models' | 12 | import { MRegistration, MUser } from '../types/models' |
13 | import { JobQueue } from './job-queue' | 13 | import { JobQueue } from './job-queue' |
14 | 14 | ||
15 | const Email = require('email-templates') | 15 | const Email = require('email-templates') |
@@ -62,7 +62,9 @@ class Emailer { | |||
62 | subject: 'Reset your account password', | 62 | subject: 'Reset your account password', |
63 | locals: { | 63 | locals: { |
64 | username, | 64 | username, |
65 | resetPasswordUrl | 65 | resetPasswordUrl, |
66 | |||
67 | hideNotificationPreferencesLink: true | ||
66 | } | 68 | } |
67 | } | 69 | } |
68 | 70 | ||
@@ -76,21 +78,33 @@ class Emailer { | |||
76 | subject: 'Create your account password', | 78 | subject: 'Create your account password', |
77 | locals: { | 79 | locals: { |
78 | username, | 80 | username, |
79 | createPasswordUrl | 81 | createPasswordUrl, |
82 | |||
83 | hideNotificationPreferencesLink: true | ||
80 | } | 84 | } |
81 | } | 85 | } |
82 | 86 | ||
83 | return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload }) | 87 | return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload }) |
84 | } | 88 | } |
85 | 89 | ||
86 | addVerifyEmailJob (username: string, to: string, verifyEmailUrl: string) { | 90 | addVerifyEmailJob (options: { |
91 | username: string | ||
92 | isRegistrationRequest: boolean | ||
93 | to: string | ||
94 | verifyEmailUrl: string | ||
95 | }) { | ||
96 | const { username, isRegistrationRequest, to, verifyEmailUrl } = options | ||
97 | |||
87 | const emailPayload: EmailPayload = { | 98 | const emailPayload: EmailPayload = { |
88 | template: 'verify-email', | 99 | template: 'verify-email', |
89 | to: [ to ], | 100 | to: [ to ], |
90 | subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`, | 101 | subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`, |
91 | locals: { | 102 | locals: { |
92 | username, | 103 | username, |
93 | verifyEmailUrl | 104 | verifyEmailUrl, |
105 | isRegistrationRequest, | ||
106 | |||
107 | hideNotificationPreferencesLink: true | ||
94 | } | 108 | } |
95 | } | 109 | } |
96 | 110 | ||
@@ -123,7 +137,33 @@ class Emailer { | |||
123 | body, | 137 | body, |
124 | 138 | ||
125 | // There are not notification preferences for the contact form | 139 | // There are not notification preferences for the contact form |
126 | hideNotificationPreferences: true | 140 | hideNotificationPreferencesLink: true |
141 | } | ||
142 | } | ||
143 | |||
144 | return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload }) | ||
145 | } | ||
146 | |||
147 | addUserRegistrationRequestProcessedJob (registration: MRegistration) { | ||
148 | let template: string | ||
149 | let subject: string | ||
150 | if (registration.state === UserRegistrationState.ACCEPTED) { | ||
151 | template = 'user-registration-request-accepted' | ||
152 | subject = `Your registration request for ${registration.username} has been accepted` | ||
153 | } else { | ||
154 | template = 'user-registration-request-rejected' | ||
155 | subject = `Your registration request for ${registration.username} has been rejected` | ||
156 | } | ||
157 | |||
158 | const to = registration.email | ||
159 | const emailPayload: EmailPayload = { | ||
160 | to: [ to ], | ||
161 | template, | ||
162 | subject, | ||
163 | locals: { | ||
164 | username: registration.username, | ||
165 | moderationResponse: registration.moderationResponse, | ||
166 | loginLink: WEBSERVER.URL + '/login' | ||
127 | } | 167 | } |
128 | } | 168 | } |
129 | 169 | ||
diff --git a/server/lib/emails/common/base.pug b/server/lib/emails/common/base.pug index 6da5648e4..41e94564d 100644 --- a/server/lib/emails/common/base.pug +++ b/server/lib/emails/common/base.pug | |||
@@ -222,19 +222,9 @@ body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: | |||
222 | td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;') | 222 | td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;') |
223 | br | 223 | br |
224 | //- Clear Spacer : END | 224 | //- Clear Spacer : END |
225 | //- 1 Column Text : BEGIN | ||
226 | if username | ||
227 | tr | ||
228 | td(style='background-color: #cccccc;') | ||
229 | table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%') | ||
230 | tr | ||
231 | td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;') | ||
232 | p(style='margin: 0;') | ||
233 | | You are receiving this email as part of your notification settings on #{instanceName} for your account #{username}. | ||
234 | //- 1 Column Text : END | ||
235 | //- Email Body : END | 225 | //- Email Body : END |
236 | //- Email Footer : BEGIN | 226 | //- Email Footer : BEGIN |
237 | unless hideNotificationPreferences | 227 | unless hideNotificationPreferencesLink |
238 | table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;') | 228 | table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;') |
239 | tr | 229 | tr |
240 | td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;') | 230 | td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;') |
diff --git a/server/lib/emails/user-registration-request-accepted/html.pug b/server/lib/emails/user-registration-request-accepted/html.pug new file mode 100644 index 000000000..7a52c3fe1 --- /dev/null +++ b/server/lib/emails/user-registration-request-accepted/html.pug | |||
@@ -0,0 +1,10 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Congratulation #{username}, your registration request has been accepted! | ||
5 | |||
6 | block content | ||
7 | p Your registration request has been accepted. | ||
8 | p Moderators sent you the following message: | ||
9 | blockquote(style='white-space: pre-wrap') #{moderationResponse} | ||
10 | p Your account has been created and you can login on #[a(href=loginLink) #{loginLink}] | ||
diff --git a/server/lib/emails/user-registration-request-rejected/html.pug b/server/lib/emails/user-registration-request-rejected/html.pug new file mode 100644 index 000000000..ec0aa8dfe --- /dev/null +++ b/server/lib/emails/user-registration-request-rejected/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Registration request of your account #{username} has rejected | ||
5 | |||
6 | block content | ||
7 | p Your registration request has been rejected. | ||
8 | p Moderators sent you the following message: | ||
9 | blockquote(style='white-space: pre-wrap') #{moderationResponse} | ||
diff --git a/server/lib/emails/user-registration-request/html.pug b/server/lib/emails/user-registration-request/html.pug new file mode 100644 index 000000000..64898f3f2 --- /dev/null +++ b/server/lib/emails/user-registration-request/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | A new user wants to register | ||
5 | |||
6 | block content | ||
7 | p User #{registration.username} wants to register on your PeerTube instance with the following reason: | ||
8 | blockquote(style='white-space: pre-wrap') #{registration.registrationReason} | ||
9 | p You can accept or reject the registration request in the #[a(href=`${WEBSERVER.URL}/admin/moderation/registrations/list`) administration]. | ||
diff --git a/server/lib/emails/verify-email/html.pug b/server/lib/emails/verify-email/html.pug index be9dde21b..19ef65f75 100644 --- a/server/lib/emails/verify-email/html.pug +++ b/server/lib/emails/verify-email/html.pug | |||
@@ -1,17 +1,19 @@ | |||
1 | extends ../common/greetings | 1 | extends ../common/greetings |
2 | 2 | ||
3 | block title | 3 | block title |
4 | | Account verification | 4 | | Email verification |
5 | 5 | ||
6 | block content | 6 | block content |
7 | p Welcome to #{instanceName}! | 7 | if isRegistrationRequest |
8 | p. | 8 | p You just requested an account on #[a(href=WEBSERVER.URL) #{instanceName}]. |
9 | You just created an account at #[a(href=WEBSERVER.URL) #{instanceName}]. | 9 | else |
10 | Your username there is: #{username}. | 10 | p You just created an account on #[a(href=WEBSERVER.URL) #{instanceName}]. |
11 | p. | 11 | |
12 | To start using your account you must verify your email first! | 12 | if isRegistrationRequest |
13 | Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you. | 13 | p To complete your registration request you must verify your email first! |
14 | p. | 14 | else |
15 | If you can't see the verification link above you can use the following link #[a(href=verifyEmailUrl) #{verifyEmailUrl}] | 15 | p To start using your account you must verify your email first! |
16 | p. | 16 | |
17 | If you are not the person who initiated this request, please ignore this email. | 17 | p Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you. |
18 | p If you can't see the verification link above you can use the following link #[a(href=verifyEmailUrl) #{verifyEmailUrl}] | ||
19 | p If you are not the person who initiated this request, please ignore this email. | ||
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 866aa1ed0..8597eb000 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -184,7 +184,7 @@ class JobQueue { | |||
184 | 184 | ||
185 | this.jobRedisPrefix = 'bull-' + WEBSERVER.HOST | 185 | this.jobRedisPrefix = 'bull-' + WEBSERVER.HOST |
186 | 186 | ||
187 | for (const handlerName of (Object.keys(handlers) as JobType[])) { | 187 | for (const handlerName of Object.keys(handlers)) { |
188 | this.buildWorker(handlerName) | 188 | this.buildWorker(handlerName) |
189 | this.buildQueue(handlerName) | 189 | this.buildQueue(handlerName) |
190 | this.buildQueueScheduler(handlerName) | 190 | this.buildQueueScheduler(handlerName) |
diff --git a/server/lib/notifier/notifier.ts b/server/lib/notifier/notifier.ts index 66cfc31c4..920c55df0 100644 --- a/server/lib/notifier/notifier.ts +++ b/server/lib/notifier/notifier.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { MUser, MUserDefault } from '@server/types/models/user' | 1 | import { MRegistration, MUser, MUserDefault } from '@server/types/models/user' |
2 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' | 2 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' |
3 | import { UserNotificationSettingValue } from '../../../shared/models/users' | 3 | import { UserNotificationSettingValue } from '../../../shared/models/users' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
@@ -13,6 +13,7 @@ import { | |||
13 | AbuseStateChangeForReporter, | 13 | AbuseStateChangeForReporter, |
14 | AutoFollowForInstance, | 14 | AutoFollowForInstance, |
15 | CommentMention, | 15 | CommentMention, |
16 | DirectRegistrationForModerators, | ||
16 | FollowForInstance, | 17 | FollowForInstance, |
17 | FollowForUser, | 18 | FollowForUser, |
18 | ImportFinishedForOwner, | 19 | ImportFinishedForOwner, |
@@ -30,7 +31,7 @@ import { | |||
30 | OwnedPublicationAfterAutoUnblacklist, | 31 | OwnedPublicationAfterAutoUnblacklist, |
31 | OwnedPublicationAfterScheduleUpdate, | 32 | OwnedPublicationAfterScheduleUpdate, |
32 | OwnedPublicationAfterTranscoding, | 33 | OwnedPublicationAfterTranscoding, |
33 | RegistrationForModerators, | 34 | RegistrationRequestForModerators, |
34 | StudioEditionFinishedForOwner, | 35 | StudioEditionFinishedForOwner, |
35 | UnblacklistForOwner | 36 | UnblacklistForOwner |
36 | } from './shared' | 37 | } from './shared' |
@@ -47,7 +48,8 @@ class Notifier { | |||
47 | newBlacklist: [ NewBlacklistForOwner ], | 48 | newBlacklist: [ NewBlacklistForOwner ], |
48 | unblacklist: [ UnblacklistForOwner ], | 49 | unblacklist: [ UnblacklistForOwner ], |
49 | importFinished: [ ImportFinishedForOwner ], | 50 | importFinished: [ ImportFinishedForOwner ], |
50 | userRegistration: [ RegistrationForModerators ], | 51 | directRegistration: [ DirectRegistrationForModerators ], |
52 | registrationRequest: [ RegistrationRequestForModerators ], | ||
51 | userFollow: [ FollowForUser ], | 53 | userFollow: [ FollowForUser ], |
52 | instanceFollow: [ FollowForInstance ], | 54 | instanceFollow: [ FollowForInstance ], |
53 | autoInstanceFollow: [ AutoFollowForInstance ], | 55 | autoInstanceFollow: [ AutoFollowForInstance ], |
@@ -138,13 +140,20 @@ class Notifier { | |||
138 | }) | 140 | }) |
139 | } | 141 | } |
140 | 142 | ||
141 | notifyOnNewUserRegistration (user: MUserDefault): void { | 143 | notifyOnNewDirectRegistration (user: MUserDefault): void { |
142 | const models = this.notificationModels.userRegistration | 144 | const models = this.notificationModels.directRegistration |
143 | 145 | ||
144 | this.sendNotifications(models, user) | 146 | this.sendNotifications(models, user) |
145 | .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) | 147 | .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) |
146 | } | 148 | } |
147 | 149 | ||
150 | notifyOnNewRegistrationRequest (registration: MRegistration): void { | ||
151 | const models = this.notificationModels.registrationRequest | ||
152 | |||
153 | this.sendNotifications(models, registration) | ||
154 | .catch(err => logger.error('Cannot notify moderators of new registration request (%s).', registration.username, { err })) | ||
155 | } | ||
156 | |||
148 | notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { | 157 | notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { |
149 | const models = this.notificationModels.userFollow | 158 | const models = this.notificationModels.userFollow |
150 | 159 | ||
diff --git a/server/lib/notifier/shared/instance/registration-for-moderators.ts b/server/lib/notifier/shared/instance/direct-registration-for-moderators.ts index e92467424..5044f2068 100644 --- a/server/lib/notifier/shared/instance/registration-for-moderators.ts +++ b/server/lib/notifier/shared/instance/direct-registration-for-moderators.ts | |||
@@ -6,7 +6,7 @@ import { MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi | |||
6 | import { UserNotificationType, UserRight } from '@shared/models' | 6 | import { UserNotificationType, UserRight } from '@shared/models' |
7 | import { AbstractNotification } from '../common/abstract-notification' | 7 | import { AbstractNotification } from '../common/abstract-notification' |
8 | 8 | ||
9 | export class RegistrationForModerators extends AbstractNotification <MUserDefault> { | 9 | export class DirectRegistrationForModerators extends AbstractNotification <MUserDefault> { |
10 | private moderators: MUserDefault[] | 10 | private moderators: MUserDefault[] |
11 | 11 | ||
12 | async prepare () { | 12 | async prepare () { |
@@ -40,7 +40,7 @@ export class RegistrationForModerators extends AbstractNotification <MUserDefaul | |||
40 | return { | 40 | return { |
41 | template: 'user-registered', | 41 | template: 'user-registered', |
42 | to, | 42 | to, |
43 | subject: `a new user registered on ${CONFIG.INSTANCE.NAME}: ${this.payload.username}`, | 43 | subject: `A new user registered on ${CONFIG.INSTANCE.NAME}: ${this.payload.username}`, |
44 | locals: { | 44 | locals: { |
45 | user: this.payload | 45 | user: this.payload |
46 | } | 46 | } |
diff --git a/server/lib/notifier/shared/instance/index.ts b/server/lib/notifier/shared/instance/index.ts index c3bb22aec..8c75a8ee9 100644 --- a/server/lib/notifier/shared/instance/index.ts +++ b/server/lib/notifier/shared/instance/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './new-peertube-version-for-admins' | 1 | export * from './new-peertube-version-for-admins' |
2 | export * from './new-plugin-version-for-admins' | 2 | export * from './new-plugin-version-for-admins' |
3 | export * from './registration-for-moderators' | 3 | export * from './direct-registration-for-moderators' |
4 | export * from './registration-request-for-moderators' | ||
diff --git a/server/lib/notifier/shared/instance/registration-request-for-moderators.ts b/server/lib/notifier/shared/instance/registration-request-for-moderators.ts new file mode 100644 index 000000000..79920245a --- /dev/null +++ b/server/lib/notifier/shared/instance/registration-request-for-moderators.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { UserModel } from '@server/models/user/user' | ||
3 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
4 | import { MRegistration, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
5 | import { UserNotificationType, UserRight } from '@shared/models' | ||
6 | import { AbstractNotification } from '../common/abstract-notification' | ||
7 | |||
8 | export class RegistrationRequestForModerators extends AbstractNotification <MRegistration> { | ||
9 | private moderators: MUserDefault[] | ||
10 | |||
11 | async prepare () { | ||
12 | this.moderators = await UserModel.listWithRight(UserRight.MANAGE_REGISTRATIONS) | ||
13 | } | ||
14 | |||
15 | log () { | ||
16 | logger.info('Notifying %s moderators of new user registration request of %s.', this.moderators.length, this.payload.username) | ||
17 | } | ||
18 | |||
19 | getSetting (user: MUserWithNotificationSetting) { | ||
20 | return user.NotificationSetting.newUserRegistration | ||
21 | } | ||
22 | |||
23 | getTargetUsers () { | ||
24 | return this.moderators | ||
25 | } | ||
26 | |||
27 | createNotification (user: MUserWithNotificationSetting) { | ||
28 | const notification = UserNotificationModel.build<UserNotificationModelForApi>({ | ||
29 | type: UserNotificationType.NEW_USER_REGISTRATION_REQUEST, | ||
30 | userId: user.id, | ||
31 | userRegistrationId: this.payload.id | ||
32 | }) | ||
33 | notification.UserRegistration = this.payload | ||
34 | |||
35 | return notification | ||
36 | } | ||
37 | |||
38 | createEmail (to: string) { | ||
39 | return { | ||
40 | template: 'user-registration-request', | ||
41 | to, | ||
42 | subject: `A new user wants to register: ${this.payload.username}`, | ||
43 | locals: { | ||
44 | registration: this.payload | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | } | ||
diff --git a/server/lib/opentelemetry/metric-helpers/bittorrent-tracker-observers-builder.ts b/server/lib/opentelemetry/metric-helpers/bittorrent-tracker-observers-builder.ts new file mode 100644 index 000000000..ef40c0fa9 --- /dev/null +++ b/server/lib/opentelemetry/metric-helpers/bittorrent-tracker-observers-builder.ts | |||
@@ -0,0 +1,51 @@ | |||
1 | import { Meter } from '@opentelemetry/api' | ||
2 | |||
3 | export class BittorrentTrackerObserversBuilder { | ||
4 | |||
5 | constructor (private readonly meter: Meter, private readonly trackerServer: any) { | ||
6 | |||
7 | } | ||
8 | |||
9 | buildObservers () { | ||
10 | const activeInfohashes = this.meter.createObservableGauge('peertube_bittorrent_tracker_active_infohashes_total', { | ||
11 | description: 'Total active infohashes in the PeerTube BitTorrent Tracker' | ||
12 | }) | ||
13 | const inactiveInfohashes = this.meter.createObservableGauge('peertube_bittorrent_tracker_inactive_infohashes_total', { | ||
14 | description: 'Total inactive infohashes in the PeerTube BitTorrent Tracker' | ||
15 | }) | ||
16 | const peers = this.meter.createObservableGauge('peertube_bittorrent_tracker_peers_total', { | ||
17 | description: 'Total peers in the PeerTube BitTorrent Tracker' | ||
18 | }) | ||
19 | |||
20 | this.meter.addBatchObservableCallback(observableResult => { | ||
21 | const infohashes = Object.keys(this.trackerServer.torrents) | ||
22 | |||
23 | const counters = { | ||
24 | activeInfohashes: 0, | ||
25 | inactiveInfohashes: 0, | ||
26 | peers: 0, | ||
27 | uncompletedPeers: 0 | ||
28 | } | ||
29 | |||
30 | for (const infohash of infohashes) { | ||
31 | const content = this.trackerServer.torrents[infohash] | ||
32 | |||
33 | const peers = content.peers | ||
34 | if (peers.keys.length !== 0) counters.activeInfohashes++ | ||
35 | else counters.inactiveInfohashes++ | ||
36 | |||
37 | for (const peerId of peers.keys) { | ||
38 | const peer = peers.peek(peerId) | ||
39 | if (peer == null) return | ||
40 | |||
41 | counters.peers++ | ||
42 | } | ||
43 | } | ||
44 | |||
45 | observableResult.observe(activeInfohashes, counters.activeInfohashes) | ||
46 | observableResult.observe(inactiveInfohashes, counters.inactiveInfohashes) | ||
47 | observableResult.observe(peers, counters.peers) | ||
48 | }, [ activeInfohashes, inactiveInfohashes, peers ]) | ||
49 | } | ||
50 | |||
51 | } | ||
diff --git a/server/lib/opentelemetry/metric-helpers/index.ts b/server/lib/opentelemetry/metric-helpers/index.ts index 775d954ba..47b24a54f 100644 --- a/server/lib/opentelemetry/metric-helpers/index.ts +++ b/server/lib/opentelemetry/metric-helpers/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './bittorrent-tracker-observers-builder' | ||
1 | export * from './lives-observers-builder' | 2 | export * from './lives-observers-builder' |
2 | export * from './job-queue-observers-builder' | 3 | export * from './job-queue-observers-builder' |
3 | export * from './nodejs-observers-builder' | 4 | export * from './nodejs-observers-builder' |
diff --git a/server/lib/opentelemetry/metrics.ts b/server/lib/opentelemetry/metrics.ts index 226d514c0..9cc067e4a 100644 --- a/server/lib/opentelemetry/metrics.ts +++ b/server/lib/opentelemetry/metrics.ts | |||
@@ -7,6 +7,7 @@ import { CONFIG } from '@server/initializers/config' | |||
7 | import { MVideoImmutable } from '@server/types/models' | 7 | import { MVideoImmutable } from '@server/types/models' |
8 | import { PlaybackMetricCreate } from '@shared/models' | 8 | import { PlaybackMetricCreate } from '@shared/models' |
9 | import { | 9 | import { |
10 | BittorrentTrackerObserversBuilder, | ||
10 | JobQueueObserversBuilder, | 11 | JobQueueObserversBuilder, |
11 | LivesObserversBuilder, | 12 | LivesObserversBuilder, |
12 | NodeJSObserversBuilder, | 13 | NodeJSObserversBuilder, |
@@ -41,7 +42,7 @@ class OpenTelemetryMetrics { | |||
41 | }) | 42 | }) |
42 | } | 43 | } |
43 | 44 | ||
44 | registerMetrics () { | 45 | registerMetrics (options: { trackerServer: any }) { |
45 | if (CONFIG.OPEN_TELEMETRY.METRICS.ENABLED !== true) return | 46 | if (CONFIG.OPEN_TELEMETRY.METRICS.ENABLED !== true) return |
46 | 47 | ||
47 | logger.info('Registering Open Telemetry metrics') | 48 | logger.info('Registering Open Telemetry metrics') |
@@ -80,6 +81,9 @@ class OpenTelemetryMetrics { | |||
80 | 81 | ||
81 | const viewersObserversBuilder = new ViewersObserversBuilder(this.meter) | 82 | const viewersObserversBuilder = new ViewersObserversBuilder(this.meter) |
82 | viewersObserversBuilder.buildObservers() | 83 | viewersObserversBuilder.buildObservers() |
84 | |||
85 | const bittorrentTrackerObserversBuilder = new BittorrentTrackerObserversBuilder(this.meter, options.trackerServer) | ||
86 | bittorrentTrackerObserversBuilder.buildObservers() | ||
83 | } | 87 | } |
84 | 88 | ||
85 | observePlaybackMetric (video: MVideoImmutable, metrics: PlaybackMetricCreate) { | 89 | observePlaybackMetric (video: MVideoImmutable, metrics: PlaybackMetricCreate) { |
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index 7b1def6e3..66383af46 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts | |||
@@ -209,6 +209,10 @@ function buildConfigHelpers () { | |||
209 | return WEBSERVER.URL | 209 | return WEBSERVER.URL |
210 | }, | 210 | }, |
211 | 211 | ||
212 | getServerListeningConfig () { | ||
213 | return { hostname: CONFIG.LISTEN.HOSTNAME, port: CONFIG.LISTEN.PORT } | ||
214 | }, | ||
215 | |||
212 | getServerConfig () { | 216 | getServerConfig () { |
213 | return ServerConfigManager.Instance.getServerConfig() | 217 | return ServerConfigManager.Instance.getServerConfig() |
214 | } | 218 | } |
@@ -245,7 +249,7 @@ function buildUserHelpers () { | |||
245 | }, | 249 | }, |
246 | 250 | ||
247 | getAuthUser: (res: express.Response) => { | 251 | getAuthUser: (res: express.Response) => { |
248 | const user = res.locals.oauth?.token?.User | 252 | const user = res.locals.oauth?.token?.User || res.locals.videoFileToken?.user |
249 | if (!user) return undefined | 253 | if (!user) return undefined |
250 | 254 | ||
251 | return UserModel.loadByIdFull(user.id) | 255 | return UserModel.loadByIdFull(user.id) |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index c0e9aece7..3706d2228 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -8,9 +8,8 @@ import { | |||
8 | AP_CLEANER, | 8 | AP_CLEANER, |
9 | CONTACT_FORM_LIFETIME, | 9 | CONTACT_FORM_LIFETIME, |
10 | RESUMABLE_UPLOAD_SESSION_LIFETIME, | 10 | RESUMABLE_UPLOAD_SESSION_LIFETIME, |
11 | TRACKER_RATE_LIMITS, | ||
12 | TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME, | 11 | TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME, |
13 | USER_EMAIL_VERIFY_LIFETIME, | 12 | EMAIL_VERIFY_LIFETIME, |
14 | USER_PASSWORD_CREATE_LIFETIME, | 13 | USER_PASSWORD_CREATE_LIFETIME, |
15 | USER_PASSWORD_RESET_LIFETIME, | 14 | USER_PASSWORD_RESET_LIFETIME, |
16 | VIEW_LIFETIME, | 15 | VIEW_LIFETIME, |
@@ -125,16 +124,28 @@ class Redis { | |||
125 | 124 | ||
126 | /* ************ Email verification ************ */ | 125 | /* ************ Email verification ************ */ |
127 | 126 | ||
128 | async setVerifyEmailVerificationString (userId: number) { | 127 | async setUserVerifyEmailVerificationString (userId: number) { |
129 | const generatedString = await generateRandomString(32) | 128 | const generatedString = await generateRandomString(32) |
130 | 129 | ||
131 | await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME) | 130 | await this.setValue(this.generateUserVerifyEmailKey(userId), generatedString, EMAIL_VERIFY_LIFETIME) |
132 | 131 | ||
133 | return generatedString | 132 | return generatedString |
134 | } | 133 | } |
135 | 134 | ||
136 | async getVerifyEmailLink (userId: number) { | 135 | async getUserVerifyEmailLink (userId: number) { |
137 | return this.getValue(this.generateVerifyEmailKey(userId)) | 136 | return this.getValue(this.generateUserVerifyEmailKey(userId)) |
137 | } | ||
138 | |||
139 | async setRegistrationVerifyEmailVerificationString (registrationId: number) { | ||
140 | const generatedString = await generateRandomString(32) | ||
141 | |||
142 | await this.setValue(this.generateRegistrationVerifyEmailKey(registrationId), generatedString, EMAIL_VERIFY_LIFETIME) | ||
143 | |||
144 | return generatedString | ||
145 | } | ||
146 | |||
147 | async getRegistrationVerifyEmailLink (registrationId: number) { | ||
148 | return this.getValue(this.generateRegistrationVerifyEmailKey(registrationId)) | ||
138 | } | 149 | } |
139 | 150 | ||
140 | /* ************ Contact form per IP ************ */ | 151 | /* ************ Contact form per IP ************ */ |
@@ -157,16 +168,6 @@ class Redis { | |||
157 | return this.exists(this.generateIPViewKey(ip, videoUUID)) | 168 | return this.exists(this.generateIPViewKey(ip, videoUUID)) |
158 | } | 169 | } |
159 | 170 | ||
160 | /* ************ Tracker IP block ************ */ | ||
161 | |||
162 | setTrackerBlockIP (ip: string) { | ||
163 | return this.setValue(this.generateTrackerBlockIPKey(ip), '1', TRACKER_RATE_LIMITS.BLOCK_IP_LIFETIME) | ||
164 | } | ||
165 | |||
166 | async doesTrackerBlockIPExist (ip: string) { | ||
167 | return this.exists(this.generateTrackerBlockIPKey(ip)) | ||
168 | } | ||
169 | |||
170 | /* ************ Video views stats ************ */ | 171 | /* ************ Video views stats ************ */ |
171 | 172 | ||
172 | addVideoViewStats (videoId: number) { | 173 | addVideoViewStats (videoId: number) { |
@@ -357,16 +358,16 @@ class Redis { | |||
357 | return 'two-factor-request-' + userId + '-' + token | 358 | return 'two-factor-request-' + userId + '-' + token |
358 | } | 359 | } |
359 | 360 | ||
360 | private generateVerifyEmailKey (userId: number) { | 361 | private generateUserVerifyEmailKey (userId: number) { |
361 | return 'verify-email-' + userId | 362 | return 'verify-email-user-' + userId |
362 | } | 363 | } |
363 | 364 | ||
364 | private generateIPViewKey (ip: string, videoUUID: string) { | 365 | private generateRegistrationVerifyEmailKey (registrationId: number) { |
365 | return `views-${videoUUID}-${ip}` | 366 | return 'verify-email-registration-' + registrationId |
366 | } | 367 | } |
367 | 368 | ||
368 | private generateTrackerBlockIPKey (ip: string) { | 369 | private generateIPViewKey (ip: string, videoUUID: string) { |
369 | return `tracker-block-ip-${ip}` | 370 | return `views-${videoUUID}-${ip}` |
370 | } | 371 | } |
371 | 372 | ||
372 | private generateContactFormKey (ip: string) { | 373 | private generateContactFormKey (ip: string) { |
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts index 78a9546ae..e87e2854f 100644 --- a/server/lib/server-config-manager.ts +++ b/server/lib/server-config-manager.ts | |||
@@ -261,10 +261,17 @@ class ServerConfigManager { | |||
261 | async getServerConfig (ip?: string): Promise<ServerConfig> { | 261 | async getServerConfig (ip?: string): Promise<ServerConfig> { |
262 | const { allowed } = await Hooks.wrapPromiseFun( | 262 | const { allowed } = await Hooks.wrapPromiseFun( |
263 | isSignupAllowed, | 263 | isSignupAllowed, |
264 | |||
264 | { | 265 | { |
265 | ip | 266 | ip, |
267 | signupMode: CONFIG.SIGNUP.REQUIRES_APPROVAL | ||
268 | ? 'request-registration' | ||
269 | : 'direct-registration' | ||
266 | }, | 270 | }, |
267 | 'filter:api.user.signup.allowed.result' | 271 | |
272 | CONFIG.SIGNUP.REQUIRES_APPROVAL | ||
273 | ? 'filter:api.user.request-signup.allowed.result' | ||
274 | : 'filter:api.user.signup.allowed.result' | ||
268 | ) | 275 | ) |
269 | 276 | ||
270 | const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip) | 277 | const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip) |
@@ -273,6 +280,7 @@ class ServerConfigManager { | |||
273 | allowed, | 280 | allowed, |
274 | allowedForCurrentIP, | 281 | allowedForCurrentIP, |
275 | minimumAge: CONFIG.SIGNUP.MINIMUM_AGE, | 282 | minimumAge: CONFIG.SIGNUP.MINIMUM_AGE, |
283 | requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL, | ||
276 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | 284 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION |
277 | } | 285 | } |
278 | 286 | ||
diff --git a/server/lib/signup.ts b/server/lib/signup.ts index f094531eb..f19232621 100644 --- a/server/lib/signup.ts +++ b/server/lib/signup.ts | |||
@@ -4,11 +4,24 @@ import { UserModel } from '../models/user/user' | |||
4 | 4 | ||
5 | const isCidr = require('is-cidr') | 5 | const isCidr = require('is-cidr') |
6 | 6 | ||
7 | async function isSignupAllowed (): Promise<{ allowed: boolean, errorMessage?: string }> { | 7 | export type SignupMode = 'direct-registration' | 'request-registration' |
8 | |||
9 | async function isSignupAllowed (options: { | ||
10 | signupMode: SignupMode | ||
11 | |||
12 | ip: string // For plugins | ||
13 | body?: any | ||
14 | }): Promise<{ allowed: boolean, errorMessage?: string }> { | ||
15 | const { signupMode } = options | ||
16 | |||
8 | if (CONFIG.SIGNUP.ENABLED === false) { | 17 | if (CONFIG.SIGNUP.ENABLED === false) { |
9 | return { allowed: false } | 18 | return { allowed: false } |
10 | } | 19 | } |
11 | 20 | ||
21 | if (signupMode === 'direct-registration' && CONFIG.SIGNUP.REQUIRES_APPROVAL === true) { | ||
22 | return { allowed: false } | ||
23 | } | ||
24 | |||
12 | // No limit and signup is enabled | 25 | // No limit and signup is enabled |
13 | if (CONFIG.SIGNUP.LIMIT === -1) { | 26 | if (CONFIG.SIGNUP.LIMIT === -1) { |
14 | return { allowed: true } | 27 | return { allowed: true } |
diff --git a/server/lib/sync-channel.ts b/server/lib/sync-channel.ts index 10167ee38..3a805a943 100644 --- a/server/lib/sync-channel.ts +++ b/server/lib/sync-channel.ts | |||
@@ -76,7 +76,7 @@ export async function synchronizeChannel (options: { | |||
76 | 76 | ||
77 | await JobQueue.Instance.createJobWithChildren(parent, children) | 77 | await JobQueue.Instance.createJobWithChildren(parent, children) |
78 | } catch (err) { | 78 | } catch (err) { |
79 | logger.error(`Failed to import channel ${channel.name}`, { err }) | 79 | logger.error(`Failed to import ${externalChannelUrl} in channel ${channel.name}`, { err }) |
80 | channelSync.state = VideoChannelSyncState.FAILED | 80 | channelSync.state = VideoChannelSyncState.FAILED |
81 | await channelSync.save() | 81 | await channelSync.save() |
82 | } | 82 | } |
diff --git a/server/lib/user.ts b/server/lib/user.ts index 2e433da04..ffb57944a 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -10,7 +10,7 @@ import { sequelizeTypescript } from '../initializers/database' | |||
10 | import { AccountModel } from '../models/account/account' | 10 | import { AccountModel } from '../models/account/account' |
11 | import { UserNotificationSettingModel } from '../models/user/user-notification-setting' | 11 | import { UserNotificationSettingModel } from '../models/user/user-notification-setting' |
12 | import { MAccountDefault, MChannelActor } from '../types/models' | 12 | import { MAccountDefault, MChannelActor } from '../types/models' |
13 | import { MUser, MUserDefault, MUserId } from '../types/models/user' | 13 | import { MRegistration, MUser, MUserDefault, MUserId } from '../types/models/user' |
14 | import { generateAndSaveActorKeys } from './activitypub/actors' | 14 | import { generateAndSaveActorKeys } from './activitypub/actors' |
15 | import { getLocalAccountActivityPubUrl } from './activitypub/url' | 15 | import { getLocalAccountActivityPubUrl } from './activitypub/url' |
16 | import { Emailer } from './emailer' | 16 | import { Emailer } from './emailer' |
@@ -97,7 +97,7 @@ async function createUserAccountAndChannelAndPlaylist (parameters: { | |||
97 | }) | 97 | }) |
98 | userCreated.Account = accountCreated | 98 | userCreated.Account = accountCreated |
99 | 99 | ||
100 | const channelAttributes = await buildChannelAttributes(userCreated, t, channelNames) | 100 | const channelAttributes = await buildChannelAttributes({ user: userCreated, transaction: t, channelNames }) |
101 | const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t) | 101 | const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t) |
102 | 102 | ||
103 | const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) | 103 | const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) |
@@ -160,15 +160,28 @@ async function createApplicationActor (applicationId: number) { | |||
160 | // --------------------------------------------------------------------------- | 160 | // --------------------------------------------------------------------------- |
161 | 161 | ||
162 | async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) { | 162 | async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) { |
163 | const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id) | 163 | const verificationString = await Redis.Instance.setUserVerifyEmailVerificationString(user.id) |
164 | let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString | 164 | let verifyEmailUrl = `${WEBSERVER.URL}/verify-account/email?userId=${user.id}&verificationString=${verificationString}` |
165 | 165 | ||
166 | if (isPendingEmail) url += '&isPendingEmail=true' | 166 | if (isPendingEmail) verifyEmailUrl += '&isPendingEmail=true' |
167 | |||
168 | const to = isPendingEmail | ||
169 | ? user.pendingEmail | ||
170 | : user.email | ||
167 | 171 | ||
168 | const email = isPendingEmail ? user.pendingEmail : user.email | ||
169 | const username = user.username | 172 | const username = user.username |
170 | 173 | ||
171 | Emailer.Instance.addVerifyEmailJob(username, email, url) | 174 | Emailer.Instance.addVerifyEmailJob({ username, to, verifyEmailUrl, isRegistrationRequest: false }) |
175 | } | ||
176 | |||
177 | async function sendVerifyRegistrationEmail (registration: MRegistration) { | ||
178 | const verificationString = await Redis.Instance.setRegistrationVerifyEmailVerificationString(registration.id) | ||
179 | const verifyEmailUrl = `${WEBSERVER.URL}/verify-account/email?registrationId=${registration.id}&verificationString=${verificationString}` | ||
180 | |||
181 | const to = registration.email | ||
182 | const username = registration.username | ||
183 | |||
184 | Emailer.Instance.addVerifyEmailJob({ username, to, verifyEmailUrl, isRegistrationRequest: true }) | ||
172 | } | 185 | } |
173 | 186 | ||
174 | // --------------------------------------------------------------------------- | 187 | // --------------------------------------------------------------------------- |
@@ -232,7 +245,10 @@ export { | |||
232 | createApplicationActor, | 245 | createApplicationActor, |
233 | createUserAccountAndChannelAndPlaylist, | 246 | createUserAccountAndChannelAndPlaylist, |
234 | createLocalAccountWithoutKeys, | 247 | createLocalAccountWithoutKeys, |
248 | |||
235 | sendVerifyUserEmail, | 249 | sendVerifyUserEmail, |
250 | sendVerifyRegistrationEmail, | ||
251 | |||
236 | isAbleToUploadVideo, | 252 | isAbleToUploadVideo, |
237 | buildUser | 253 | buildUser |
238 | } | 254 | } |
@@ -264,7 +280,13 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | | |||
264 | return UserNotificationSettingModel.create(values, { transaction: t }) | 280 | return UserNotificationSettingModel.create(values, { transaction: t }) |
265 | } | 281 | } |
266 | 282 | ||
267 | async function buildChannelAttributes (user: MUser, transaction?: Transaction, channelNames?: ChannelNames) { | 283 | async function buildChannelAttributes (options: { |
284 | user: MUser | ||
285 | transaction?: Transaction | ||
286 | channelNames?: ChannelNames | ||
287 | }) { | ||
288 | const { user, transaction, channelNames } = options | ||
289 | |||
268 | if (channelNames) return channelNames | 290 | if (channelNames) return channelNames |
269 | 291 | ||
270 | const channelName = await findAvailableLocalActorName(user.username + '_channel', transaction) | 292 | const channelName = await findAvailableLocalActorName(user.username + '_channel', transaction) |
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts index 02f160fe8..6eb865f7f 100644 --- a/server/lib/video-comment.ts +++ b/server/lib/video-comment.ts | |||
@@ -1,30 +1,41 @@ | |||
1 | import express from 'express' | ||
1 | import { cloneDeep } from 'lodash' | 2 | import { cloneDeep } from 'lodash' |
2 | import * as Sequelize from 'sequelize' | 3 | import * as Sequelize from 'sequelize' |
3 | import express from 'express' | ||
4 | import { logger } from '@server/helpers/logger' | 4 | import { logger } from '@server/helpers/logger' |
5 | import { sequelizeTypescript } from '@server/initializers/database' | 5 | import { sequelizeTypescript } from '@server/initializers/database' |
6 | import { ResultList } from '../../shared/models' | 6 | import { ResultList } from '../../shared/models' |
7 | import { VideoCommentThreadTree } from '../../shared/models/videos/comment/video-comment.model' | 7 | import { VideoCommentThreadTree } from '../../shared/models/videos/comment/video-comment.model' |
8 | import { VideoCommentModel } from '../models/video/video-comment' | 8 | import { VideoCommentModel } from '../models/video/video-comment' |
9 | import { MAccountDefault, MComment, MCommentOwnerVideo, MCommentOwnerVideoReply, MVideoFullLight } from '../types/models' | 9 | import { |
10 | MAccountDefault, | ||
11 | MComment, | ||
12 | MCommentFormattable, | ||
13 | MCommentOwnerVideo, | ||
14 | MCommentOwnerVideoReply, | ||
15 | MVideoFullLight | ||
16 | } from '../types/models' | ||
10 | import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send' | 17 | import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send' |
11 | import { getLocalVideoCommentActivityPubUrl } from './activitypub/url' | 18 | import { getLocalVideoCommentActivityPubUrl } from './activitypub/url' |
12 | import { Hooks } from './plugins/hooks' | 19 | import { Hooks } from './plugins/hooks' |
13 | 20 | ||
14 | async function removeComment (videoCommentInstance: MCommentOwnerVideo, req: express.Request, res: express.Response) { | 21 | async function removeComment (commentArg: MComment, req: express.Request, res: express.Response) { |
15 | const videoCommentInstanceBefore = cloneDeep(videoCommentInstance) | 22 | let videoCommentInstanceBefore: MCommentOwnerVideo |
16 | 23 | ||
17 | await sequelizeTypescript.transaction(async t => { | 24 | await sequelizeTypescript.transaction(async t => { |
18 | if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) { | 25 | const comment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(commentArg.url, t) |
19 | await sendDeleteVideoComment(videoCommentInstance, t) | 26 | |
27 | videoCommentInstanceBefore = cloneDeep(comment) | ||
28 | |||
29 | if (comment.isOwned() || comment.Video.isOwned()) { | ||
30 | await sendDeleteVideoComment(comment, t) | ||
20 | } | 31 | } |
21 | 32 | ||
22 | videoCommentInstance.markAsDeleted() | 33 | comment.markAsDeleted() |
23 | 34 | ||
24 | await videoCommentInstance.save({ transaction: t }) | 35 | await comment.save({ transaction: t }) |
25 | }) | ||
26 | 36 | ||
27 | logger.info('Video comment %d deleted.', videoCommentInstance.id) | 37 | logger.info('Video comment %d deleted.', comment.id) |
38 | }) | ||
28 | 39 | ||
29 | Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore, req, res }) | 40 | Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore, req, res }) |
30 | } | 41 | } |
@@ -64,7 +75,7 @@ async function createVideoComment (obj: { | |||
64 | return savedComment | 75 | return savedComment |
65 | } | 76 | } |
66 | 77 | ||
67 | function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>): VideoCommentThreadTree { | 78 | function buildFormattedCommentTree (resultList: ResultList<MCommentFormattable>): VideoCommentThreadTree { |
68 | // Comments are sorted by id ASC | 79 | // Comments are sorted by id ASC |
69 | const comments = resultList.data | 80 | const comments = resultList.data |
70 | 81 | ||
diff --git a/server/lib/video-tokens-manager.ts b/server/lib/video-tokens-manager.ts index c43085d16..17aa29cdd 100644 --- a/server/lib/video-tokens-manager.ts +++ b/server/lib/video-tokens-manager.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import LRUCache from 'lru-cache' | 1 | import LRUCache from 'lru-cache' |
2 | import { LRU_CACHE } from '@server/initializers/constants' | 2 | import { LRU_CACHE } from '@server/initializers/constants' |
3 | import { MUserAccountUrl } from '@server/types/models' | ||
4 | import { pick } from '@shared/core-utils' | ||
3 | import { buildUUID } from '@shared/extra-utils' | 5 | import { buildUUID } from '@shared/extra-utils' |
4 | 6 | ||
5 | // --------------------------------------------------------------------------- | 7 | // --------------------------------------------------------------------------- |
@@ -10,19 +12,22 @@ class VideoTokensManager { | |||
10 | 12 | ||
11 | private static instance: VideoTokensManager | 13 | private static instance: VideoTokensManager |
12 | 14 | ||
13 | private readonly lruCache = new LRUCache<string, string>({ | 15 | private readonly lruCache = new LRUCache<string, { videoUUID: string, user: MUserAccountUrl }>({ |
14 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, | 16 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, |
15 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL | 17 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL |
16 | }) | 18 | }) |
17 | 19 | ||
18 | private constructor () {} | 20 | private constructor () {} |
19 | 21 | ||
20 | create (videoUUID: string) { | 22 | create (options: { |
23 | user: MUserAccountUrl | ||
24 | videoUUID: string | ||
25 | }) { | ||
21 | const token = buildUUID() | 26 | const token = buildUUID() |
22 | 27 | ||
23 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) | 28 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) |
24 | 29 | ||
25 | this.lruCache.set(token, videoUUID) | 30 | this.lruCache.set(token, pick(options, [ 'user', 'videoUUID' ])) |
26 | 31 | ||
27 | return { token, expires } | 32 | return { token, expires } |
28 | } | 33 | } |
@@ -34,7 +39,16 @@ class VideoTokensManager { | |||
34 | const value = this.lruCache.get(options.token) | 39 | const value = this.lruCache.get(options.token) |
35 | if (!value) return false | 40 | if (!value) return false |
36 | 41 | ||
37 | return value === options.videoUUID | 42 | return value.videoUUID === options.videoUUID |
43 | } | ||
44 | |||
45 | getUserFromToken (options: { | ||
46 | token: string | ||
47 | }) { | ||
48 | const value = this.lruCache.get(options.token) | ||
49 | if (!value) return undefined | ||
50 | |||
51 | return value.user | ||
38 | } | 52 | } |
39 | 53 | ||
40 | static get Instance () { | 54 | static get Instance () { |
diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts index 458895898..77a532276 100644 --- a/server/middlewares/sort.ts +++ b/server/middlewares/sort.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { SortType } from '../models/utils' | ||
3 | 2 | ||
4 | const setDefaultSort = setDefaultSortFactory('-createdAt') | 3 | const setDefaultSort = setDefaultSortFactory('-createdAt') |
5 | const setDefaultVideosSort = setDefaultSortFactory('-publishedAt') | 4 | const setDefaultVideosSort = setDefaultSortFactory('-publishedAt') |
@@ -7,27 +6,7 @@ const setDefaultVideosSort = setDefaultSortFactory('-publishedAt') | |||
7 | const setDefaultVideoRedundanciesSort = setDefaultSortFactory('name') | 6 | const setDefaultVideoRedundanciesSort = setDefaultSortFactory('name') |
8 | 7 | ||
9 | const setDefaultSearchSort = setDefaultSortFactory('-match') | 8 | const setDefaultSearchSort = setDefaultSortFactory('-match') |
10 | 9 | const setBlacklistSort = setDefaultSortFactory('-createdAt') | |
11 | function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
12 | const newSort: SortType = { sortModel: undefined, sortValue: '' } | ||
13 | |||
14 | if (!req.query.sort) req.query.sort = '-createdAt' | ||
15 | |||
16 | // Set model we want to sort onto | ||
17 | if (req.query.sort === '-createdAt' || req.query.sort === 'createdAt' || | ||
18 | req.query.sort === '-id' || req.query.sort === 'id') { | ||
19 | // If we want to sort onto the BlacklistedVideos relation, we won't specify it in the query parameter... | ||
20 | newSort.sortModel = undefined | ||
21 | } else { | ||
22 | newSort.sortModel = 'Video' | ||
23 | } | ||
24 | |||
25 | newSort.sortValue = req.query.sort | ||
26 | |||
27 | req.query.sort = newSort | ||
28 | |||
29 | return next() | ||
30 | } | ||
31 | 10 | ||
32 | // --------------------------------------------------------------------------- | 11 | // --------------------------------------------------------------------------- |
33 | 12 | ||
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index 3a7daa573..c2dbfadb7 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -29,6 +29,7 @@ const customConfigUpdateValidator = [ | |||
29 | body('signup.enabled').isBoolean(), | 29 | body('signup.enabled').isBoolean(), |
30 | body('signup.limit').isInt(), | 30 | body('signup.limit').isInt(), |
31 | body('signup.requiresEmailVerification').isBoolean(), | 31 | body('signup.requiresEmailVerification').isBoolean(), |
32 | body('signup.requiresApproval').isBoolean(), | ||
32 | body('signup.minimumAge').isInt(), | 33 | body('signup.minimumAge').isInt(), |
33 | 34 | ||
34 | body('admin.email').isEmail(), | 35 | body('admin.email').isEmail(), |
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 9bc8887ff..1d0964667 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -21,8 +21,10 @@ export * from './server' | |||
21 | export * from './sort' | 21 | export * from './sort' |
22 | export * from './static' | 22 | export * from './static' |
23 | export * from './themes' | 23 | export * from './themes' |
24 | export * from './user-email-verification' | ||
24 | export * from './user-history' | 25 | export * from './user-history' |
25 | export * from './user-notifications' | 26 | export * from './user-notifications' |
27 | export * from './user-registrations' | ||
26 | export * from './user-subscriptions' | 28 | export * from './user-subscriptions' |
27 | export * from './users' | 29 | export * from './users' |
28 | export * from './videos' | 30 | export * from './videos' |
diff --git a/server/middlewares/validators/shared/user-registrations.ts b/server/middlewares/validators/shared/user-registrations.ts new file mode 100644 index 000000000..dbc7dda06 --- /dev/null +++ b/server/middlewares/validators/shared/user-registrations.ts | |||
@@ -0,0 +1,60 @@ | |||
1 | import express from 'express' | ||
2 | import { UserRegistrationModel } from '@server/models/user/user-registration' | ||
3 | import { MRegistration } from '@server/types/models' | ||
4 | import { forceNumber, pick } from '@shared/core-utils' | ||
5 | import { HttpStatusCode } from '@shared/models' | ||
6 | |||
7 | function checkRegistrationIdExist (idArg: number | string, res: express.Response) { | ||
8 | const id = forceNumber(idArg) | ||
9 | return checkRegistrationExist(() => UserRegistrationModel.load(id), res) | ||
10 | } | ||
11 | |||
12 | function checkRegistrationEmailExist (email: string, res: express.Response, abortResponse = true) { | ||
13 | return checkRegistrationExist(() => UserRegistrationModel.loadByEmail(email), res, abortResponse) | ||
14 | } | ||
15 | |||
16 | async function checkRegistrationHandlesDoNotAlreadyExist (options: { | ||
17 | username: string | ||
18 | channelHandle: string | ||
19 | email: string | ||
20 | res: express.Response | ||
21 | }) { | ||
22 | const { res } = options | ||
23 | |||
24 | const registration = await UserRegistrationModel.loadByEmailOrHandle(pick(options, [ 'username', 'email', 'channelHandle' ])) | ||
25 | |||
26 | if (registration) { | ||
27 | res.fail({ | ||
28 | status: HttpStatusCode.CONFLICT_409, | ||
29 | message: 'Registration with this username, channel name or email already exists.' | ||
30 | }) | ||
31 | return false | ||
32 | } | ||
33 | |||
34 | return true | ||
35 | } | ||
36 | |||
37 | async function checkRegistrationExist (finder: () => Promise<MRegistration>, res: express.Response, abortResponse = true) { | ||
38 | const registration = await finder() | ||
39 | |||
40 | if (!registration) { | ||
41 | if (abortResponse === true) { | ||
42 | res.fail({ | ||
43 | status: HttpStatusCode.NOT_FOUND_404, | ||
44 | message: 'User not found' | ||
45 | }) | ||
46 | } | ||
47 | |||
48 | return false | ||
49 | } | ||
50 | |||
51 | res.locals.userRegistration = registration | ||
52 | return true | ||
53 | } | ||
54 | |||
55 | export { | ||
56 | checkRegistrationIdExist, | ||
57 | checkRegistrationEmailExist, | ||
58 | checkRegistrationHandlesDoNotAlreadyExist, | ||
59 | checkRegistrationExist | ||
60 | } | ||
diff --git a/server/middlewares/validators/shared/users.ts b/server/middlewares/validators/shared/users.ts index b8f1436d3..030adc9f7 100644 --- a/server/middlewares/validators/shared/users.ts +++ b/server/middlewares/validators/shared/users.ts | |||
@@ -14,7 +14,7 @@ function checkUserEmailExist (email: string, res: express.Response, abortRespons | |||
14 | return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse) | 14 | return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse) |
15 | } | 15 | } |
16 | 16 | ||
17 | async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) { | 17 | async function checkUserNameOrEmailDoNotAlreadyExist (username: string, email: string, res: express.Response) { |
18 | const user = await UserModel.loadByUsernameOrEmail(username, email) | 18 | const user = await UserModel.loadByUsernameOrEmail(username, email) |
19 | 19 | ||
20 | if (user) { | 20 | if (user) { |
@@ -58,6 +58,6 @@ async function checkUserExist (finder: () => Promise<MUserDefault>, res: express | |||
58 | export { | 58 | export { |
59 | checkUserIdExist, | 59 | checkUserIdExist, |
60 | checkUserEmailExist, | 60 | checkUserEmailExist, |
61 | checkUserNameOrEmailDoesNotAlreadyExist, | 61 | checkUserNameOrEmailDoNotAlreadyExist, |
62 | checkUserExist | 62 | checkUserExist |
63 | } | 63 | } |
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts index ebbfc0a0a..0033a32ff 100644 --- a/server/middlewares/validators/shared/videos.ts +++ b/server/middlewares/validators/shared/videos.ts | |||
@@ -180,18 +180,16 @@ async function checkCanAccessVideoStaticFiles (options: { | |||
180 | return checkCanSeeVideo(options) | 180 | return checkCanSeeVideo(options) |
181 | } | 181 | } |
182 | 182 | ||
183 | if (!video.hasPrivateStaticPath()) return true | ||
184 | |||
185 | const videoFileToken = req.query.videoFileToken | 183 | const videoFileToken = req.query.videoFileToken |
186 | if (!videoFileToken) { | 184 | if (videoFileToken && VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) { |
187 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 185 | const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken }) |
188 | return false | ||
189 | } | ||
190 | 186 | ||
191 | if (VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) { | 187 | res.locals.videoFileToken = { user } |
192 | return true | 188 | return true |
193 | } | 189 | } |
194 | 190 | ||
191 | if (!video.hasPrivateStaticPath()) return true | ||
192 | |||
195 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 193 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) |
196 | return false | 194 | return false |
197 | } | 195 | } |
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 7d0639107..e6cc46317 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -1,9 +1,41 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { query } from 'express-validator' | 2 | import { query } from 'express-validator' |
3 | |||
4 | import { SORTABLE_COLUMNS } from '../../initializers/constants' | 3 | import { SORTABLE_COLUMNS } from '../../initializers/constants' |
5 | import { areValidationErrors } from './shared' | 4 | import { areValidationErrors } from './shared' |
6 | 5 | ||
6 | export const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS) | ||
7 | export const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) | ||
8 | export const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) | ||
9 | export const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) | ||
10 | export const videosSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS) | ||
11 | export const videoImportsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_IMPORTS) | ||
12 | export const videosSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS_SEARCH) | ||
13 | export const videoChannelsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) | ||
14 | export const videoPlaylistsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH) | ||
15 | export const videoCommentsValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENTS) | ||
16 | export const videoCommentThreadsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) | ||
17 | export const videoRatesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_RATES) | ||
18 | export const blacklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.BLACKLISTS) | ||
19 | export const videoChannelsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS) | ||
20 | export const instanceFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWERS) | ||
21 | export const instanceFollowingSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWING) | ||
22 | export const userSubscriptionsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) | ||
23 | export const accountsBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) | ||
24 | export const serversBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) | ||
25 | export const userNotificationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_NOTIFICATIONS) | ||
26 | export const videoPlaylistsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS) | ||
27 | export const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS) | ||
28 | export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) | ||
29 | export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) | ||
30 | export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS) | ||
31 | |||
32 | export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS) | ||
33 | export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS) | ||
34 | |||
35 | export const userRegistrationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_REGISTRATIONS) | ||
36 | |||
37 | // --------------------------------------------------------------------------- | ||
38 | |||
7 | function checkSortFactory (columns: string[], tags: string[] = []) { | 39 | function checkSortFactory (columns: string[], tags: string[] = []) { |
8 | return checkSort(createSortableColumns(columns), tags) | 40 | return checkSort(createSortableColumns(columns), tags) |
9 | } | 41 | } |
@@ -27,64 +59,3 @@ function createSortableColumns (sortableColumns: string[]) { | |||
27 | 59 | ||
28 | return sortableColumns.concat(sortableColumnDesc) | 60 | return sortableColumns.concat(sortableColumnDesc) |
29 | } | 61 | } |
30 | |||
31 | const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS) | ||
32 | const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) | ||
33 | const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) | ||
34 | const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) | ||
35 | const videosSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS) | ||
36 | const videoImportsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_IMPORTS) | ||
37 | const videosSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS_SEARCH) | ||
38 | const videoChannelsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) | ||
39 | const videoPlaylistsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH) | ||
40 | const videoCommentsValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENTS) | ||
41 | const videoCommentThreadsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) | ||
42 | const videoRatesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_RATES) | ||
43 | const blacklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.BLACKLISTS) | ||
44 | const videoChannelsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS) | ||
45 | const instanceFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWERS) | ||
46 | const instanceFollowingSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWING) | ||
47 | const userSubscriptionsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) | ||
48 | const accountsBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) | ||
49 | const serversBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) | ||
50 | const userNotificationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_NOTIFICATIONS) | ||
51 | const videoPlaylistsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS) | ||
52 | const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS) | ||
53 | const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) | ||
54 | const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) | ||
55 | const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS) | ||
56 | |||
57 | const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS) | ||
58 | const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS) | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | export { | ||
63 | adminUsersSortValidator, | ||
64 | abusesSortValidator, | ||
65 | videoChannelsSortValidator, | ||
66 | videoImportsSortValidator, | ||
67 | videoCommentsValidator, | ||
68 | videosSearchSortValidator, | ||
69 | videosSortValidator, | ||
70 | blacklistSortValidator, | ||
71 | accountsSortValidator, | ||
72 | instanceFollowersSortValidator, | ||
73 | instanceFollowingSortValidator, | ||
74 | jobsSortValidator, | ||
75 | videoCommentThreadsSortValidator, | ||
76 | videoRatesSortValidator, | ||
77 | userSubscriptionsSortValidator, | ||
78 | availablePluginsSortValidator, | ||
79 | videoChannelsSearchSortValidator, | ||
80 | accountsBlocklistSortValidator, | ||
81 | serversBlocklistSortValidator, | ||
82 | userNotificationsSortValidator, | ||
83 | videoPlaylistsSortValidator, | ||
84 | videoRedundanciesSortValidator, | ||
85 | videoPlaylistsSearchSortValidator, | ||
86 | accountsFollowersSortValidator, | ||
87 | videoChannelsFollowersSortValidator, | ||
88 | videoChannelSyncsSortValidator, | ||
89 | pluginsSortValidator | ||
90 | } | ||
diff --git a/server/middlewares/validators/user-email-verification.ts b/server/middlewares/validators/user-email-verification.ts new file mode 100644 index 000000000..74702a8f5 --- /dev/null +++ b/server/middlewares/validators/user-email-verification.ts | |||
@@ -0,0 +1,94 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param } from 'express-validator' | ||
3 | import { toBooleanOrNull } from '@server/helpers/custom-validators/misc' | ||
4 | import { HttpStatusCode } from '@shared/models' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { Redis } from '../../lib/redis' | ||
7 | import { areValidationErrors, checkUserEmailExist, checkUserIdExist } from './shared' | ||
8 | import { checkRegistrationEmailExist, checkRegistrationIdExist } from './shared/user-registrations' | ||
9 | |||
10 | const usersAskSendVerifyEmailValidator = [ | ||
11 | body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'), | ||
12 | |||
13 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
14 | if (areValidationErrors(req, res)) return | ||
15 | |||
16 | const [ userExists, registrationExists ] = await Promise.all([ | ||
17 | checkUserEmailExist(req.body.email, res, false), | ||
18 | checkRegistrationEmailExist(req.body.email, res, false) | ||
19 | ]) | ||
20 | |||
21 | if (!userExists && !registrationExists) { | ||
22 | logger.debug('User or registration with email %s does not exist (asking verify email).', req.body.email) | ||
23 | // Do not leak our emails | ||
24 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
25 | } | ||
26 | |||
27 | if (res.locals.user?.pluginAuth) { | ||
28 | return res.fail({ | ||
29 | status: HttpStatusCode.CONFLICT_409, | ||
30 | message: 'Cannot ask verification email of a user that uses a plugin authentication.' | ||
31 | }) | ||
32 | } | ||
33 | |||
34 | return next() | ||
35 | } | ||
36 | ] | ||
37 | |||
38 | const usersVerifyEmailValidator = [ | ||
39 | param('id') | ||
40 | .isInt().not().isEmpty().withMessage('Should have a valid id'), | ||
41 | |||
42 | body('verificationString') | ||
43 | .not().isEmpty().withMessage('Should have a valid verification string'), | ||
44 | body('isPendingEmail') | ||
45 | .optional() | ||
46 | .customSanitizer(toBooleanOrNull), | ||
47 | |||
48 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
49 | if (areValidationErrors(req, res)) return | ||
50 | if (!await checkUserIdExist(req.params.id, res)) return | ||
51 | |||
52 | const user = res.locals.user | ||
53 | const redisVerificationString = await Redis.Instance.getUserVerifyEmailLink(user.id) | ||
54 | |||
55 | if (redisVerificationString !== req.body.verificationString) { | ||
56 | return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Invalid verification string.' }) | ||
57 | } | ||
58 | |||
59 | return next() | ||
60 | } | ||
61 | ] | ||
62 | |||
63 | // --------------------------------------------------------------------------- | ||
64 | |||
65 | const registrationVerifyEmailValidator = [ | ||
66 | param('registrationId') | ||
67 | .isInt().not().isEmpty().withMessage('Should have a valid registrationId'), | ||
68 | |||
69 | body('verificationString') | ||
70 | .not().isEmpty().withMessage('Should have a valid verification string'), | ||
71 | |||
72 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
73 | if (areValidationErrors(req, res)) return | ||
74 | if (!await checkRegistrationIdExist(req.params.registrationId, res)) return | ||
75 | |||
76 | const registration = res.locals.userRegistration | ||
77 | const redisVerificationString = await Redis.Instance.getRegistrationVerifyEmailLink(registration.id) | ||
78 | |||
79 | if (redisVerificationString !== req.body.verificationString) { | ||
80 | return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Invalid verification string.' }) | ||
81 | } | ||
82 | |||
83 | return next() | ||
84 | } | ||
85 | ] | ||
86 | |||
87 | // --------------------------------------------------------------------------- | ||
88 | |||
89 | export { | ||
90 | usersAskSendVerifyEmailValidator, | ||
91 | usersVerifyEmailValidator, | ||
92 | |||
93 | registrationVerifyEmailValidator | ||
94 | } | ||
diff --git a/server/middlewares/validators/user-registrations.ts b/server/middlewares/validators/user-registrations.ts new file mode 100644 index 000000000..fcf655a2c --- /dev/null +++ b/server/middlewares/validators/user-registrations.ts | |||
@@ -0,0 +1,208 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query, ValidationChain } from 'express-validator' | ||
3 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc' | ||
4 | import { isRegistrationModerationResponseValid, isRegistrationReasonValid } from '@server/helpers/custom-validators/user-registration' | ||
5 | import { CONFIG } from '@server/initializers/config' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
7 | import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState } from '@shared/models' | ||
8 | import { isUserDisplayNameValid, isUserPasswordValid, isUserUsernameValid } from '../../helpers/custom-validators/users' | ||
9 | import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels' | ||
10 | import { isSignupAllowed, isSignupAllowedForCurrentIP, SignupMode } from '../../lib/signup' | ||
11 | import { ActorModel } from '../../models/actor/actor' | ||
12 | import { areValidationErrors, checkUserNameOrEmailDoNotAlreadyExist } from './shared' | ||
13 | import { checkRegistrationHandlesDoNotAlreadyExist, checkRegistrationIdExist } from './shared/user-registrations' | ||
14 | |||
15 | const usersDirectRegistrationValidator = usersCommonRegistrationValidatorFactory() | ||
16 | |||
17 | const usersRequestRegistrationValidator = [ | ||
18 | ...usersCommonRegistrationValidatorFactory([ | ||
19 | body('registrationReason') | ||
20 | .custom(isRegistrationReasonValid) | ||
21 | ]), | ||
22 | |||
23 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
24 | const body: UserRegistrationRequest = req.body | ||
25 | |||
26 | if (CONFIG.SIGNUP.REQUIRES_APPROVAL !== true) { | ||
27 | return res.fail({ | ||
28 | status: HttpStatusCode.BAD_REQUEST_400, | ||
29 | message: 'Signup approval is not enabled on this instance' | ||
30 | }) | ||
31 | } | ||
32 | |||
33 | const options = { username: body.username, email: body.email, channelHandle: body.channel?.name, res } | ||
34 | if (!await checkRegistrationHandlesDoNotAlreadyExist(options)) return | ||
35 | |||
36 | return next() | ||
37 | } | ||
38 | ] | ||
39 | |||
40 | // --------------------------------------------------------------------------- | ||
41 | |||
42 | function ensureUserRegistrationAllowedFactory (signupMode: SignupMode) { | ||
43 | return async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
44 | const allowedParams = { | ||
45 | body: req.body, | ||
46 | ip: req.ip, | ||
47 | signupMode | ||
48 | } | ||
49 | |||
50 | const allowedResult = await Hooks.wrapPromiseFun( | ||
51 | isSignupAllowed, | ||
52 | allowedParams, | ||
53 | |||
54 | signupMode === 'direct-registration' | ||
55 | ? 'filter:api.user.signup.allowed.result' | ||
56 | : 'filter:api.user.request-signup.allowed.result' | ||
57 | ) | ||
58 | |||
59 | if (allowedResult.allowed === false) { | ||
60 | return res.fail({ | ||
61 | status: HttpStatusCode.FORBIDDEN_403, | ||
62 | message: allowedResult.errorMessage || 'User registration is not enabled, user limit is reached or registration requires approval.' | ||
63 | }) | ||
64 | } | ||
65 | |||
66 | return next() | ||
67 | } | ||
68 | } | ||
69 | |||
70 | const ensureUserRegistrationAllowedForIP = [ | ||
71 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
72 | const allowed = isSignupAllowedForCurrentIP(req.ip) | ||
73 | |||
74 | if (allowed === false) { | ||
75 | return res.fail({ | ||
76 | status: HttpStatusCode.FORBIDDEN_403, | ||
77 | message: 'You are not on a network authorized for registration.' | ||
78 | }) | ||
79 | } | ||
80 | |||
81 | return next() | ||
82 | } | ||
83 | ] | ||
84 | |||
85 | // --------------------------------------------------------------------------- | ||
86 | |||
87 | const acceptOrRejectRegistrationValidator = [ | ||
88 | param('registrationId') | ||
89 | .custom(isIdValid), | ||
90 | |||
91 | body('moderationResponse') | ||
92 | .custom(isRegistrationModerationResponseValid), | ||
93 | |||
94 | body('preventEmailDelivery') | ||
95 | .optional() | ||
96 | .customSanitizer(toBooleanOrNull) | ||
97 | .custom(isBooleanValid).withMessage('Should have preventEmailDelivery boolean'), | ||
98 | |||
99 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
100 | if (areValidationErrors(req, res)) return | ||
101 | if (!await checkRegistrationIdExist(req.params.registrationId, res)) return | ||
102 | |||
103 | if (res.locals.userRegistration.state !== UserRegistrationState.PENDING) { | ||
104 | return res.fail({ | ||
105 | status: HttpStatusCode.CONFLICT_409, | ||
106 | message: 'This registration is already accepted or rejected.' | ||
107 | }) | ||
108 | } | ||
109 | |||
110 | return next() | ||
111 | } | ||
112 | ] | ||
113 | |||
114 | // --------------------------------------------------------------------------- | ||
115 | |||
116 | const getRegistrationValidator = [ | ||
117 | param('registrationId') | ||
118 | .custom(isIdValid), | ||
119 | |||
120 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
121 | if (areValidationErrors(req, res)) return | ||
122 | if (!await checkRegistrationIdExist(req.params.registrationId, res)) return | ||
123 | |||
124 | return next() | ||
125 | } | ||
126 | ] | ||
127 | |||
128 | // --------------------------------------------------------------------------- | ||
129 | |||
130 | const listRegistrationsValidator = [ | ||
131 | query('search') | ||
132 | .optional() | ||
133 | .custom(exists), | ||
134 | |||
135 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
136 | if (areValidationErrors(req, res)) return | ||
137 | |||
138 | return next() | ||
139 | } | ||
140 | ] | ||
141 | |||
142 | // --------------------------------------------------------------------------- | ||
143 | |||
144 | export { | ||
145 | usersDirectRegistrationValidator, | ||
146 | usersRequestRegistrationValidator, | ||
147 | |||
148 | ensureUserRegistrationAllowedFactory, | ||
149 | ensureUserRegistrationAllowedForIP, | ||
150 | |||
151 | getRegistrationValidator, | ||
152 | listRegistrationsValidator, | ||
153 | |||
154 | acceptOrRejectRegistrationValidator | ||
155 | } | ||
156 | |||
157 | // --------------------------------------------------------------------------- | ||
158 | |||
159 | function usersCommonRegistrationValidatorFactory (additionalValidationChain: ValidationChain[] = []) { | ||
160 | return [ | ||
161 | body('username') | ||
162 | .custom(isUserUsernameValid), | ||
163 | body('password') | ||
164 | .custom(isUserPasswordValid), | ||
165 | body('email') | ||
166 | .isEmail(), | ||
167 | body('displayName') | ||
168 | .optional() | ||
169 | .custom(isUserDisplayNameValid), | ||
170 | |||
171 | body('channel.name') | ||
172 | .optional() | ||
173 | .custom(isVideoChannelUsernameValid), | ||
174 | body('channel.displayName') | ||
175 | .optional() | ||
176 | .custom(isVideoChannelDisplayNameValid), | ||
177 | |||
178 | ...additionalValidationChain, | ||
179 | |||
180 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
181 | if (areValidationErrors(req, res, { omitBodyLog: true })) return | ||
182 | |||
183 | const body: UserRegister | UserRegistrationRequest = req.body | ||
184 | |||
185 | if (!await checkUserNameOrEmailDoNotAlreadyExist(body.username, body.email, res)) return | ||
186 | |||
187 | if (body.channel) { | ||
188 | if (!body.channel.name || !body.channel.displayName) { | ||
189 | return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' }) | ||
190 | } | ||
191 | |||
192 | if (body.channel.name === body.username) { | ||
193 | return res.fail({ message: 'Channel name cannot be the same as user username.' }) | ||
194 | } | ||
195 | |||
196 | const existing = await ActorModel.loadLocalByName(body.channel.name) | ||
197 | if (existing) { | ||
198 | return res.fail({ | ||
199 | status: HttpStatusCode.CONFLICT_409, | ||
200 | message: `Channel with name ${body.channel.name} already exists.` | ||
201 | }) | ||
202 | } | ||
203 | } | ||
204 | |||
205 | return next() | ||
206 | } | ||
207 | ] | ||
208 | } | ||
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 64bd9ca70..f7033f44a 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { Hooks } from '@server/lib/plugins/hooks' | ||
4 | import { forceNumber } from '@shared/core-utils' | 3 | import { forceNumber } from '@shared/core-utils' |
5 | import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models' | 4 | import { HttpStatusCode, UserRight, UserRole } from '@shared/models' |
6 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | 5 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' |
7 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | 6 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' |
8 | import { | 7 | import { |
@@ -24,17 +23,16 @@ import { | |||
24 | isUserVideoQuotaValid, | 23 | isUserVideoQuotaValid, |
25 | isUserVideosHistoryEnabledValid | 24 | isUserVideosHistoryEnabledValid |
26 | } from '../../helpers/custom-validators/users' | 25 | } from '../../helpers/custom-validators/users' |
27 | import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels' | 26 | import { isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels' |
28 | import { logger } from '../../helpers/logger' | 27 | import { logger } from '../../helpers/logger' |
29 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' | 28 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' |
30 | import { Redis } from '../../lib/redis' | 29 | import { Redis } from '../../lib/redis' |
31 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup' | ||
32 | import { ActorModel } from '../../models/actor/actor' | 30 | import { ActorModel } from '../../models/actor/actor' |
33 | import { | 31 | import { |
34 | areValidationErrors, | 32 | areValidationErrors, |
35 | checkUserEmailExist, | 33 | checkUserEmailExist, |
36 | checkUserIdExist, | 34 | checkUserIdExist, |
37 | checkUserNameOrEmailDoesNotAlreadyExist, | 35 | checkUserNameOrEmailDoNotAlreadyExist, |
38 | doesVideoChannelIdExist, | 36 | doesVideoChannelIdExist, |
39 | doesVideoExist, | 37 | doesVideoExist, |
40 | isValidVideoIdParam | 38 | isValidVideoIdParam |
@@ -81,7 +79,7 @@ const usersAddValidator = [ | |||
81 | 79 | ||
82 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 80 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
83 | if (areValidationErrors(req, res, { omitBodyLog: true })) return | 81 | if (areValidationErrors(req, res, { omitBodyLog: true })) return |
84 | if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return | 82 | if (!await checkUserNameOrEmailDoNotAlreadyExist(req.body.username, req.body.email, res)) return |
85 | 83 | ||
86 | const authUser = res.locals.oauth.token.User | 84 | const authUser = res.locals.oauth.token.User |
87 | if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) { | 85 | if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) { |
@@ -109,51 +107,6 @@ const usersAddValidator = [ | |||
109 | } | 107 | } |
110 | ] | 108 | ] |
111 | 109 | ||
112 | const usersRegisterValidator = [ | ||
113 | body('username') | ||
114 | .custom(isUserUsernameValid), | ||
115 | body('password') | ||
116 | .custom(isUserPasswordValid), | ||
117 | body('email') | ||
118 | .isEmail(), | ||
119 | body('displayName') | ||
120 | .optional() | ||
121 | .custom(isUserDisplayNameValid), | ||
122 | |||
123 | body('channel.name') | ||
124 | .optional() | ||
125 | .custom(isVideoChannelUsernameValid), | ||
126 | body('channel.displayName') | ||
127 | .optional() | ||
128 | .custom(isVideoChannelDisplayNameValid), | ||
129 | |||
130 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
131 | if (areValidationErrors(req, res, { omitBodyLog: true })) return | ||
132 | if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return | ||
133 | |||
134 | const body: UserRegister = req.body | ||
135 | if (body.channel) { | ||
136 | if (!body.channel.name || !body.channel.displayName) { | ||
137 | return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' }) | ||
138 | } | ||
139 | |||
140 | if (body.channel.name === body.username) { | ||
141 | return res.fail({ message: 'Channel name cannot be the same as user username.' }) | ||
142 | } | ||
143 | |||
144 | const existing = await ActorModel.loadLocalByName(body.channel.name) | ||
145 | if (existing) { | ||
146 | return res.fail({ | ||
147 | status: HttpStatusCode.CONFLICT_409, | ||
148 | message: `Channel with name ${body.channel.name} already exists.` | ||
149 | }) | ||
150 | } | ||
151 | } | ||
152 | |||
153 | return next() | ||
154 | } | ||
155 | ] | ||
156 | |||
157 | const usersRemoveValidator = [ | 110 | const usersRemoveValidator = [ |
158 | param('id') | 111 | param('id') |
159 | .custom(isIdValid), | 112 | .custom(isIdValid), |
@@ -365,45 +318,6 @@ const usersVideosValidator = [ | |||
365 | } | 318 | } |
366 | ] | 319 | ] |
367 | 320 | ||
368 | const ensureUserRegistrationAllowed = [ | ||
369 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
370 | const allowedParams = { | ||
371 | body: req.body, | ||
372 | ip: req.ip | ||
373 | } | ||
374 | |||
375 | const allowedResult = await Hooks.wrapPromiseFun( | ||
376 | isSignupAllowed, | ||
377 | allowedParams, | ||
378 | 'filter:api.user.signup.allowed.result' | ||
379 | ) | ||
380 | |||
381 | if (allowedResult.allowed === false) { | ||
382 | return res.fail({ | ||
383 | status: HttpStatusCode.FORBIDDEN_403, | ||
384 | message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' | ||
385 | }) | ||
386 | } | ||
387 | |||
388 | return next() | ||
389 | } | ||
390 | ] | ||
391 | |||
392 | const ensureUserRegistrationAllowedForIP = [ | ||
393 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
394 | const allowed = isSignupAllowedForCurrentIP(req.ip) | ||
395 | |||
396 | if (allowed === false) { | ||
397 | return res.fail({ | ||
398 | status: HttpStatusCode.FORBIDDEN_403, | ||
399 | message: 'You are not on a network authorized for registration.' | ||
400 | }) | ||
401 | } | ||
402 | |||
403 | return next() | ||
404 | } | ||
405 | ] | ||
406 | |||
407 | const usersAskResetPasswordValidator = [ | 321 | const usersAskResetPasswordValidator = [ |
408 | body('email') | 322 | body('email') |
409 | .isEmail(), | 323 | .isEmail(), |
@@ -455,58 +369,6 @@ const usersResetPasswordValidator = [ | |||
455 | } | 369 | } |
456 | ] | 370 | ] |
457 | 371 | ||
458 | const usersAskSendVerifyEmailValidator = [ | ||
459 | body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'), | ||
460 | |||
461 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
462 | if (areValidationErrors(req, res)) return | ||
463 | |||
464 | const exists = await checkUserEmailExist(req.body.email, res, false) | ||
465 | if (!exists) { | ||
466 | logger.debug('User with email %s does not exist (asking verify email).', req.body.email) | ||
467 | // Do not leak our emails | ||
468 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
469 | } | ||
470 | |||
471 | if (res.locals.user.pluginAuth) { | ||
472 | return res.fail({ | ||
473 | status: HttpStatusCode.CONFLICT_409, | ||
474 | message: 'Cannot ask verification email of a user that uses a plugin authentication.' | ||
475 | }) | ||
476 | } | ||
477 | |||
478 | return next() | ||
479 | } | ||
480 | ] | ||
481 | |||
482 | const usersVerifyEmailValidator = [ | ||
483 | param('id') | ||
484 | .isInt().not().isEmpty().withMessage('Should have a valid id'), | ||
485 | |||
486 | body('verificationString') | ||
487 | .not().isEmpty().withMessage('Should have a valid verification string'), | ||
488 | body('isPendingEmail') | ||
489 | .optional() | ||
490 | .customSanitizer(toBooleanOrNull), | ||
491 | |||
492 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
493 | if (areValidationErrors(req, res)) return | ||
494 | if (!await checkUserIdExist(req.params.id, res)) return | ||
495 | |||
496 | const user = res.locals.user | ||
497 | const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id) | ||
498 | |||
499 | if (redisVerificationString !== req.body.verificationString) { | ||
500 | return res.fail({ | ||
501 | status: HttpStatusCode.FORBIDDEN_403, | ||
502 | message: 'Invalid verification string.' | ||
503 | }) | ||
504 | } | ||
505 | |||
506 | return next() | ||
507 | } | ||
508 | ] | ||
509 | |||
510 | const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => { | 372 | const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => { |
511 | return [ | 373 | return [ |
512 | body('currentPassword').optional().custom(exists), | 374 | body('currentPassword').optional().custom(exists), |
@@ -603,21 +465,16 @@ export { | |||
603 | usersListValidator, | 465 | usersListValidator, |
604 | usersAddValidator, | 466 | usersAddValidator, |
605 | deleteMeValidator, | 467 | deleteMeValidator, |
606 | usersRegisterValidator, | ||
607 | usersBlockingValidator, | 468 | usersBlockingValidator, |
608 | usersRemoveValidator, | 469 | usersRemoveValidator, |
609 | usersUpdateValidator, | 470 | usersUpdateValidator, |
610 | usersUpdateMeValidator, | 471 | usersUpdateMeValidator, |
611 | usersVideoRatingValidator, | 472 | usersVideoRatingValidator, |
612 | usersCheckCurrentPasswordFactory, | 473 | usersCheckCurrentPasswordFactory, |
613 | ensureUserRegistrationAllowed, | ||
614 | ensureUserRegistrationAllowedForIP, | ||
615 | usersGetValidator, | 474 | usersGetValidator, |
616 | usersVideosValidator, | 475 | usersVideosValidator, |
617 | usersAskResetPasswordValidator, | 476 | usersAskResetPasswordValidator, |
618 | usersResetPasswordValidator, | 477 | usersResetPasswordValidator, |
619 | usersAskSendVerifyEmailValidator, | ||
620 | usersVerifyEmailValidator, | ||
621 | userAutocompleteValidator, | 478 | userAutocompleteValidator, |
622 | ensureAuthUserOwnsAccountValidator, | 479 | ensureAuthUserOwnsAccountValidator, |
623 | ensureCanModerateUser, | 480 | ensureCanModerateUser, |
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts index 20008768b..14a5bffa2 100644 --- a/server/models/abuse/abuse-message.ts +++ b/server/models/abuse/abuse-message.ts | |||
@@ -5,7 +5,7 @@ import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' | |||
5 | import { AbuseMessage } from '@shared/models' | 5 | import { AbuseMessage } from '@shared/models' |
6 | import { AttributesOnly } from '@shared/typescript-utils' | 6 | import { AttributesOnly } from '@shared/typescript-utils' |
7 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 7 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' |
8 | import { getSort, throwIfNotValid } from '../utils' | 8 | import { getSort, throwIfNotValid } from '../shared' |
9 | import { AbuseModel } from './abuse' | 9 | import { AbuseModel } from './abuse' |
10 | 10 | ||
11 | @Table({ | 11 | @Table({ |
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index 4c6a96a86..4ce40bf2f 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts | |||
@@ -34,13 +34,13 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
34 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' | 34 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
35 | import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' | 35 | import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' |
36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | 36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
37 | import { getSort, throwIfNotValid } from '../utils' | 37 | import { getSort, throwIfNotValid } from '../shared' |
38 | import { ThumbnailModel } from '../video/thumbnail' | 38 | import { ThumbnailModel } from '../video/thumbnail' |
39 | import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video' | 39 | import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video' |
40 | import { VideoBlacklistModel } from '../video/video-blacklist' | 40 | import { VideoBlacklistModel } from '../video/video-blacklist' |
41 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' | 41 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' |
42 | import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment' | 42 | import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment' |
43 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' | 43 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './sql/abuse-query-builder' |
44 | import { VideoAbuseModel } from './video-abuse' | 44 | import { VideoAbuseModel } from './video-abuse' |
45 | import { VideoCommentAbuseModel } from './video-comment-abuse' | 45 | import { VideoCommentAbuseModel } from './video-comment-abuse' |
46 | 46 | ||
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/sql/abuse-query-builder.ts index 74f4542e5..282d4541a 100644 --- a/server/models/abuse/abuse-query-builder.ts +++ b/server/models/abuse/sql/abuse-query-builder.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | import { exists } from '@server/helpers/custom-validators/misc' | 2 | import { exists } from '@server/helpers/custom-validators/misc' |
3 | import { forceNumber } from '@shared/core-utils' | 3 | import { forceNumber } from '@shared/core-utils' |
4 | import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' | 4 | import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' |
5 | import { buildBlockedAccountSQL, buildDirectionAndField } from '../utils' | 5 | import { buildBlockedAccountSQL, buildSortDirectionAndField } from '../../shared' |
6 | 6 | ||
7 | export type BuildAbusesQueryOptions = { | 7 | export type BuildAbusesQueryOptions = { |
8 | start: number | 8 | start: number |
@@ -157,7 +157,7 @@ function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | | |||
157 | } | 157 | } |
158 | 158 | ||
159 | function buildAbuseOrder (value: string) { | 159 | function buildAbuseOrder (value: string) { |
160 | const { direction, field } = buildDirectionAndField(value) | 160 | const { direction, field } = buildSortDirectionAndField(value) |
161 | 161 | ||
162 | return `ORDER BY "abuse"."${field}" ${direction}` | 162 | return `ORDER BY "abuse"."${field}" ${direction}` |
163 | } | 163 | } |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 377249b38..f6212ff6e 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -6,7 +6,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
6 | import { AccountBlock } from '../../../shared/models' | 6 | import { AccountBlock } from '../../../shared/models' |
7 | import { ActorModel } from '../actor/actor' | 7 | import { ActorModel } from '../actor/actor' |
8 | import { ServerModel } from '../server/server' | 8 | import { ServerModel } from '../server/server' |
9 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 9 | import { createSafeIn, getSort, searchAttribute } from '../shared' |
10 | import { AccountModel } from './account' | 10 | import { AccountModel } from './account' |
11 | 11 | ||
12 | @Table({ | 12 | @Table({ |
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index 7afc907da..9e7ef4394 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -11,7 +11,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
12 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' | 12 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' |
13 | import { ActorModel } from '../actor/actor' | 13 | import { ActorModel } from '../actor/actor' |
14 | import { getSort, throwIfNotValid } from '../utils' | 14 | import { getSort, throwIfNotValid } from '../shared' |
15 | import { VideoModel } from '../video/video' | 15 | import { VideoModel } from '../video/video' |
16 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' | 16 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' |
17 | import { AccountModel } from './account' | 17 | import { AccountModel } from './account' |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 8a7dfba94..dc989417b 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -16,7 +16,7 @@ import { | |||
16 | Table, | 16 | Table, |
17 | UpdatedAt | 17 | UpdatedAt |
18 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { ModelCache } from '@server/models/model-cache' | 19 | import { ModelCache } from '@server/models/shared/model-cache' |
20 | import { AttributesOnly } from '@shared/typescript-utils' | 20 | import { AttributesOnly } from '@shared/typescript-utils' |
21 | import { Account, AccountSummary } from '../../../shared/models/actors' | 21 | import { Account, AccountSummary } from '../../../shared/models/actors' |
22 | import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' | 22 | import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' |
@@ -38,7 +38,7 @@ import { ApplicationModel } from '../application/application' | |||
38 | import { ServerModel } from '../server/server' | 38 | import { ServerModel } from '../server/server' |
39 | import { ServerBlocklistModel } from '../server/server-blocklist' | 39 | import { ServerBlocklistModel } from '../server/server-blocklist' |
40 | import { UserModel } from '../user/user' | 40 | import { UserModel } from '../user/user' |
41 | import { getSort, throwIfNotValid } from '../utils' | 41 | import { buildSQLAttributes, getSort, throwIfNotValid } from '../shared' |
42 | import { VideoModel } from '../video/video' | 42 | import { VideoModel } from '../video/video' |
43 | import { VideoChannelModel } from '../video/video-channel' | 43 | import { VideoChannelModel } from '../video/video-channel' |
44 | import { VideoCommentModel } from '../video/video-comment' | 44 | import { VideoCommentModel } from '../video/video-comment' |
@@ -251,6 +251,18 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
251 | return undefined | 251 | return undefined |
252 | } | 252 | } |
253 | 253 | ||
254 | // --------------------------------------------------------------------------- | ||
255 | |||
256 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
257 | return buildSQLAttributes({ | ||
258 | model: this, | ||
259 | tableName, | ||
260 | aliasPrefix | ||
261 | }) | ||
262 | } | ||
263 | |||
264 | // --------------------------------------------------------------------------- | ||
265 | |||
254 | static load (id: number, transaction?: Transaction): Promise<MAccountDefault> { | 266 | static load (id: number, transaction?: Transaction): Promise<MAccountDefault> { |
255 | return AccountModel.findByPk(id, { transaction }) | 267 | return AccountModel.findByPk(id, { transaction }) |
256 | } | 268 | } |
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 9615229dd..32e5d78b0 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -38,7 +38,7 @@ import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAM | |||
38 | import { AccountModel } from '../account/account' | 38 | import { AccountModel } from '../account/account' |
39 | import { ServerModel } from '../server/server' | 39 | import { ServerModel } from '../server/server' |
40 | import { doesExist } from '../shared/query' | 40 | import { doesExist } from '../shared/query' |
41 | import { createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../utils' | 41 | import { buildSQLAttributes, createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../shared' |
42 | import { VideoChannelModel } from '../video/video-channel' | 42 | import { VideoChannelModel } from '../video/video-channel' |
43 | import { ActorModel, unusedActorAttributesForAPI } from './actor' | 43 | import { ActorModel, unusedActorAttributesForAPI } from './actor' |
44 | import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' | 44 | import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' |
@@ -140,6 +140,18 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
140 | }) | 140 | }) |
141 | } | 141 | } |
142 | 142 | ||
143 | // --------------------------------------------------------------------------- | ||
144 | |||
145 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
146 | return buildSQLAttributes({ | ||
147 | model: this, | ||
148 | tableName, | ||
149 | aliasPrefix | ||
150 | }) | ||
151 | } | ||
152 | |||
153 | // --------------------------------------------------------------------------- | ||
154 | |||
143 | /* | 155 | /* |
144 | * @deprecated Use `findOrCreateCustom` instead | 156 | * @deprecated Use `findOrCreateCustom` instead |
145 | */ | 157 | */ |
@@ -213,7 +225,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
213 | `WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId AND "state" = 'accepted' ` + | 225 | `WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId AND "state" = 'accepted' ` + |
214 | `LIMIT 1` | 226 | `LIMIT 1` |
215 | 227 | ||
216 | return doesExist(query, { actorId, followerActorId }) | 228 | return doesExist(this.sequelize, query, { actorId, followerActorId }) |
217 | } | 229 | } |
218 | 230 | ||
219 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { | 231 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { |
diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts index f2b3b2f4b..9c34a0101 100644 --- a/server/models/actor/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -22,7 +22,7 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp | |||
22 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
23 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
24 | import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants' | 24 | import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants' |
25 | import { throwIfNotValid } from '../utils' | 25 | import { buildSQLAttributes, throwIfNotValid } from '../shared' |
26 | import { ActorModel } from './actor' | 26 | import { ActorModel } from './actor' |
27 | 27 | ||
28 | @Table({ | 28 | @Table({ |
@@ -94,6 +94,18 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
94 | .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, { err })) | 94 | .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, { err })) |
95 | } | 95 | } |
96 | 96 | ||
97 | // --------------------------------------------------------------------------- | ||
98 | |||
99 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
100 | return buildSQLAttributes({ | ||
101 | model: this, | ||
102 | tableName, | ||
103 | aliasPrefix | ||
104 | }) | ||
105 | } | ||
106 | |||
107 | // --------------------------------------------------------------------------- | ||
108 | |||
97 | static loadByName (filename: string) { | 109 | static loadByName (filename: string) { |
98 | const query = { | 110 | const query = { |
99 | where: { | 111 | where: { |
diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts index d7afa727d..1432e8757 100644 --- a/server/models/actor/actor.ts +++ b/server/models/actor/actor.ts | |||
@@ -17,7 +17,7 @@ import { | |||
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { activityPubContextify } from '@server/lib/activitypub/context' | 18 | import { activityPubContextify } from '@server/lib/activitypub/context' |
19 | import { getBiggestActorImage } from '@server/lib/actor-image' | 19 | import { getBiggestActorImage } from '@server/lib/actor-image' |
20 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/shared/model-cache' |
21 | import { forceNumber, getLowercaseExtension } from '@shared/core-utils' | 21 | import { forceNumber, getLowercaseExtension } from '@shared/core-utils' |
22 | import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' | 22 | import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' |
23 | import { AttributesOnly } from '@shared/typescript-utils' | 23 | import { AttributesOnly } from '@shared/typescript-utils' |
@@ -55,7 +55,7 @@ import { | |||
55 | import { AccountModel } from '../account/account' | 55 | import { AccountModel } from '../account/account' |
56 | import { getServerActor } from '../application/application' | 56 | import { getServerActor } from '../application/application' |
57 | import { ServerModel } from '../server/server' | 57 | import { ServerModel } from '../server/server' |
58 | import { isOutdated, throwIfNotValid } from '../utils' | 58 | import { buildSQLAttributes, isOutdated, throwIfNotValid } from '../shared' |
59 | import { VideoModel } from '../video/video' | 59 | import { VideoModel } from '../video/video' |
60 | import { VideoChannelModel } from '../video/video-channel' | 60 | import { VideoChannelModel } from '../video/video-channel' |
61 | import { ActorFollowModel } from './actor-follow' | 61 | import { ActorFollowModel } from './actor-follow' |
@@ -65,7 +65,7 @@ enum ScopeNames { | |||
65 | FULL = 'FULL' | 65 | FULL = 'FULL' |
66 | } | 66 | } |
67 | 67 | ||
68 | export const unusedActorAttributesForAPI = [ | 68 | export const unusedActorAttributesForAPI: (keyof AttributesOnly<ActorModel>)[] = [ |
69 | 'publicKey', | 69 | 'publicKey', |
70 | 'privateKey', | 70 | 'privateKey', |
71 | 'inboxUrl', | 71 | 'inboxUrl', |
@@ -306,6 +306,27 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
306 | }) | 306 | }) |
307 | VideoChannel: VideoChannelModel | 307 | VideoChannel: VideoChannelModel |
308 | 308 | ||
309 | // --------------------------------------------------------------------------- | ||
310 | |||
311 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
312 | return buildSQLAttributes({ | ||
313 | model: this, | ||
314 | tableName, | ||
315 | aliasPrefix | ||
316 | }) | ||
317 | } | ||
318 | |||
319 | static getSQLAPIAttributes (tableName: string, aliasPrefix = '') { | ||
320 | return buildSQLAttributes({ | ||
321 | model: this, | ||
322 | tableName, | ||
323 | aliasPrefix, | ||
324 | excludeAttributes: unusedActorAttributesForAPI | ||
325 | }) | ||
326 | } | ||
327 | |||
328 | // --------------------------------------------------------------------------- | ||
329 | |||
309 | static async load (id: number): Promise<MActor> { | 330 | static async load (id: number): Promise<MActor> { |
310 | const actorServer = await getServerActor() | 331 | const actorServer = await getServerActor() |
311 | if (id === actorServer.id) return actorServer | 332 | if (id === actorServer.id) return actorServer |
diff --git a/server/models/actor/sql/instance-list-followers-query-builder.ts b/server/models/actor/sql/instance-list-followers-query-builder.ts index 4a17a8f11..34ce29b5d 100644 --- a/server/models/actor/sql/instance-list-followers-query-builder.ts +++ b/server/models/actor/sql/instance-list-followers-query-builder.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { ModelBuilder } from '@server/models/shared' | 2 | import { ModelBuilder } from '@server/models/shared' |
3 | import { parseRowCountResult } from '@server/models/utils' | ||
4 | import { MActorFollowActorsDefault } from '@server/types/models' | 3 | import { MActorFollowActorsDefault } from '@server/types/models' |
5 | import { ActivityPubActorType, FollowState } from '@shared/models' | 4 | import { ActivityPubActorType, FollowState } from '@shared/models' |
5 | import { parseRowCountResult } from '../../shared' | ||
6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' | 6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' |
7 | 7 | ||
8 | export interface ListFollowersOptions { | 8 | export interface ListFollowersOptions { |
diff --git a/server/models/actor/sql/instance-list-following-query-builder.ts b/server/models/actor/sql/instance-list-following-query-builder.ts index 880170b85..77b4e3dce 100644 --- a/server/models/actor/sql/instance-list-following-query-builder.ts +++ b/server/models/actor/sql/instance-list-following-query-builder.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { ModelBuilder } from '@server/models/shared' | 2 | import { ModelBuilder } from '@server/models/shared' |
3 | import { parseRowCountResult } from '@server/models/utils' | ||
4 | import { MActorFollowActorsDefault } from '@server/types/models' | 3 | import { MActorFollowActorsDefault } from '@server/types/models' |
5 | import { ActivityPubActorType, FollowState } from '@shared/models' | 4 | import { ActivityPubActorType, FollowState } from '@shared/models' |
5 | import { parseRowCountResult } from '../../shared' | ||
6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' | 6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' |
7 | 7 | ||
8 | export interface ListFollowingOptions { | 8 | export interface ListFollowingOptions { |
diff --git a/server/models/actor/sql/shared/actor-follow-table-attributes.ts b/server/models/actor/sql/shared/actor-follow-table-attributes.ts index 156b37d44..7dd908ece 100644 --- a/server/models/actor/sql/shared/actor-follow-table-attributes.ts +++ b/server/models/actor/sql/shared/actor-follow-table-attributes.ts | |||
@@ -1,62 +1,31 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { Memoize } from '@server/helpers/memoize' | ||
3 | import { ServerModel } from '@server/models/server/server' | ||
4 | import { ActorModel } from '../../actor' | ||
5 | import { ActorFollowModel } from '../../actor-follow' | ||
6 | import { ActorImageModel } from '../../actor-image' | ||
7 | |||
1 | export class ActorFollowTableAttributes { | 8 | export class ActorFollowTableAttributes { |
2 | 9 | ||
10 | @Memoize() | ||
3 | getFollowAttributes () { | 11 | getFollowAttributes () { |
4 | return [ | 12 | logger.error('coucou') |
5 | '"ActorFollowModel"."id"', | 13 | |
6 | '"ActorFollowModel"."state"', | 14 | return ActorFollowModel.getSQLAttributes('ActorFollowModel').join(', ') |
7 | '"ActorFollowModel"."score"', | ||
8 | '"ActorFollowModel"."url"', | ||
9 | '"ActorFollowModel"."actorId"', | ||
10 | '"ActorFollowModel"."targetActorId"', | ||
11 | '"ActorFollowModel"."createdAt"', | ||
12 | '"ActorFollowModel"."updatedAt"' | ||
13 | ].join(', ') | ||
14 | } | 15 | } |
15 | 16 | ||
17 | @Memoize() | ||
16 | getActorAttributes (actorTableName: string) { | 18 | getActorAttributes (actorTableName: string) { |
17 | return [ | 19 | return ActorModel.getSQLAttributes(actorTableName, `${actorTableName}.`).join(', ') |
18 | `"${actorTableName}"."id" AS "${actorTableName}.id"`, | ||
19 | `"${actorTableName}"."type" AS "${actorTableName}.type"`, | ||
20 | `"${actorTableName}"."preferredUsername" AS "${actorTableName}.preferredUsername"`, | ||
21 | `"${actorTableName}"."url" AS "${actorTableName}.url"`, | ||
22 | `"${actorTableName}"."publicKey" AS "${actorTableName}.publicKey"`, | ||
23 | `"${actorTableName}"."privateKey" AS "${actorTableName}.privateKey"`, | ||
24 | `"${actorTableName}"."followersCount" AS "${actorTableName}.followersCount"`, | ||
25 | `"${actorTableName}"."followingCount" AS "${actorTableName}.followingCount"`, | ||
26 | `"${actorTableName}"."inboxUrl" AS "${actorTableName}.inboxUrl"`, | ||
27 | `"${actorTableName}"."outboxUrl" AS "${actorTableName}.outboxUrl"`, | ||
28 | `"${actorTableName}"."sharedInboxUrl" AS "${actorTableName}.sharedInboxUrl"`, | ||
29 | `"${actorTableName}"."followersUrl" AS "${actorTableName}.followersUrl"`, | ||
30 | `"${actorTableName}"."followingUrl" AS "${actorTableName}.followingUrl"`, | ||
31 | `"${actorTableName}"."remoteCreatedAt" AS "${actorTableName}.remoteCreatedAt"`, | ||
32 | `"${actorTableName}"."serverId" AS "${actorTableName}.serverId"`, | ||
33 | `"${actorTableName}"."createdAt" AS "${actorTableName}.createdAt"`, | ||
34 | `"${actorTableName}"."updatedAt" AS "${actorTableName}.updatedAt"` | ||
35 | ].join(', ') | ||
36 | } | 20 | } |
37 | 21 | ||
22 | @Memoize() | ||
38 | getServerAttributes (actorTableName: string) { | 23 | getServerAttributes (actorTableName: string) { |
39 | return [ | 24 | return ServerModel.getSQLAttributes(`${actorTableName}->Server`, `${actorTableName}.Server.`).join(', ') |
40 | `"${actorTableName}->Server"."id" AS "${actorTableName}.Server.id"`, | ||
41 | `"${actorTableName}->Server"."host" AS "${actorTableName}.Server.host"`, | ||
42 | `"${actorTableName}->Server"."redundancyAllowed" AS "${actorTableName}.Server.redundancyAllowed"`, | ||
43 | `"${actorTableName}->Server"."createdAt" AS "${actorTableName}.Server.createdAt"`, | ||
44 | `"${actorTableName}->Server"."updatedAt" AS "${actorTableName}.Server.updatedAt"` | ||
45 | ].join(', ') | ||
46 | } | 25 | } |
47 | 26 | ||
27 | @Memoize() | ||
48 | getAvatarAttributes (actorTableName: string) { | 28 | getAvatarAttributes (actorTableName: string) { |
49 | return [ | 29 | return ActorImageModel.getSQLAttributes(`${actorTableName}->Avatars`, `${actorTableName}.Avatars.`).join(', ') |
50 | `"${actorTableName}->Avatars"."id" AS "${actorTableName}.Avatars.id"`, | ||
51 | `"${actorTableName}->Avatars"."filename" AS "${actorTableName}.Avatars.filename"`, | ||
52 | `"${actorTableName}->Avatars"."height" AS "${actorTableName}.Avatars.height"`, | ||
53 | `"${actorTableName}->Avatars"."width" AS "${actorTableName}.Avatars.width"`, | ||
54 | `"${actorTableName}->Avatars"."fileUrl" AS "${actorTableName}.Avatars.fileUrl"`, | ||
55 | `"${actorTableName}->Avatars"."onDisk" AS "${actorTableName}.Avatars.onDisk"`, | ||
56 | `"${actorTableName}->Avatars"."type" AS "${actorTableName}.Avatars.type"`, | ||
57 | `"${actorTableName}->Avatars"."actorId" AS "${actorTableName}.Avatars.actorId"`, | ||
58 | `"${actorTableName}->Avatars"."createdAt" AS "${actorTableName}.Avatars.createdAt"`, | ||
59 | `"${actorTableName}->Avatars"."updatedAt" AS "${actorTableName}.Avatars.updatedAt"` | ||
60 | ].join(', ') | ||
61 | } | 30 | } |
62 | } | 31 | } |
diff --git a/server/models/actor/sql/shared/instance-list-follows-query-builder.ts b/server/models/actor/sql/shared/instance-list-follows-query-builder.ts index 1d70fbe70..d9593e48b 100644 --- a/server/models/actor/sql/shared/instance-list-follows-query-builder.ts +++ b/server/models/actor/sql/shared/instance-list-follows-query-builder.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { AbstractRunQuery } from '@server/models/shared' | 2 | import { AbstractRunQuery } from '@server/models/shared' |
3 | import { getInstanceFollowsSort } from '@server/models/utils' | ||
4 | import { ActorImageType } from '@shared/models' | 3 | import { ActorImageType } from '@shared/models' |
4 | import { getInstanceFollowsSort } from '../../../shared' | ||
5 | import { ActorFollowTableAttributes } from './actor-follow-table-attributes' | 5 | import { ActorFollowTableAttributes } from './actor-follow-table-attributes' |
6 | 6 | ||
7 | type BaseOptions = { | 7 | type BaseOptions = { |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 15909d5f3..c2a72b71f 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -34,7 +34,7 @@ import { CONFIG } from '../../initializers/config' | |||
34 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' | 34 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' |
35 | import { ActorModel } from '../actor/actor' | 35 | import { ActorModel } from '../actor/actor' |
36 | import { ServerModel } from '../server/server' | 36 | import { ServerModel } from '../server/server' |
37 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' | 37 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../shared' |
38 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' | 38 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' |
39 | import { VideoModel } from '../video/video' | 39 | import { VideoModel } from '../video/video' |
40 | import { VideoChannelModel } from '../video/video-channel' | 40 | import { VideoChannelModel } from '../video/video-channel' |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 71c205ffa..9948c9f7a 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | isPluginStableVersionValid, | 11 | isPluginStableVersionValid, |
12 | isPluginTypeValid | 12 | isPluginTypeValid |
13 | } from '../../helpers/custom-validators/plugins' | 13 | } from '../../helpers/custom-validators/plugins' |
14 | import { getSort, throwIfNotValid } from '../utils' | 14 | import { getSort, throwIfNotValid } from '../shared' |
15 | 15 | ||
16 | @DefaultScope(() => ({ | 16 | @DefaultScope(() => ({ |
17 | attributes: { | 17 | attributes: { |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 9752dfbc3..3d755fe4a 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -4,7 +4,7 @@ import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormat | |||
4 | import { ServerBlock } from '@shared/models' | 4 | import { ServerBlock } from '@shared/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | 5 | import { AttributesOnly } from '@shared/typescript-utils' |
6 | import { AccountModel } from '../account/account' | 6 | import { AccountModel } from '../account/account' |
7 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 7 | import { createSafeIn, getSort, searchAttribute } from '../shared' |
8 | import { ServerModel } from './server' | 8 | import { ServerModel } from './server' |
9 | 9 | ||
10 | enum ScopeNames { | 10 | enum ScopeNames { |
diff --git a/server/models/server/server.ts b/server/models/server/server.ts index ef42de090..a5e05f460 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts | |||
@@ -4,7 +4,7 @@ import { MServer, MServerFormattable } from '@server/types/models/server' | |||
4 | import { AttributesOnly } from '@shared/typescript-utils' | 4 | import { AttributesOnly } from '@shared/typescript-utils' |
5 | import { isHostValid } from '../../helpers/custom-validators/servers' | 5 | import { isHostValid } from '../../helpers/custom-validators/servers' |
6 | import { ActorModel } from '../actor/actor' | 6 | import { ActorModel } from '../actor/actor' |
7 | import { throwIfNotValid } from '../utils' | 7 | import { buildSQLAttributes, throwIfNotValid } from '../shared' |
8 | import { ServerBlocklistModel } from './server-blocklist' | 8 | import { ServerBlocklistModel } from './server-blocklist' |
9 | 9 | ||
10 | @Table({ | 10 | @Table({ |
@@ -52,6 +52,18 @@ export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> { | |||
52 | }) | 52 | }) |
53 | BlockedBy: ServerBlocklistModel[] | 53 | BlockedBy: ServerBlocklistModel[] |
54 | 54 | ||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
57 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
58 | return buildSQLAttributes({ | ||
59 | model: this, | ||
60 | tableName, | ||
61 | aliasPrefix | ||
62 | }) | ||
63 | } | ||
64 | |||
65 | // --------------------------------------------------------------------------- | ||
66 | |||
55 | static load (id: number, transaction?: Transaction): Promise<MServer> { | 67 | static load (id: number, transaction?: Transaction): Promise<MServer> { |
56 | const query = { | 68 | const query = { |
57 | where: { | 69 | where: { |
diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts index 04528929c..5a7621e4d 100644 --- a/server/models/shared/index.ts +++ b/server/models/shared/index.ts | |||
@@ -1,4 +1,8 @@ | |||
1 | export * from './abstract-run-query' | 1 | export * from './abstract-run-query' |
2 | export * from './model-builder' | 2 | export * from './model-builder' |
3 | export * from './model-cache' | ||
3 | export * from './query' | 4 | export * from './query' |
5 | export * from './sequelize-helpers' | ||
6 | export * from './sort' | ||
7 | export * from './sql' | ||
4 | export * from './update' | 8 | export * from './update' |
diff --git a/server/models/shared/model-builder.ts b/server/models/shared/model-builder.ts index c015ca4f5..07f7c4038 100644 --- a/server/models/shared/model-builder.ts +++ b/server/models/shared/model-builder.ts | |||
@@ -1,7 +1,24 @@ | |||
1 | import { isPlainObject } from 'lodash' | 1 | import { isPlainObject } from 'lodash' |
2 | import { Model as SequelizeModel, Sequelize } from 'sequelize' | 2 | import { Model as SequelizeModel, ModelStatic, Sequelize } from 'sequelize' |
3 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
4 | 4 | ||
5 | /** | ||
6 | * | ||
7 | * Build Sequelize models from sequelize raw query (that must use { nest: true } options) | ||
8 | * | ||
9 | * In order to sequelize to correctly build the JSON this class will ingest, | ||
10 | * the columns selected in the raw query should be in the following form: | ||
11 | * * All tables must be Pascal Cased (for example "VideoChannel") | ||
12 | * * Root table must end with `Model` (for example "VideoCommentModel") | ||
13 | * * Joined tables must contain the origin table name + '->JoinedTable'. For example: | ||
14 | * * "Actor" is joined to "Account": "Actor" table must be renamed "Account->Actor" | ||
15 | * * "Account->Actor" is joined to "Server": "Server" table must be renamed to "Account->Actor->Server" | ||
16 | * * Selected columns must be renamed to contain the JSON path: | ||
17 | * * "videoComment"."id": "VideoCommentModel"."id" | ||
18 | * * "Account"."Actor"."Server"."id": "Account.Actor.Server.id" | ||
19 | * * All tables must contain the row id | ||
20 | */ | ||
21 | |||
5 | export class ModelBuilder <T extends SequelizeModel> { | 22 | export class ModelBuilder <T extends SequelizeModel> { |
6 | private readonly modelRegistry = new Map<string, T>() | 23 | private readonly modelRegistry = new Map<string, T>() |
7 | 24 | ||
@@ -72,18 +89,18 @@ export class ModelBuilder <T extends SequelizeModel> { | |||
72 | 'Cannot build model %s that does not exist', this.buildSequelizeModelName(modelName), | 89 | 'Cannot build model %s that does not exist', this.buildSequelizeModelName(modelName), |
73 | { existing: this.sequelize.modelManager.all.map(m => m.name) } | 90 | { existing: this.sequelize.modelManager.all.map(m => m.name) } |
74 | ) | 91 | ) |
75 | return undefined | 92 | return { created: false, model: null } |
76 | } | 93 | } |
77 | 94 | ||
78 | // FIXME: typings | 95 | const model = Model.build(json, { raw: true, isNewRecord: false }) |
79 | const model = new (Model as any)(json) | 96 | |
80 | this.modelRegistry.set(registryKey, model) | 97 | this.modelRegistry.set(registryKey, model) |
81 | 98 | ||
82 | return { created: true, model } | 99 | return { created: true, model } |
83 | } | 100 | } |
84 | 101 | ||
85 | private findModelBuilder (modelName: string) { | 102 | private findModelBuilder (modelName: string) { |
86 | return this.sequelize.modelManager.getModel(this.buildSequelizeModelName(modelName)) | 103 | return this.sequelize.modelManager.getModel(this.buildSequelizeModelName(modelName)) as ModelStatic<T> |
87 | } | 104 | } |
88 | 105 | ||
89 | private buildSequelizeModelName (modelName: string) { | 106 | private buildSequelizeModelName (modelName: string) { |
diff --git a/server/models/model-cache.ts b/server/models/shared/model-cache.ts index 3651267e7..3651267e7 100644 --- a/server/models/model-cache.ts +++ b/server/models/shared/model-cache.ts | |||
diff --git a/server/models/shared/query.ts b/server/models/shared/query.ts index 036cc13c6..934acc21f 100644 --- a/server/models/shared/query.ts +++ b/server/models/shared/query.ts | |||
@@ -1,17 +1,82 @@ | |||
1 | import { BindOrReplacements, QueryTypes } from 'sequelize' | 1 | import { BindOrReplacements, Op, QueryTypes, Sequelize } from 'sequelize' |
2 | import { sequelizeTypescript } from '@server/initializers/database' | 2 | import validator from 'validator' |
3 | import { forceNumber } from '@shared/core-utils' | ||
3 | 4 | ||
4 | function doesExist (query: string, bind?: BindOrReplacements) { | 5 | function doesExist (sequelize: Sequelize, query: string, bind?: BindOrReplacements) { |
5 | const options = { | 6 | const options = { |
6 | type: QueryTypes.SELECT as QueryTypes.SELECT, | 7 | type: QueryTypes.SELECT as QueryTypes.SELECT, |
7 | bind, | 8 | bind, |
8 | raw: true | 9 | raw: true |
9 | } | 10 | } |
10 | 11 | ||
11 | return sequelizeTypescript.query(query, options) | 12 | return sequelize.query(query, options) |
12 | .then(results => results.length === 1) | 13 | .then(results => results.length === 1) |
13 | } | 14 | } |
14 | 15 | ||
16 | function createSimilarityAttribute (col: string, value: string) { | ||
17 | return Sequelize.fn( | ||
18 | 'similarity', | ||
19 | |||
20 | searchTrigramNormalizeCol(col), | ||
21 | |||
22 | searchTrigramNormalizeValue(value) | ||
23 | ) | ||
24 | } | ||
25 | |||
26 | function buildWhereIdOrUUID (id: number | string) { | ||
27 | return validator.isInt('' + id) ? { id } : { uuid: id } | ||
28 | } | ||
29 | |||
30 | function parseAggregateResult (result: any) { | ||
31 | if (!result) return 0 | ||
32 | |||
33 | const total = forceNumber(result) | ||
34 | if (isNaN(total)) return 0 | ||
35 | |||
36 | return total | ||
37 | } | ||
38 | |||
39 | function parseRowCountResult (result: any) { | ||
40 | if (result.length !== 0) return result[0].total | ||
41 | |||
42 | return 0 | ||
43 | } | ||
44 | |||
45 | function createSafeIn (sequelize: Sequelize, toEscape: (string | number)[], additionalUnescaped: string[] = []) { | ||
46 | return toEscape.map(t => { | ||
47 | return t === null | ||
48 | ? null | ||
49 | : sequelize.escape('' + t) | ||
50 | }).concat(additionalUnescaped).join(', ') | ||
51 | } | ||
52 | |||
53 | function searchAttribute (sourceField?: string, targetField?: string) { | ||
54 | if (!sourceField) return {} | ||
55 | |||
56 | return { | ||
57 | [targetField]: { | ||
58 | // FIXME: ts error | ||
59 | [Op.iLike as any]: `%${sourceField}%` | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | |||
15 | export { | 64 | export { |
16 | doesExist | 65 | doesExist, |
66 | createSimilarityAttribute, | ||
67 | buildWhereIdOrUUID, | ||
68 | parseAggregateResult, | ||
69 | parseRowCountResult, | ||
70 | createSafeIn, | ||
71 | searchAttribute | ||
72 | } | ||
73 | |||
74 | // --------------------------------------------------------------------------- | ||
75 | |||
76 | function searchTrigramNormalizeValue (value: string) { | ||
77 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value)) | ||
78 | } | ||
79 | |||
80 | function searchTrigramNormalizeCol (col: string) { | ||
81 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) | ||
17 | } | 82 | } |
diff --git a/server/models/shared/sequelize-helpers.ts b/server/models/shared/sequelize-helpers.ts new file mode 100644 index 000000000..7af8471dc --- /dev/null +++ b/server/models/shared/sequelize-helpers.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | |||
3 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { | ||
4 | if (!model.createdAt || !model.updatedAt) { | ||
5 | throw new Error('Miss createdAt & updatedAt attributes to model') | ||
6 | } | ||
7 | |||
8 | const now = Date.now() | ||
9 | const createdAtTime = model.createdAt.getTime() | ||
10 | const updatedAtTime = model.updatedAt.getTime() | ||
11 | |||
12 | return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval | ||
13 | } | ||
14 | |||
15 | function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) { | ||
16 | if (nullable && (value === null || value === undefined)) return | ||
17 | |||
18 | if (validator(value) === false) { | ||
19 | throw new Error(`"${value}" is not a valid ${fieldName}.`) | ||
20 | } | ||
21 | } | ||
22 | |||
23 | function buildTrigramSearchIndex (indexName: string, attribute: string) { | ||
24 | return { | ||
25 | name: indexName, | ||
26 | // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function | ||
27 | fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ], | ||
28 | using: 'gin', | ||
29 | operator: 'gin_trgm_ops' | ||
30 | } | ||
31 | } | ||
32 | |||
33 | // --------------------------------------------------------------------------- | ||
34 | |||
35 | export { | ||
36 | throwIfNotValid, | ||
37 | buildTrigramSearchIndex, | ||
38 | isOutdated | ||
39 | } | ||
diff --git a/server/models/shared/sort.ts b/server/models/shared/sort.ts new file mode 100644 index 000000000..d923072f2 --- /dev/null +++ b/server/models/shared/sort.ts | |||
@@ -0,0 +1,146 @@ | |||
1 | import { literal, OrderItem, Sequelize } from 'sequelize' | ||
2 | |||
3 | // Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] | ||
4 | function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
5 | const { direction, field } = buildSortDirectionAndField(value) | ||
6 | |||
7 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
8 | |||
9 | if (field.toLowerCase() === 'match') { // Search | ||
10 | finalField = Sequelize.col('similarity') | ||
11 | } else { | ||
12 | finalField = field | ||
13 | } | ||
14 | |||
15 | return [ [ finalField, direction ], lastSort ] | ||
16 | } | ||
17 | |||
18 | function getAdminUsersSort (value: string): OrderItem[] { | ||
19 | const { direction, field } = buildSortDirectionAndField(value) | ||
20 | |||
21 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
22 | |||
23 | if (field === 'videoQuotaUsed') { // Users list | ||
24 | finalField = Sequelize.col('videoQuotaUsed') | ||
25 | } else { | ||
26 | finalField = field | ||
27 | } | ||
28 | |||
29 | const nullPolicy = direction === 'ASC' | ||
30 | ? 'NULLS FIRST' | ||
31 | : 'NULLS LAST' | ||
32 | |||
33 | // FIXME: typings | ||
34 | return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ] | ||
35 | } | ||
36 | |||
37 | function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
38 | const { direction, field } = buildSortDirectionAndField(value) | ||
39 | |||
40 | if (field.toLowerCase() === 'name') { | ||
41 | return [ [ 'displayName', direction ], lastSort ] | ||
42 | } | ||
43 | |||
44 | return getSort(value, lastSort) | ||
45 | } | ||
46 | |||
47 | function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
48 | const { direction, field } = buildSortDirectionAndField(value) | ||
49 | |||
50 | if (field.toLowerCase() === 'trending') { // Sort by aggregation | ||
51 | return [ | ||
52 | [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ], | ||
53 | |||
54 | [ Sequelize.col('VideoModel.views'), direction ], | ||
55 | |||
56 | lastSort | ||
57 | ] | ||
58 | } else if (field === 'publishedAt') { | ||
59 | return [ | ||
60 | [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], | ||
61 | |||
62 | [ Sequelize.col('VideoModel.publishedAt'), direction ], | ||
63 | |||
64 | lastSort | ||
65 | ] | ||
66 | } | ||
67 | |||
68 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
69 | |||
70 | // Alias | ||
71 | if (field.toLowerCase() === 'match') { // Search | ||
72 | finalField = Sequelize.col('similarity') | ||
73 | } else { | ||
74 | finalField = field | ||
75 | } | ||
76 | |||
77 | const firstSort: OrderItem = typeof finalField === 'string' | ||
78 | ? finalField.split('.').concat([ direction ]) as OrderItem | ||
79 | : [ finalField, direction ] | ||
80 | |||
81 | return [ firstSort, lastSort ] | ||
82 | } | ||
83 | |||
84 | function getBlacklistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
85 | const { direction, field } = buildSortDirectionAndField(value) | ||
86 | |||
87 | const videoFields = new Set([ 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid' ]) | ||
88 | |||
89 | if (videoFields.has(field)) { | ||
90 | return [ | ||
91 | [ literal(`"Video.${field}" ${direction}`) ], | ||
92 | lastSort | ||
93 | ] as OrderItem[] | ||
94 | } | ||
95 | |||
96 | return getSort(value, lastSort) | ||
97 | } | ||
98 | |||
99 | function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
100 | const { direction, field } = buildSortDirectionAndField(value) | ||
101 | |||
102 | if (field === 'redundancyAllowed') { | ||
103 | return [ | ||
104 | [ 'ActorFollowing.Server.redundancyAllowed', direction ], | ||
105 | lastSort | ||
106 | ] | ||
107 | } | ||
108 | |||
109 | return getSort(value, lastSort) | ||
110 | } | ||
111 | |||
112 | function getChannelSyncSort (value: string): OrderItem[] { | ||
113 | const { direction, field } = buildSortDirectionAndField(value) | ||
114 | if (field.toLowerCase() === 'videochannel') { | ||
115 | return [ | ||
116 | [ literal('"VideoChannel.name"'), direction ] | ||
117 | ] | ||
118 | } | ||
119 | return [ [ field, direction ] ] | ||
120 | } | ||
121 | |||
122 | function buildSortDirectionAndField (value: string) { | ||
123 | let field: string | ||
124 | let direction: 'ASC' | 'DESC' | ||
125 | |||
126 | if (value.substring(0, 1) === '-') { | ||
127 | direction = 'DESC' | ||
128 | field = value.substring(1) | ||
129 | } else { | ||
130 | direction = 'ASC' | ||
131 | field = value | ||
132 | } | ||
133 | |||
134 | return { direction, field } | ||
135 | } | ||
136 | |||
137 | export { | ||
138 | buildSortDirectionAndField, | ||
139 | getPlaylistSort, | ||
140 | getSort, | ||
141 | getAdminUsersSort, | ||
142 | getVideoSort, | ||
143 | getBlacklistSort, | ||
144 | getChannelSyncSort, | ||
145 | getInstanceFollowsSort | ||
146 | } | ||
diff --git a/server/models/shared/sql.ts b/server/models/shared/sql.ts new file mode 100644 index 000000000..5aaeb49f0 --- /dev/null +++ b/server/models/shared/sql.ts | |||
@@ -0,0 +1,68 @@ | |||
1 | import { literal, Model, ModelStatic } from 'sequelize' | ||
2 | import { forceNumber } from '@shared/core-utils' | ||
3 | import { AttributesOnly } from '@shared/typescript-utils' | ||
4 | |||
5 | function buildLocalAccountIdsIn () { | ||
6 | return literal( | ||
7 | '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)' | ||
8 | ) | ||
9 | } | ||
10 | |||
11 | function buildLocalActorIdsIn () { | ||
12 | return literal( | ||
13 | '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)' | ||
14 | ) | ||
15 | } | ||
16 | |||
17 | function buildBlockedAccountSQL (blockerIds: number[]) { | ||
18 | const blockerIdsString = blockerIds.join(', ') | ||
19 | |||
20 | return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + | ||
21 | ' UNION ' + | ||
22 | 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + | ||
23 | 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + | ||
24 | 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' | ||
25 | } | ||
26 | |||
27 | function buildServerIdsFollowedBy (actorId: any) { | ||
28 | const actorIdNumber = forceNumber(actorId) | ||
29 | |||
30 | return '(' + | ||
31 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + | ||
32 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + | ||
33 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | ||
34 | ')' | ||
35 | } | ||
36 | |||
37 | function buildSQLAttributes<M extends Model> (options: { | ||
38 | model: ModelStatic<M> | ||
39 | tableName: string | ||
40 | |||
41 | excludeAttributes?: Exclude<keyof AttributesOnly<M>, symbol>[] | ||
42 | aliasPrefix?: string | ||
43 | }) { | ||
44 | const { model, tableName, aliasPrefix, excludeAttributes } = options | ||
45 | |||
46 | const attributes = Object.keys(model.getAttributes()) as Exclude<keyof AttributesOnly<M>, symbol>[] | ||
47 | |||
48 | return attributes | ||
49 | .filter(a => { | ||
50 | if (!excludeAttributes) return true | ||
51 | if (excludeAttributes.includes(a)) return false | ||
52 | |||
53 | return true | ||
54 | }) | ||
55 | .map(a => { | ||
56 | return `"${tableName}"."${a}" AS "${aliasPrefix || ''}${a}"` | ||
57 | }) | ||
58 | } | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | export { | ||
63 | buildSQLAttributes, | ||
64 | buildBlockedAccountSQL, | ||
65 | buildServerIdsFollowedBy, | ||
66 | buildLocalAccountIdsIn, | ||
67 | buildLocalActorIdsIn | ||
68 | } | ||
diff --git a/server/models/shared/update.ts b/server/models/shared/update.ts index d338211e3..d02c4535d 100644 --- a/server/models/shared/update.ts +++ b/server/models/shared/update.ts | |||
@@ -1,9 +1,15 @@ | |||
1 | import { QueryTypes, Transaction } from 'sequelize' | 1 | import { QueryTypes, Sequelize, Transaction } from 'sequelize' |
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
3 | 2 | ||
4 | // Sequelize always skip the update if we only update updatedAt field | 3 | // Sequelize always skip the update if we only update updatedAt field |
5 | function setAsUpdated (table: string, id: number, transaction?: Transaction) { | 4 | function setAsUpdated (options: { |
6 | return sequelizeTypescript.query( | 5 | sequelize: Sequelize |
6 | table: string | ||
7 | id: number | ||
8 | transaction?: Transaction | ||
9 | }) { | ||
10 | const { sequelize, table, id, transaction } = options | ||
11 | |||
12 | return sequelize.query( | ||
7 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, | 13 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, |
8 | { | 14 | { |
9 | replacements: { table, id, updatedAt: new Date() }, | 15 | replacements: { table, id, updatedAt: new Date() }, |
diff --git a/server/models/user/sql/user-notitication-list-query-builder.ts b/server/models/user/sql/user-notitication-list-query-builder.ts index 31b4932bf..7b29807a3 100644 --- a/server/models/user/sql/user-notitication-list-query-builder.ts +++ b/server/models/user/sql/user-notitication-list-query-builder.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' | 2 | import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' |
3 | import { getSort } from '@server/models/utils' | ||
4 | import { UserNotificationModelForApi } from '@server/types/models' | 3 | import { UserNotificationModelForApi } from '@server/types/models' |
5 | import { ActorImageType } from '@shared/models' | 4 | import { ActorImageType } from '@shared/models' |
5 | import { getSort } from '../../shared' | ||
6 | 6 | ||
7 | export interface ListNotificationsOptions { | 7 | export interface ListNotificationsOptions { |
8 | userId: number | 8 | userId: number |
@@ -180,7 +180,9 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery { | |||
180 | "Account->Actor->Avatars"."type" AS "Account.Actor.Avatars.type", | 180 | "Account->Actor->Avatars"."type" AS "Account.Actor.Avatars.type", |
181 | "Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename", | 181 | "Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename", |
182 | "Account->Actor->Server"."id" AS "Account.Actor.Server.id", | 182 | "Account->Actor->Server"."id" AS "Account.Actor.Server.id", |
183 | "Account->Actor->Server"."host" AS "Account.Actor.Server.host"` | 183 | "Account->Actor->Server"."host" AS "Account.Actor.Server.host", |
184 | "UserRegistration"."id" AS "UserRegistration.id", | ||
185 | "UserRegistration"."username" AS "UserRegistration.username"` | ||
184 | } | 186 | } |
185 | 187 | ||
186 | private getJoins () { | 188 | private getJoins () { |
@@ -196,74 +198,76 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery { | |||
196 | ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id" | 198 | ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id" |
197 | ) ON "UserNotificationModel"."videoId" = "Video"."id" | 199 | ) ON "UserNotificationModel"."videoId" = "Video"."id" |
198 | 200 | ||
199 | LEFT JOIN ( | 201 | LEFT JOIN ( |
200 | "videoComment" AS "VideoComment" | 202 | "videoComment" AS "VideoComment" |
201 | INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id" | 203 | INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id" |
202 | INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id" | 204 | INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id" |
203 | LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars" | 205 | LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars" |
204 | ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId" | 206 | ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId" |
205 | AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | 207 | AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} |
206 | LEFT JOIN "server" AS "VideoComment->Account->Actor->Server" | 208 | LEFT JOIN "server" AS "VideoComment->Account->Actor->Server" |
207 | ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id" | 209 | ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id" |
208 | INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id" | 210 | INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id" |
209 | ) ON "UserNotificationModel"."commentId" = "VideoComment"."id" | 211 | ) ON "UserNotificationModel"."commentId" = "VideoComment"."id" |
212 | |||
213 | LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id" | ||
214 | LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId" | ||
215 | LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id" | ||
216 | LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId" | ||
217 | LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment" | ||
218 | ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id" | ||
219 | LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video" | ||
220 | ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id" | ||
221 | LEFT JOIN ( | ||
222 | "account" AS "Abuse->FlaggedAccount" | ||
223 | INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id" | ||
224 | LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars" | ||
225 | ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId" | ||
226 | AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
227 | LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server" | ||
228 | ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id" | ||
229 | ) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id" | ||
210 | 230 | ||
211 | LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id" | 231 | LEFT JOIN ( |
212 | LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId" | 232 | "videoBlacklist" AS "VideoBlacklist" |
213 | LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id" | 233 | INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id" |
214 | LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId" | 234 | ) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id" |
215 | LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment" | ||
216 | ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id" | ||
217 | LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video" | ||
218 | ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id" | ||
219 | LEFT JOIN ( | ||
220 | "account" AS "Abuse->FlaggedAccount" | ||
221 | INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id" | ||
222 | LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars" | ||
223 | ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId" | ||
224 | AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
225 | LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server" | ||
226 | ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id" | ||
227 | ) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id" | ||
228 | 235 | ||
229 | LEFT JOIN ( | 236 | LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id" |
230 | "videoBlacklist" AS "VideoBlacklist" | 237 | LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id" |
231 | INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id" | ||
232 | ) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id" | ||
233 | 238 | ||
234 | LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id" | 239 | LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id" |
235 | LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id" | ||
236 | 240 | ||
237 | LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id" | 241 | LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id" |
238 | 242 | ||
239 | LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id" | 243 | LEFT JOIN ( |
244 | "actorFollow" AS "ActorFollow" | ||
245 | INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id" | ||
246 | INNER JOIN "account" AS "ActorFollow->ActorFollower->Account" | ||
247 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId" | ||
248 | LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars" | ||
249 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId" | ||
250 | AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR} | ||
251 | LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server" | ||
252 | ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id" | ||
253 | INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id" | ||
254 | LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel" | ||
255 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId" | ||
256 | LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account" | ||
257 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId" | ||
258 | LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server" | ||
259 | ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id" | ||
260 | ) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id" | ||
240 | 261 | ||
241 | LEFT JOIN ( | 262 | LEFT JOIN ( |
242 | "actorFollow" AS "ActorFollow" | 263 | "account" AS "Account" |
243 | INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id" | 264 | INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id" |
244 | INNER JOIN "account" AS "ActorFollow->ActorFollower->Account" | 265 | LEFT JOIN "actorImage" AS "Account->Actor->Avatars" |
245 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId" | 266 | ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId" |
246 | LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars" | 267 | AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} |
247 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId" | 268 | LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" |
248 | AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR} | 269 | ) ON "UserNotificationModel"."accountId" = "Account"."id" |
249 | LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server" | ||
250 | ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id" | ||
251 | INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id" | ||
252 | LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel" | ||
253 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId" | ||
254 | LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account" | ||
255 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId" | ||
256 | LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server" | ||
257 | ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id" | ||
258 | ) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id" | ||
259 | 270 | ||
260 | LEFT JOIN ( | 271 | LEFT JOIN "userRegistration" as "UserRegistration" ON "UserNotificationModel"."userRegistrationId" = "UserRegistration"."id"` |
261 | "account" AS "Account" | ||
262 | INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id" | ||
263 | LEFT JOIN "actorImage" AS "Account->Actor->Avatars" | ||
264 | ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId" | ||
265 | AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
266 | LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" | ||
267 | ) ON "UserNotificationModel"."accountId" = "Account"."id"` | ||
268 | } | 272 | } |
269 | } | 273 | } |
diff --git a/server/models/user/user-notification-setting.ts b/server/models/user/user-notification-setting.ts index 66e1d85b3..394494c0c 100644 --- a/server/models/user/user-notification-setting.ts +++ b/server/models/user/user-notification-setting.ts | |||
@@ -17,7 +17,7 @@ import { MNotificationSettingFormattable } from '@server/types/models' | |||
17 | import { AttributesOnly } from '@shared/typescript-utils' | 17 | import { AttributesOnly } from '@shared/typescript-utils' |
18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
19 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | 19 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' |
20 | import { throwIfNotValid } from '../utils' | 20 | import { throwIfNotValid } from '../shared' |
21 | import { UserModel } from './user' | 21 | import { UserModel } from './user' |
22 | 22 | ||
23 | @Table({ | 23 | @Table({ |
diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts index d37fa5dc7..667ee7f5f 100644 --- a/server/models/user/user-notification.ts +++ b/server/models/user/user-notification.ts | |||
@@ -13,13 +13,14 @@ import { AccountModel } from '../account/account' | |||
13 | import { ActorFollowModel } from '../actor/actor-follow' | 13 | import { ActorFollowModel } from '../actor/actor-follow' |
14 | import { ApplicationModel } from '../application/application' | 14 | import { ApplicationModel } from '../application/application' |
15 | import { PluginModel } from '../server/plugin' | 15 | import { PluginModel } from '../server/plugin' |
16 | import { throwIfNotValid } from '../utils' | 16 | import { throwIfNotValid } from '../shared' |
17 | import { VideoModel } from '../video/video' | 17 | import { VideoModel } from '../video/video' |
18 | import { VideoBlacklistModel } from '../video/video-blacklist' | 18 | import { VideoBlacklistModel } from '../video/video-blacklist' |
19 | import { VideoCommentModel } from '../video/video-comment' | 19 | import { VideoCommentModel } from '../video/video-comment' |
20 | import { VideoImportModel } from '../video/video-import' | 20 | import { VideoImportModel } from '../video/video-import' |
21 | import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder' | 21 | import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder' |
22 | import { UserModel } from './user' | 22 | import { UserModel } from './user' |
23 | import { UserRegistrationModel } from './user-registration' | ||
23 | 24 | ||
24 | @Table({ | 25 | @Table({ |
25 | tableName: 'userNotification', | 26 | tableName: 'userNotification', |
@@ -98,6 +99,14 @@ import { UserModel } from './user' | |||
98 | [Op.ne]: null | 99 | [Op.ne]: null |
99 | } | 100 | } |
100 | } | 101 | } |
102 | }, | ||
103 | { | ||
104 | fields: [ 'userRegistrationId' ], | ||
105 | where: { | ||
106 | userRegistrationId: { | ||
107 | [Op.ne]: null | ||
108 | } | ||
109 | } | ||
101 | } | 110 | } |
102 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] | 111 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] |
103 | }) | 112 | }) |
@@ -241,6 +250,18 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
241 | }) | 250 | }) |
242 | Application: ApplicationModel | 251 | Application: ApplicationModel |
243 | 252 | ||
253 | @ForeignKey(() => UserRegistrationModel) | ||
254 | @Column | ||
255 | userRegistrationId: number | ||
256 | |||
257 | @BelongsTo(() => UserRegistrationModel, { | ||
258 | foreignKey: { | ||
259 | allowNull: true | ||
260 | }, | ||
261 | onDelete: 'cascade' | ||
262 | }) | ||
263 | UserRegistration: UserRegistrationModel | ||
264 | |||
244 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { | 265 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { |
245 | const where = { userId } | 266 | const where = { userId } |
246 | 267 | ||
@@ -416,6 +437,10 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
416 | ? { latestVersion: this.Application.latestPeerTubeVersion } | 437 | ? { latestVersion: this.Application.latestPeerTubeVersion } |
417 | : undefined | 438 | : undefined |
418 | 439 | ||
440 | const registration = this.UserRegistration | ||
441 | ? { id: this.UserRegistration.id, username: this.UserRegistration.username } | ||
442 | : undefined | ||
443 | |||
419 | return { | 444 | return { |
420 | id: this.id, | 445 | id: this.id, |
421 | type: this.type, | 446 | type: this.type, |
@@ -429,6 +454,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
429 | actorFollow, | 454 | actorFollow, |
430 | plugin, | 455 | plugin, |
431 | peertube, | 456 | peertube, |
457 | registration, | ||
432 | createdAt: this.createdAt.toISOString(), | 458 | createdAt: this.createdAt.toISOString(), |
433 | updatedAt: this.updatedAt.toISOString() | 459 | updatedAt: this.updatedAt.toISOString() |
434 | } | 460 | } |
diff --git a/server/models/user/user-registration.ts b/server/models/user/user-registration.ts new file mode 100644 index 000000000..adda3cc7e --- /dev/null +++ b/server/models/user/user-registration.ts | |||
@@ -0,0 +1,259 @@ | |||
1 | import { FindOptions, Op, WhereOptions } from 'sequelize' | ||
2 | import { | ||
3 | AllowNull, | ||
4 | BeforeCreate, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | IsEmail, | ||
12 | Model, | ||
13 | Table, | ||
14 | UpdatedAt | ||
15 | } from 'sequelize-typescript' | ||
16 | import { | ||
17 | isRegistrationModerationResponseValid, | ||
18 | isRegistrationReasonValid, | ||
19 | isRegistrationStateValid | ||
20 | } from '@server/helpers/custom-validators/user-registration' | ||
21 | import { isVideoChannelDisplayNameValid } from '@server/helpers/custom-validators/video-channels' | ||
22 | import { cryptPassword } from '@server/helpers/peertube-crypto' | ||
23 | import { USER_REGISTRATION_STATES } from '@server/initializers/constants' | ||
24 | import { MRegistration, MRegistrationFormattable } from '@server/types/models' | ||
25 | import { UserRegistration, UserRegistrationState } from '@shared/models' | ||
26 | import { AttributesOnly } from '@shared/typescript-utils' | ||
27 | import { isUserDisplayNameValid, isUserEmailVerifiedValid, isUserPasswordValid } from '../../helpers/custom-validators/users' | ||
28 | import { getSort, throwIfNotValid } from '../shared' | ||
29 | import { UserModel } from './user' | ||
30 | |||
31 | @Table({ | ||
32 | tableName: 'userRegistration', | ||
33 | indexes: [ | ||
34 | { | ||
35 | fields: [ 'username' ], | ||
36 | unique: true | ||
37 | }, | ||
38 | { | ||
39 | fields: [ 'email' ], | ||
40 | unique: true | ||
41 | }, | ||
42 | { | ||
43 | fields: [ 'channelHandle' ], | ||
44 | unique: true | ||
45 | }, | ||
46 | { | ||
47 | fields: [ 'userId' ], | ||
48 | unique: true | ||
49 | } | ||
50 | ] | ||
51 | }) | ||
52 | export class UserRegistrationModel extends Model<Partial<AttributesOnly<UserRegistrationModel>>> { | ||
53 | |||
54 | @AllowNull(false) | ||
55 | @Is('RegistrationState', value => throwIfNotValid(value, isRegistrationStateValid, 'state')) | ||
56 | @Column | ||
57 | state: UserRegistrationState | ||
58 | |||
59 | @AllowNull(false) | ||
60 | @Is('RegistrationReason', value => throwIfNotValid(value, isRegistrationReasonValid, 'registration reason')) | ||
61 | @Column(DataType.TEXT) | ||
62 | registrationReason: string | ||
63 | |||
64 | @AllowNull(true) | ||
65 | @Is('RegistrationModerationResponse', value => throwIfNotValid(value, isRegistrationModerationResponseValid, 'moderation response', true)) | ||
66 | @Column(DataType.TEXT) | ||
67 | moderationResponse: string | ||
68 | |||
69 | @AllowNull(true) | ||
70 | @Is('RegistrationPassword', value => throwIfNotValid(value, isUserPasswordValid, 'registration password', true)) | ||
71 | @Column | ||
72 | password: string | ||
73 | |||
74 | @AllowNull(false) | ||
75 | @Column | ||
76 | username: string | ||
77 | |||
78 | @AllowNull(false) | ||
79 | @IsEmail | ||
80 | @Column(DataType.STRING(400)) | ||
81 | email: string | ||
82 | |||
83 | @AllowNull(true) | ||
84 | @Is('RegistrationEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true)) | ||
85 | @Column | ||
86 | emailVerified: boolean | ||
87 | |||
88 | @AllowNull(true) | ||
89 | @Is('RegistrationAccountDisplayName', value => throwIfNotValid(value, isUserDisplayNameValid, 'account display name', true)) | ||
90 | @Column | ||
91 | accountDisplayName: string | ||
92 | |||
93 | @AllowNull(true) | ||
94 | @Is('ChannelHandle', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'channel handle', true)) | ||
95 | @Column | ||
96 | channelHandle: string | ||
97 | |||
98 | @AllowNull(true) | ||
99 | @Is('ChannelDisplayName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'channel display name', true)) | ||
100 | @Column | ||
101 | channelDisplayName: string | ||
102 | |||
103 | @CreatedAt | ||
104 | createdAt: Date | ||
105 | |||
106 | @UpdatedAt | ||
107 | updatedAt: Date | ||
108 | |||
109 | @ForeignKey(() => UserModel) | ||
110 | @Column | ||
111 | userId: number | ||
112 | |||
113 | @BelongsTo(() => UserModel, { | ||
114 | foreignKey: { | ||
115 | allowNull: true | ||
116 | }, | ||
117 | onDelete: 'SET NULL' | ||
118 | }) | ||
119 | User: UserModel | ||
120 | |||
121 | @BeforeCreate | ||
122 | static async cryptPasswordIfNeeded (instance: UserRegistrationModel) { | ||
123 | instance.password = await cryptPassword(instance.password) | ||
124 | } | ||
125 | |||
126 | static load (id: number): Promise<MRegistration> { | ||
127 | return UserRegistrationModel.findByPk(id) | ||
128 | } | ||
129 | |||
130 | static loadByEmail (email: string): Promise<MRegistration> { | ||
131 | const query = { | ||
132 | where: { email } | ||
133 | } | ||
134 | |||
135 | return UserRegistrationModel.findOne(query) | ||
136 | } | ||
137 | |||
138 | static loadByEmailOrUsername (emailOrUsername: string): Promise<MRegistration> { | ||
139 | const query = { | ||
140 | where: { | ||
141 | [Op.or]: [ | ||
142 | { email: emailOrUsername }, | ||
143 | { username: emailOrUsername } | ||
144 | ] | ||
145 | } | ||
146 | } | ||
147 | |||
148 | return UserRegistrationModel.findOne(query) | ||
149 | } | ||
150 | |||
151 | static loadByEmailOrHandle (options: { | ||
152 | email: string | ||
153 | username: string | ||
154 | channelHandle?: string | ||
155 | }): Promise<MRegistration> { | ||
156 | const { email, username, channelHandle } = options | ||
157 | |||
158 | let or: WhereOptions = [ | ||
159 | { email }, | ||
160 | { channelHandle: username }, | ||
161 | { username } | ||
162 | ] | ||
163 | |||
164 | if (channelHandle) { | ||
165 | or = or.concat([ | ||
166 | { username: channelHandle }, | ||
167 | { channelHandle } | ||
168 | ]) | ||
169 | } | ||
170 | |||
171 | const query = { | ||
172 | where: { | ||
173 | [Op.or]: or | ||
174 | } | ||
175 | } | ||
176 | |||
177 | return UserRegistrationModel.findOne(query) | ||
178 | } | ||
179 | |||
180 | // --------------------------------------------------------------------------- | ||
181 | |||
182 | static listForApi (options: { | ||
183 | start: number | ||
184 | count: number | ||
185 | sort: string | ||
186 | search?: string | ||
187 | }) { | ||
188 | const { start, count, sort, search } = options | ||
189 | |||
190 | const where: WhereOptions = {} | ||
191 | |||
192 | if (search) { | ||
193 | Object.assign(where, { | ||
194 | [Op.or]: [ | ||
195 | { | ||
196 | email: { | ||
197 | [Op.iLike]: '%' + search + '%' | ||
198 | } | ||
199 | }, | ||
200 | { | ||
201 | username: { | ||
202 | [Op.iLike]: '%' + search + '%' | ||
203 | } | ||
204 | } | ||
205 | ] | ||
206 | }) | ||
207 | } | ||
208 | |||
209 | const query: FindOptions = { | ||
210 | offset: start, | ||
211 | limit: count, | ||
212 | order: getSort(sort), | ||
213 | where, | ||
214 | include: [ | ||
215 | { | ||
216 | model: UserModel.unscoped(), | ||
217 | required: false | ||
218 | } | ||
219 | ] | ||
220 | } | ||
221 | |||
222 | return Promise.all([ | ||
223 | UserRegistrationModel.count(query), | ||
224 | UserRegistrationModel.findAll<MRegistrationFormattable>(query) | ||
225 | ]).then(([ total, data ]) => ({ total, data })) | ||
226 | } | ||
227 | |||
228 | // --------------------------------------------------------------------------- | ||
229 | |||
230 | toFormattedJSON (this: MRegistrationFormattable): UserRegistration { | ||
231 | return { | ||
232 | id: this.id, | ||
233 | |||
234 | state: { | ||
235 | id: this.state, | ||
236 | label: USER_REGISTRATION_STATES[this.state] | ||
237 | }, | ||
238 | |||
239 | registrationReason: this.registrationReason, | ||
240 | moderationResponse: this.moderationResponse, | ||
241 | |||
242 | username: this.username, | ||
243 | email: this.email, | ||
244 | emailVerified: this.emailVerified, | ||
245 | |||
246 | accountDisplayName: this.accountDisplayName, | ||
247 | |||
248 | channelHandle: this.channelHandle, | ||
249 | channelDisplayName: this.channelDisplayName, | ||
250 | |||
251 | createdAt: this.createdAt, | ||
252 | updatedAt: this.updatedAt, | ||
253 | |||
254 | user: this.User | ||
255 | ? { id: this.User.id } | ||
256 | : null | ||
257 | } | ||
258 | } | ||
259 | } | ||
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 3fd808edc..bfc9b3049 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -30,6 +30,7 @@ import { | |||
30 | MUserNotifSettingChannelDefault, | 30 | MUserNotifSettingChannelDefault, |
31 | MUserWithNotificationSetting | 31 | MUserWithNotificationSetting |
32 | } from '@server/types/models' | 32 | } from '@server/types/models' |
33 | import { forceNumber } from '@shared/core-utils' | ||
33 | import { AttributesOnly } from '@shared/typescript-utils' | 34 | import { AttributesOnly } from '@shared/typescript-utils' |
34 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' | 35 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' |
35 | import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models' | 36 | import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models' |
@@ -63,14 +64,13 @@ import { ActorModel } from '../actor/actor' | |||
63 | import { ActorFollowModel } from '../actor/actor-follow' | 64 | import { ActorFollowModel } from '../actor/actor-follow' |
64 | import { ActorImageModel } from '../actor/actor-image' | 65 | import { ActorImageModel } from '../actor/actor-image' |
65 | import { OAuthTokenModel } from '../oauth/oauth-token' | 66 | import { OAuthTokenModel } from '../oauth/oauth-token' |
66 | import { getAdminUsersSort, throwIfNotValid } from '../utils' | 67 | import { getAdminUsersSort, throwIfNotValid } from '../shared' |
67 | import { VideoModel } from '../video/video' | 68 | import { VideoModel } from '../video/video' |
68 | import { VideoChannelModel } from '../video/video-channel' | 69 | import { VideoChannelModel } from '../video/video-channel' |
69 | import { VideoImportModel } from '../video/video-import' | 70 | import { VideoImportModel } from '../video/video-import' |
70 | import { VideoLiveModel } from '../video/video-live' | 71 | import { VideoLiveModel } from '../video/video-live' |
71 | import { VideoPlaylistModel } from '../video/video-playlist' | 72 | import { VideoPlaylistModel } from '../video/video-playlist' |
72 | import { UserNotificationSettingModel } from './user-notification-setting' | 73 | import { UserNotificationSettingModel } from './user-notification-setting' |
73 | import { forceNumber } from '@shared/core-utils' | ||
74 | 74 | ||
75 | enum ScopeNames { | 75 | enum ScopeNames { |
76 | FOR_ME_API = 'FOR_ME_API', | 76 | FOR_ME_API = 'FOR_ME_API', |
@@ -441,16 +441,17 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
441 | }) | 441 | }) |
442 | OAuthTokens: OAuthTokenModel[] | 442 | OAuthTokens: OAuthTokenModel[] |
443 | 443 | ||
444 | // Used if we already set an encrypted password in user model | ||
445 | skipPasswordEncryption = false | ||
446 | |||
444 | @BeforeCreate | 447 | @BeforeCreate |
445 | @BeforeUpdate | 448 | @BeforeUpdate |
446 | static cryptPasswordIfNeeded (instance: UserModel) { | 449 | static async cryptPasswordIfNeeded (instance: UserModel) { |
447 | if (instance.changed('password') && instance.password) { | 450 | if (instance.skipPasswordEncryption) return |
448 | return cryptPassword(instance.password) | 451 | if (!instance.changed('password')) return |
449 | .then(hash => { | 452 | if (!instance.password) return |
450 | instance.password = hash | 453 | |
451 | return undefined | 454 | instance.password = await cryptPassword(instance.password) |
452 | }) | ||
453 | } | ||
454 | } | 455 | } |
455 | 456 | ||
456 | @AfterUpdate | 457 | @AfterUpdate |
diff --git a/server/models/utils.ts b/server/models/utils.ts deleted file mode 100644 index 3476799ce..000000000 --- a/server/models/utils.ts +++ /dev/null | |||
@@ -1,317 +0,0 @@ | |||
1 | import { literal, Op, OrderItem, Sequelize } from 'sequelize' | ||
2 | import validator from 'validator' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | |||
5 | type SortType = { sortModel: string, sortValue: string } | ||
6 | |||
7 | // Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] | ||
8 | function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
9 | const { direction, field } = buildDirectionAndField(value) | ||
10 | |||
11 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
12 | |||
13 | if (field.toLowerCase() === 'match') { // Search | ||
14 | finalField = Sequelize.col('similarity') | ||
15 | } else { | ||
16 | finalField = field | ||
17 | } | ||
18 | |||
19 | return [ [ finalField, direction ], lastSort ] | ||
20 | } | ||
21 | |||
22 | function getAdminUsersSort (value: string): OrderItem[] { | ||
23 | const { direction, field } = buildDirectionAndField(value) | ||
24 | |||
25 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
26 | |||
27 | if (field === 'videoQuotaUsed') { // Users list | ||
28 | finalField = Sequelize.col('videoQuotaUsed') | ||
29 | } else { | ||
30 | finalField = field | ||
31 | } | ||
32 | |||
33 | const nullPolicy = direction === 'ASC' | ||
34 | ? 'NULLS FIRST' | ||
35 | : 'NULLS LAST' | ||
36 | |||
37 | // FIXME: typings | ||
38 | return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ] | ||
39 | } | ||
40 | |||
41 | function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
42 | const { direction, field } = buildDirectionAndField(value) | ||
43 | |||
44 | if (field.toLowerCase() === 'name') { | ||
45 | return [ [ 'displayName', direction ], lastSort ] | ||
46 | } | ||
47 | |||
48 | return getSort(value, lastSort) | ||
49 | } | ||
50 | |||
51 | function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
52 | const { direction, field } = buildDirectionAndField(value) | ||
53 | |||
54 | if (field === 'totalReplies') { | ||
55 | return [ | ||
56 | [ Sequelize.literal('"totalReplies"'), direction ], | ||
57 | lastSort | ||
58 | ] | ||
59 | } | ||
60 | |||
61 | return getSort(value, lastSort) | ||
62 | } | ||
63 | |||
64 | function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
65 | const { direction, field } = buildDirectionAndField(value) | ||
66 | |||
67 | if (field.toLowerCase() === 'trending') { // Sort by aggregation | ||
68 | return [ | ||
69 | [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ], | ||
70 | |||
71 | [ Sequelize.col('VideoModel.views'), direction ], | ||
72 | |||
73 | lastSort | ||
74 | ] | ||
75 | } else if (field === 'publishedAt') { | ||
76 | return [ | ||
77 | [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], | ||
78 | |||
79 | [ Sequelize.col('VideoModel.publishedAt'), direction ], | ||
80 | |||
81 | lastSort | ||
82 | ] | ||
83 | } | ||
84 | |||
85 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
86 | |||
87 | // Alias | ||
88 | if (field.toLowerCase() === 'match') { // Search | ||
89 | finalField = Sequelize.col('similarity') | ||
90 | } else { | ||
91 | finalField = field | ||
92 | } | ||
93 | |||
94 | const firstSort: OrderItem = typeof finalField === 'string' | ||
95 | ? finalField.split('.').concat([ direction ]) as OrderItem | ||
96 | : [ finalField, direction ] | ||
97 | |||
98 | return [ firstSort, lastSort ] | ||
99 | } | ||
100 | |||
101 | function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
102 | const [ firstSort ] = getSort(value) | ||
103 | |||
104 | if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as OrderItem[] | ||
105 | return [ firstSort, lastSort ] | ||
106 | } | ||
107 | |||
108 | function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
109 | const { direction, field } = buildDirectionAndField(value) | ||
110 | |||
111 | if (field === 'redundancyAllowed') { | ||
112 | return [ | ||
113 | [ 'ActorFollowing.Server.redundancyAllowed', direction ], | ||
114 | lastSort | ||
115 | ] | ||
116 | } | ||
117 | |||
118 | return getSort(value, lastSort) | ||
119 | } | ||
120 | |||
121 | function getChannelSyncSort (value: string): OrderItem[] { | ||
122 | const { direction, field } = buildDirectionAndField(value) | ||
123 | if (field.toLowerCase() === 'videochannel') { | ||
124 | return [ | ||
125 | [ literal('"VideoChannel.name"'), direction ] | ||
126 | ] | ||
127 | } | ||
128 | return [ [ field, direction ] ] | ||
129 | } | ||
130 | |||
131 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { | ||
132 | if (!model.createdAt || !model.updatedAt) { | ||
133 | throw new Error('Miss createdAt & updatedAt attributes to model') | ||
134 | } | ||
135 | |||
136 | const now = Date.now() | ||
137 | const createdAtTime = model.createdAt.getTime() | ||
138 | const updatedAtTime = model.updatedAt.getTime() | ||
139 | |||
140 | return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval | ||
141 | } | ||
142 | |||
143 | function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) { | ||
144 | if (nullable && (value === null || value === undefined)) return | ||
145 | |||
146 | if (validator(value) === false) { | ||
147 | throw new Error(`"${value}" is not a valid ${fieldName}.`) | ||
148 | } | ||
149 | } | ||
150 | |||
151 | function buildTrigramSearchIndex (indexName: string, attribute: string) { | ||
152 | return { | ||
153 | name: indexName, | ||
154 | // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function | ||
155 | fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ], | ||
156 | using: 'gin', | ||
157 | operator: 'gin_trgm_ops' | ||
158 | } | ||
159 | } | ||
160 | |||
161 | function createSimilarityAttribute (col: string, value: string) { | ||
162 | return Sequelize.fn( | ||
163 | 'similarity', | ||
164 | |||
165 | searchTrigramNormalizeCol(col), | ||
166 | |||
167 | searchTrigramNormalizeValue(value) | ||
168 | ) | ||
169 | } | ||
170 | |||
171 | function buildBlockedAccountSQL (blockerIds: number[]) { | ||
172 | const blockerIdsString = blockerIds.join(', ') | ||
173 | |||
174 | return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + | ||
175 | ' UNION ' + | ||
176 | 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + | ||
177 | 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + | ||
178 | 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' | ||
179 | } | ||
180 | |||
181 | function buildBlockedAccountSQLOptimized (columnNameJoin: string, blockerIds: number[]) { | ||
182 | const blockerIdsString = blockerIds.join(', ') | ||
183 | |||
184 | return [ | ||
185 | literal( | ||
186 | `NOT EXISTS (` + | ||
187 | ` SELECT 1 FROM "accountBlocklist" ` + | ||
188 | ` WHERE "targetAccountId" = ${columnNameJoin} ` + | ||
189 | ` AND "accountId" IN (${blockerIdsString})` + | ||
190 | `)` | ||
191 | ), | ||
192 | |||
193 | literal( | ||
194 | `NOT EXISTS (` + | ||
195 | ` SELECT 1 FROM "account" ` + | ||
196 | ` INNER JOIN "actor" ON account."actorId" = actor.id ` + | ||
197 | ` INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` + | ||
198 | ` WHERE "account"."id" = ${columnNameJoin} ` + | ||
199 | ` AND "serverBlocklist"."accountId" IN (${blockerIdsString})` + | ||
200 | `)` | ||
201 | ) | ||
202 | ] | ||
203 | } | ||
204 | |||
205 | function buildServerIdsFollowedBy (actorId: any) { | ||
206 | const actorIdNumber = forceNumber(actorId) | ||
207 | |||
208 | return '(' + | ||
209 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + | ||
210 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + | ||
211 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | ||
212 | ')' | ||
213 | } | ||
214 | |||
215 | function buildWhereIdOrUUID (id: number | string) { | ||
216 | return validator.isInt('' + id) ? { id } : { uuid: id } | ||
217 | } | ||
218 | |||
219 | function parseAggregateResult (result: any) { | ||
220 | if (!result) return 0 | ||
221 | |||
222 | const total = forceNumber(result) | ||
223 | if (isNaN(total)) return 0 | ||
224 | |||
225 | return total | ||
226 | } | ||
227 | |||
228 | function parseRowCountResult (result: any) { | ||
229 | if (result.length !== 0) return result[0].total | ||
230 | |||
231 | return 0 | ||
232 | } | ||
233 | |||
234 | function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) { | ||
235 | return stringArr.map(t => { | ||
236 | return t === null | ||
237 | ? null | ||
238 | : sequelize.escape('' + t) | ||
239 | }).join(', ') | ||
240 | } | ||
241 | |||
242 | function buildLocalAccountIdsIn () { | ||
243 | return literal( | ||
244 | '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)' | ||
245 | ) | ||
246 | } | ||
247 | |||
248 | function buildLocalActorIdsIn () { | ||
249 | return literal( | ||
250 | '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)' | ||
251 | ) | ||
252 | } | ||
253 | |||
254 | function buildDirectionAndField (value: string) { | ||
255 | let field: string | ||
256 | let direction: 'ASC' | 'DESC' | ||
257 | |||
258 | if (value.substring(0, 1) === '-') { | ||
259 | direction = 'DESC' | ||
260 | field = value.substring(1) | ||
261 | } else { | ||
262 | direction = 'ASC' | ||
263 | field = value | ||
264 | } | ||
265 | |||
266 | return { direction, field } | ||
267 | } | ||
268 | |||
269 | function searchAttribute (sourceField?: string, targetField?: string) { | ||
270 | if (!sourceField) return {} | ||
271 | |||
272 | return { | ||
273 | [targetField]: { | ||
274 | // FIXME: ts error | ||
275 | [Op.iLike as any]: `%${sourceField}%` | ||
276 | } | ||
277 | } | ||
278 | } | ||
279 | |||
280 | // --------------------------------------------------------------------------- | ||
281 | |||
282 | export { | ||
283 | buildBlockedAccountSQL, | ||
284 | buildBlockedAccountSQLOptimized, | ||
285 | buildLocalActorIdsIn, | ||
286 | getPlaylistSort, | ||
287 | SortType, | ||
288 | buildLocalAccountIdsIn, | ||
289 | getSort, | ||
290 | getCommentSort, | ||
291 | getAdminUsersSort, | ||
292 | getVideoSort, | ||
293 | getBlacklistSort, | ||
294 | getChannelSyncSort, | ||
295 | createSimilarityAttribute, | ||
296 | throwIfNotValid, | ||
297 | buildServerIdsFollowedBy, | ||
298 | buildTrigramSearchIndex, | ||
299 | buildWhereIdOrUUID, | ||
300 | isOutdated, | ||
301 | parseAggregateResult, | ||
302 | getInstanceFollowsSort, | ||
303 | buildDirectionAndField, | ||
304 | createSafeIn, | ||
305 | searchAttribute, | ||
306 | parseRowCountResult | ||
307 | } | ||
308 | |||
309 | // --------------------------------------------------------------------------- | ||
310 | |||
311 | function searchTrigramNormalizeValue (value: string) { | ||
312 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value)) | ||
313 | } | ||
314 | |||
315 | function searchTrigramNormalizeCol (col: string) { | ||
316 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) | ||
317 | } | ||
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts index f285db477..6f05dbdc8 100644 --- a/server/models/video/formatter/video-format-utils.ts +++ b/server/models/video/formatter/video-format-utils.ts | |||
@@ -488,7 +488,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
488 | } | 488 | } |
489 | 489 | ||
490 | function getCategoryLabel (id: number) { | 490 | function getCategoryLabel (id: number) { |
491 | return VIDEO_CATEGORIES[id] || 'Misc' | 491 | return VIDEO_CATEGORIES[id] || 'Unknown' |
492 | } | 492 | } |
493 | 493 | ||
494 | function getLicenceLabel (id: number) { | 494 | function getLicenceLabel (id: number) { |
diff --git a/server/models/video/sql/comment/video-comment-list-query-builder.ts b/server/models/video/sql/comment/video-comment-list-query-builder.ts new file mode 100644 index 000000000..a7eed22a1 --- /dev/null +++ b/server/models/video/sql/comment/video-comment-list-query-builder.ts | |||
@@ -0,0 +1,400 @@ | |||
1 | import { Model, Sequelize, Transaction } from 'sequelize' | ||
2 | import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' | ||
3 | import { ActorImageType, VideoPrivacy } from '@shared/models' | ||
4 | import { createSafeIn, getSort, parseRowCountResult } from '../../../shared' | ||
5 | import { VideoCommentTableAttributes } from './video-comment-table-attributes' | ||
6 | |||
7 | export interface ListVideoCommentsOptions { | ||
8 | selectType: 'api' | 'feed' | 'comment-only' | ||
9 | |||
10 | start?: number | ||
11 | count?: number | ||
12 | sort?: string | ||
13 | |||
14 | videoId?: number | ||
15 | threadId?: number | ||
16 | accountId?: number | ||
17 | videoChannelId?: number | ||
18 | |||
19 | blockerAccountIds?: number[] | ||
20 | |||
21 | isThread?: boolean | ||
22 | notDeleted?: boolean | ||
23 | isLocal?: boolean | ||
24 | onLocalVideo?: boolean | ||
25 | onPublicVideo?: boolean | ||
26 | videoAccountOwnerId?: boolean | ||
27 | |||
28 | search?: string | ||
29 | searchAccount?: string | ||
30 | searchVideo?: string | ||
31 | |||
32 | includeReplyCounters?: boolean | ||
33 | |||
34 | transaction?: Transaction | ||
35 | } | ||
36 | |||
37 | export class VideoCommentListQueryBuilder extends AbstractRunQuery { | ||
38 | private readonly tableAttributes = new VideoCommentTableAttributes() | ||
39 | |||
40 | private innerQuery: string | ||
41 | |||
42 | private select = '' | ||
43 | private joins = '' | ||
44 | |||
45 | private innerSelect = '' | ||
46 | private innerJoins = '' | ||
47 | private innerLateralJoins = '' | ||
48 | private innerWhere = '' | ||
49 | |||
50 | private readonly built = { | ||
51 | cte: false, | ||
52 | accountJoin: false, | ||
53 | videoJoin: false, | ||
54 | videoChannelJoin: false, | ||
55 | avatarJoin: false | ||
56 | } | ||
57 | |||
58 | constructor ( | ||
59 | protected readonly sequelize: Sequelize, | ||
60 | private readonly options: ListVideoCommentsOptions | ||
61 | ) { | ||
62 | super(sequelize) | ||
63 | |||
64 | if (this.options.includeReplyCounters && !this.options.videoId) { | ||
65 | throw new Error('Cannot include reply counters without videoId') | ||
66 | } | ||
67 | } | ||
68 | |||
69 | async listComments <T extends Model> () { | ||
70 | this.buildListQuery() | ||
71 | |||
72 | const results = await this.runQuery({ nest: true, transaction: this.options.transaction }) | ||
73 | const modelBuilder = new ModelBuilder<T>(this.sequelize) | ||
74 | |||
75 | return modelBuilder.createModels(results, 'VideoComment') | ||
76 | } | ||
77 | |||
78 | async countComments () { | ||
79 | this.buildCountQuery() | ||
80 | |||
81 | const result = await this.runQuery({ transaction: this.options.transaction }) | ||
82 | |||
83 | return parseRowCountResult(result) | ||
84 | } | ||
85 | |||
86 | // --------------------------------------------------------------------------- | ||
87 | |||
88 | private buildListQuery () { | ||
89 | this.buildInnerListQuery() | ||
90 | this.buildListSelect() | ||
91 | |||
92 | this.query = `${this.select} ` + | ||
93 | `FROM (${this.innerQuery}) AS "VideoCommentModel" ` + | ||
94 | `${this.joins} ` + | ||
95 | `${this.getOrder()}` | ||
96 | } | ||
97 | |||
98 | private buildInnerListQuery () { | ||
99 | this.buildWhere() | ||
100 | this.buildInnerListSelect() | ||
101 | |||
102 | this.innerQuery = `${this.innerSelect} ` + | ||
103 | `FROM "videoComment" AS "VideoCommentModel" ` + | ||
104 | `${this.innerJoins} ` + | ||
105 | `${this.innerLateralJoins} ` + | ||
106 | `${this.innerWhere} ` + | ||
107 | `${this.getOrder()} ` + | ||
108 | `${this.getInnerLimit()}` | ||
109 | } | ||
110 | |||
111 | // --------------------------------------------------------------------------- | ||
112 | |||
113 | private buildCountQuery () { | ||
114 | this.buildWhere() | ||
115 | |||
116 | this.query = `SELECT COUNT(*) AS "total" ` + | ||
117 | `FROM "videoComment" AS "VideoCommentModel" ` + | ||
118 | `${this.innerJoins} ` + | ||
119 | `${this.innerWhere}` | ||
120 | } | ||
121 | |||
122 | // --------------------------------------------------------------------------- | ||
123 | |||
124 | private buildWhere () { | ||
125 | let where: string[] = [] | ||
126 | |||
127 | if (this.options.videoId) { | ||
128 | this.replacements.videoId = this.options.videoId | ||
129 | |||
130 | where.push('"VideoCommentModel"."videoId" = :videoId') | ||
131 | } | ||
132 | |||
133 | if (this.options.threadId) { | ||
134 | this.replacements.threadId = this.options.threadId | ||
135 | |||
136 | where.push('("VideoCommentModel"."id" = :threadId OR "VideoCommentModel"."originCommentId" = :threadId)') | ||
137 | } | ||
138 | |||
139 | if (this.options.accountId) { | ||
140 | this.replacements.accountId = this.options.accountId | ||
141 | |||
142 | where.push('"VideoCommentModel"."accountId" = :accountId') | ||
143 | } | ||
144 | |||
145 | if (this.options.videoChannelId) { | ||
146 | this.buildVideoChannelJoin() | ||
147 | |||
148 | this.replacements.videoChannelId = this.options.videoChannelId | ||
149 | |||
150 | where.push('"Account->VideoChannel"."id" = :videoChannelId') | ||
151 | } | ||
152 | |||
153 | if (this.options.blockerAccountIds) { | ||
154 | this.buildVideoChannelJoin() | ||
155 | |||
156 | where = where.concat(this.getBlockWhere('VideoCommentModel', 'Video->VideoChannel')) | ||
157 | } | ||
158 | |||
159 | if (this.options.isThread === true) { | ||
160 | where.push('"VideoCommentModel"."inReplyToCommentId" IS NULL') | ||
161 | } | ||
162 | |||
163 | if (this.options.notDeleted === true) { | ||
164 | where.push('"VideoCommentModel"."deletedAt" IS NULL') | ||
165 | } | ||
166 | |||
167 | if (this.options.isLocal === true) { | ||
168 | this.buildAccountJoin() | ||
169 | |||
170 | where.push('"Account->Actor"."serverId" IS NULL') | ||
171 | } else if (this.options.isLocal === false) { | ||
172 | this.buildAccountJoin() | ||
173 | |||
174 | where.push('"Account->Actor"."serverId" IS NOT NULL') | ||
175 | } | ||
176 | |||
177 | if (this.options.onLocalVideo === true) { | ||
178 | this.buildVideoJoin() | ||
179 | |||
180 | where.push('"Video"."remote" IS FALSE') | ||
181 | } else if (this.options.onLocalVideo === false) { | ||
182 | this.buildVideoJoin() | ||
183 | |||
184 | where.push('"Video"."remote" IS TRUE') | ||
185 | } | ||
186 | |||
187 | if (this.options.onPublicVideo === true) { | ||
188 | this.buildVideoJoin() | ||
189 | |||
190 | where.push(`"Video"."privacy" = ${VideoPrivacy.PUBLIC}`) | ||
191 | } | ||
192 | |||
193 | if (this.options.videoAccountOwnerId) { | ||
194 | this.buildVideoChannelJoin() | ||
195 | |||
196 | this.replacements.videoAccountOwnerId = this.options.videoAccountOwnerId | ||
197 | |||
198 | where.push(`"Video->VideoChannel"."accountId" = :videoAccountOwnerId`) | ||
199 | } | ||
200 | |||
201 | if (this.options.search) { | ||
202 | this.buildVideoJoin() | ||
203 | this.buildAccountJoin() | ||
204 | |||
205 | const escapedLikeSearch = this.sequelize.escape('%' + this.options.search + '%') | ||
206 | |||
207 | where.push( | ||
208 | `(` + | ||
209 | `"VideoCommentModel"."text" ILIKE ${escapedLikeSearch} OR ` + | ||
210 | `"Account->Actor"."preferredUsername" ILIKE ${escapedLikeSearch} OR ` + | ||
211 | `"Account"."name" ILIKE ${escapedLikeSearch} OR ` + | ||
212 | `"Video"."name" ILIKE ${escapedLikeSearch} ` + | ||
213 | `)` | ||
214 | ) | ||
215 | } | ||
216 | |||
217 | if (this.options.searchAccount) { | ||
218 | this.buildAccountJoin() | ||
219 | |||
220 | const escapedLikeSearch = this.sequelize.escape('%' + this.options.searchAccount + '%') | ||
221 | |||
222 | where.push( | ||
223 | `(` + | ||
224 | `"Account->Actor"."preferredUsername" ILIKE ${escapedLikeSearch} OR ` + | ||
225 | `"Account"."name" ILIKE ${escapedLikeSearch} ` + | ||
226 | `)` | ||
227 | ) | ||
228 | } | ||
229 | |||
230 | if (this.options.searchVideo) { | ||
231 | this.buildVideoJoin() | ||
232 | |||
233 | const escapedLikeSearch = this.sequelize.escape('%' + this.options.searchVideo + '%') | ||
234 | |||
235 | where.push(`"Video"."name" ILIKE ${escapedLikeSearch}`) | ||
236 | } | ||
237 | |||
238 | if (where.length !== 0) { | ||
239 | this.innerWhere = `WHERE ${where.join(' AND ')}` | ||
240 | } | ||
241 | } | ||
242 | |||
243 | private buildAccountJoin () { | ||
244 | if (this.built.accountJoin) return | ||
245 | |||
246 | this.innerJoins += ' LEFT JOIN "account" "Account" ON "Account"."id" = "VideoCommentModel"."accountId" ' + | ||
247 | 'LEFT JOIN "actor" "Account->Actor" ON "Account->Actor"."id" = "Account"."actorId" ' + | ||
248 | 'LEFT JOIN "server" "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" ' | ||
249 | |||
250 | this.built.accountJoin = true | ||
251 | } | ||
252 | |||
253 | private buildVideoJoin () { | ||
254 | if (this.built.videoJoin) return | ||
255 | |||
256 | this.innerJoins += ' LEFT JOIN "video" "Video" ON "Video"."id" = "VideoCommentModel"."videoId" ' | ||
257 | |||
258 | this.built.videoJoin = true | ||
259 | } | ||
260 | |||
261 | private buildVideoChannelJoin () { | ||
262 | if (this.built.videoChannelJoin) return | ||
263 | |||
264 | this.buildVideoJoin() | ||
265 | |||
266 | this.innerJoins += ' LEFT JOIN "videoChannel" "Video->VideoChannel" ON "Video"."channelId" = "Video->VideoChannel"."id" ' | ||
267 | |||
268 | this.built.videoChannelJoin = true | ||
269 | } | ||
270 | |||
271 | private buildAvatarsJoin () { | ||
272 | if (this.options.selectType !== 'api' && this.options.selectType !== 'feed') return '' | ||
273 | if (this.built.avatarJoin) return | ||
274 | |||
275 | this.joins += `LEFT JOIN "actorImage" "Account->Actor->Avatars" ` + | ||
276 | `ON "VideoCommentModel"."Account.Actor.id" = "Account->Actor->Avatars"."actorId" ` + | ||
277 | `AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}` | ||
278 | |||
279 | this.built.avatarJoin = true | ||
280 | } | ||
281 | |||
282 | // --------------------------------------------------------------------------- | ||
283 | |||
284 | private buildListSelect () { | ||
285 | const toSelect = [ '"VideoCommentModel".*' ] | ||
286 | |||
287 | if (this.options.selectType === 'api' || this.options.selectType === 'feed') { | ||
288 | this.buildAvatarsJoin() | ||
289 | |||
290 | toSelect.push(this.tableAttributes.getAvatarAttributes()) | ||
291 | } | ||
292 | |||
293 | this.select = this.buildSelect(toSelect) | ||
294 | } | ||
295 | |||
296 | private buildInnerListSelect () { | ||
297 | let toSelect = [ this.tableAttributes.getVideoCommentAttributes() ] | ||
298 | |||
299 | if (this.options.selectType === 'api' || this.options.selectType === 'feed') { | ||
300 | this.buildAccountJoin() | ||
301 | this.buildVideoJoin() | ||
302 | |||
303 | toSelect = toSelect.concat([ | ||
304 | this.tableAttributes.getVideoAttributes(), | ||
305 | this.tableAttributes.getAccountAttributes(), | ||
306 | this.tableAttributes.getActorAttributes(), | ||
307 | this.tableAttributes.getServerAttributes() | ||
308 | ]) | ||
309 | } | ||
310 | |||
311 | if (this.options.includeReplyCounters === true) { | ||
312 | this.buildTotalRepliesSelect() | ||
313 | this.buildAuthorTotalRepliesSelect() | ||
314 | |||
315 | toSelect.push('"totalRepliesFromVideoAuthor"."count" AS "totalRepliesFromVideoAuthor"') | ||
316 | toSelect.push('"totalReplies"."count" AS "totalReplies"') | ||
317 | } | ||
318 | |||
319 | this.innerSelect = this.buildSelect(toSelect) | ||
320 | } | ||
321 | |||
322 | // --------------------------------------------------------------------------- | ||
323 | |||
324 | private getBlockWhere (commentTableName: string, channelTableName: string) { | ||
325 | const where: string[] = [] | ||
326 | |||
327 | const blockerIdsString = createSafeIn( | ||
328 | this.sequelize, | ||
329 | this.options.blockerAccountIds, | ||
330 | [ `"${channelTableName}"."accountId"` ] | ||
331 | ) | ||
332 | |||
333 | where.push( | ||
334 | `NOT EXISTS (` + | ||
335 | `SELECT 1 FROM "accountBlocklist" ` + | ||
336 | `WHERE "targetAccountId" = "${commentTableName}"."accountId" ` + | ||
337 | `AND "accountId" IN (${blockerIdsString})` + | ||
338 | `)` | ||
339 | ) | ||
340 | |||
341 | where.push( | ||
342 | `NOT EXISTS (` + | ||
343 | `SELECT 1 FROM "account" ` + | ||
344 | `INNER JOIN "actor" ON account."actorId" = actor.id ` + | ||
345 | `INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` + | ||
346 | `WHERE "account"."id" = "${commentTableName}"."accountId" ` + | ||
347 | `AND "serverBlocklist"."accountId" IN (${blockerIdsString})` + | ||
348 | `)` | ||
349 | ) | ||
350 | |||
351 | return where | ||
352 | } | ||
353 | |||
354 | // --------------------------------------------------------------------------- | ||
355 | |||
356 | private buildTotalRepliesSelect () { | ||
357 | const blockWhereString = this.getBlockWhere('replies', 'videoChannel').join(' AND ') | ||
358 | |||
359 | // Help the planner by providing videoId that should filter out many comments | ||
360 | this.replacements.videoId = this.options.videoId | ||
361 | |||
362 | this.innerLateralJoins += `LEFT JOIN LATERAL (` + | ||
363 | `SELECT COUNT("replies"."id") AS "count" FROM "videoComment" AS "replies" ` + | ||
364 | `INNER JOIN "video" ON "video"."id" = "replies"."videoId" AND "replies"."videoId" = :videoId ` + | ||
365 | `LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id" ` + | ||
366 | `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ` + | ||
367 | `AND "deletedAt" IS NULL ` + | ||
368 | `AND ${blockWhereString} ` + | ||
369 | `) "totalReplies" ON TRUE ` | ||
370 | } | ||
371 | |||
372 | private buildAuthorTotalRepliesSelect () { | ||
373 | // Help the planner by providing videoId that should filter out many comments | ||
374 | this.replacements.videoId = this.options.videoId | ||
375 | |||
376 | this.innerLateralJoins += `LEFT JOIN LATERAL (` + | ||
377 | `SELECT COUNT("replies"."id") AS "count" FROM "videoComment" AS "replies" ` + | ||
378 | `INNER JOIN "video" ON "video"."id" = "replies"."videoId" AND "replies"."videoId" = :videoId ` + | ||
379 | `INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ` + | ||
380 | `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" AND "replies"."accountId" = "videoChannel"."accountId"` + | ||
381 | `) "totalRepliesFromVideoAuthor" ON TRUE ` | ||
382 | } | ||
383 | |||
384 | private getOrder () { | ||
385 | if (!this.options.sort) return '' | ||
386 | |||
387 | const orders = getSort(this.options.sort) | ||
388 | |||
389 | return 'ORDER BY ' + orders.map(o => `"${o[0]}" ${o[1]}`).join(', ') | ||
390 | } | ||
391 | |||
392 | private getInnerLimit () { | ||
393 | if (!this.options.count) return '' | ||
394 | |||
395 | this.replacements.limit = this.options.count | ||
396 | this.replacements.offset = this.options.start || 0 | ||
397 | |||
398 | return `LIMIT :limit OFFSET :offset ` | ||
399 | } | ||
400 | } | ||
diff --git a/server/models/video/sql/comment/video-comment-table-attributes.ts b/server/models/video/sql/comment/video-comment-table-attributes.ts new file mode 100644 index 000000000..87f8750c1 --- /dev/null +++ b/server/models/video/sql/comment/video-comment-table-attributes.ts | |||
@@ -0,0 +1,43 @@ | |||
1 | import { Memoize } from '@server/helpers/memoize' | ||
2 | import { AccountModel } from '@server/models/account/account' | ||
3 | import { ActorModel } from '@server/models/actor/actor' | ||
4 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
5 | import { ServerModel } from '@server/models/server/server' | ||
6 | import { VideoCommentModel } from '../../video-comment' | ||
7 | |||
8 | export class VideoCommentTableAttributes { | ||
9 | |||
10 | @Memoize() | ||
11 | getVideoCommentAttributes () { | ||
12 | return VideoCommentModel.getSQLAttributes('VideoCommentModel').join(', ') | ||
13 | } | ||
14 | |||
15 | @Memoize() | ||
16 | getAccountAttributes () { | ||
17 | return AccountModel.getSQLAttributes('Account', 'Account.').join(', ') | ||
18 | } | ||
19 | |||
20 | @Memoize() | ||
21 | getVideoAttributes () { | ||
22 | return [ | ||
23 | `"Video"."id" AS "Video.id"`, | ||
24 | `"Video"."uuid" AS "Video.uuid"`, | ||
25 | `"Video"."name" AS "Video.name"` | ||
26 | ].join(', ') | ||
27 | } | ||
28 | |||
29 | @Memoize() | ||
30 | getActorAttributes () { | ||
31 | return ActorModel.getSQLAPIAttributes('Account->Actor', `Account.Actor.`).join(', ') | ||
32 | } | ||
33 | |||
34 | @Memoize() | ||
35 | getServerAttributes () { | ||
36 | return ServerModel.getSQLAttributes('Account->Actor->Server', `Account.Actor.Server.`).join(', ') | ||
37 | } | ||
38 | |||
39 | @Memoize() | ||
40 | getAvatarAttributes () { | ||
41 | return ActorImageModel.getSQLAttributes('Account->Actor->Avatars', 'Account.Actor.Avatars.').join(', ') | ||
42 | } | ||
43 | } | ||
diff --git a/server/models/video/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index f0ce69501..cbd57ad8c 100644 --- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { createSafeIn } from '@server/models/utils' | ||
4 | import { MUserAccountId } from '@server/types/models' | 3 | import { MUserAccountId } from '@server/types/models' |
5 | import { ActorImageType } from '@shared/models' | 4 | import { ActorImageType } from '@shared/models' |
6 | import { AbstractRunQuery } from '../../../../shared/abstract-run-query' | 5 | import { AbstractRunQuery } from '../../../../shared/abstract-run-query' |
6 | import { createSafeIn } from '../../../../shared' | ||
7 | import { VideoTableAttributes } from './video-table-attributes' | 7 | import { VideoTableAttributes } from './video-table-attributes' |
8 | 8 | ||
9 | /** | 9 | /** |
diff --git a/server/models/video/sql/video/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts index 7c864bf27..62f1855c7 100644 --- a/server/models/video/sql/video/videos-id-list-query-builder.ts +++ b/server/models/video/sql/video/videos-id-list-query-builder.ts | |||
@@ -2,11 +2,12 @@ import { Sequelize, Transaction } from 'sequelize' | |||
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { exists } from '@server/helpers/custom-validators/misc' | 3 | import { exists } from '@server/helpers/custom-validators/misc' |
4 | import { WEBSERVER } from '@server/initializers/constants' | 4 | import { WEBSERVER } from '@server/initializers/constants' |
5 | import { buildDirectionAndField, createSafeIn, parseRowCountResult } from '@server/models/utils' | 5 | import { buildSortDirectionAndField } from '@server/models/shared' |
6 | import { MUserAccountId, MUserId } from '@server/types/models' | 6 | import { MUserAccountId, MUserId } from '@server/types/models' |
7 | import { forceNumber } from '@shared/core-utils' | ||
7 | import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' | 8 | import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' |
9 | import { createSafeIn, parseRowCountResult } from '../../../shared' | ||
8 | import { AbstractRunQuery } from '../../../shared/abstract-run-query' | 10 | import { AbstractRunQuery } from '../../../shared/abstract-run-query' |
9 | import { forceNumber } from '@shared/core-utils' | ||
10 | 11 | ||
11 | /** | 12 | /** |
12 | * | 13 | * |
@@ -665,7 +666,7 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
665 | } | 666 | } |
666 | 667 | ||
667 | private buildOrder (value: string) { | 668 | private buildOrder (value: string) { |
668 | const { direction, field } = buildDirectionAndField(value) | 669 | const { direction, field } = buildSortDirectionAndField(value) |
669 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) | 670 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) |
670 | 671 | ||
671 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' | 672 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' |
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts index 653b9694b..cebde3755 100644 --- a/server/models/video/tag.ts +++ b/server/models/video/tag.ts | |||
@@ -4,7 +4,7 @@ import { MTag } from '@server/types/models' | |||
4 | import { AttributesOnly } from '@shared/typescript-utils' | 4 | import { AttributesOnly } from '@shared/typescript-utils' |
5 | import { VideoPrivacy, VideoState } from '../../../shared/models/videos' | 5 | import { VideoPrivacy, VideoState } from '../../../shared/models/videos' |
6 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' | 6 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' |
7 | import { throwIfNotValid } from '../utils' | 7 | import { throwIfNotValid } from '../shared' |
8 | import { VideoModel } from './video' | 8 | import { VideoModel } from './video' |
9 | import { VideoTagModel } from './video-tag' | 9 | import { VideoTagModel } from './video-tag' |
10 | 10 | ||
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 1cd8224c0..9247d0e2b 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -5,7 +5,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
5 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' | 5 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' |
6 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' | 6 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' |
7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
8 | import { getBlacklistSort, searchAttribute, SortType, throwIfNotValid } from '../utils' | 8 | import { getBlacklistSort, searchAttribute, throwIfNotValid } from '../shared' |
9 | import { ThumbnailModel } from './thumbnail' | 9 | import { ThumbnailModel } from './thumbnail' |
10 | import { VideoModel } from './video' | 10 | import { VideoModel } from './video' |
11 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | 11 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
@@ -57,7 +57,7 @@ export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlack | |||
57 | static listForApi (parameters: { | 57 | static listForApi (parameters: { |
58 | start: number | 58 | start: number |
59 | count: number | 59 | count: number |
60 | sort: SortType | 60 | sort: string |
61 | search?: string | 61 | search?: string |
62 | type?: VideoBlacklistType | 62 | type?: VideoBlacklistType |
63 | }) { | 63 | }) { |
@@ -67,7 +67,7 @@ export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlack | |||
67 | return { | 67 | return { |
68 | offset: start, | 68 | offset: start, |
69 | limit: count, | 69 | limit: count, |
70 | order: getBlacklistSort(sort.sortModel, sort.sortValue) | 70 | order: getBlacklistSort(sort) |
71 | } | 71 | } |
72 | } | 72 | } |
73 | 73 | ||
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index 5fbcd6e3b..2eaa77407 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts | |||
@@ -23,7 +23,7 @@ import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/vid | |||
23 | import { logger } from '../../helpers/logger' | 23 | import { logger } from '../../helpers/logger' |
24 | import { CONFIG } from '../../initializers/config' | 24 | import { CONFIG } from '../../initializers/config' |
25 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants' | 25 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants' |
26 | import { buildWhereIdOrUUID, throwIfNotValid } from '../utils' | 26 | import { buildWhereIdOrUUID, throwIfNotValid } from '../shared' |
27 | import { VideoModel } from './video' | 27 | import { VideoModel } from './video' |
28 | 28 | ||
29 | export enum ScopeNames { | 29 | export enum ScopeNames { |
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts index 1a1b8c88d..2db4b523a 100644 --- a/server/models/video/video-change-ownership.ts +++ b/server/models/video/video-change-ownership.ts | |||
@@ -3,7 +3,7 @@ import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@se | |||
3 | import { AttributesOnly } from '@shared/typescript-utils' | 3 | import { AttributesOnly } from '@shared/typescript-utils' |
4 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' | 4 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' |
5 | import { AccountModel } from '../account/account' | 5 | import { AccountModel } from '../account/account' |
6 | import { getSort } from '../utils' | 6 | import { getSort } from '../shared' |
7 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' | 7 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' |
8 | 8 | ||
9 | enum ScopeNames { | 9 | enum ScopeNames { |
diff --git a/server/models/video/video-channel-sync.ts b/server/models/video/video-channel-sync.ts index 6e49cde10..a4cbf51f5 100644 --- a/server/models/video/video-channel-sync.ts +++ b/server/models/video/video-channel-sync.ts | |||
@@ -21,7 +21,7 @@ import { VideoChannelSync, VideoChannelSyncState } from '@shared/models' | |||
21 | import { AttributesOnly } from '@shared/typescript-utils' | 21 | import { AttributesOnly } from '@shared/typescript-utils' |
22 | import { AccountModel } from '../account/account' | 22 | import { AccountModel } from '../account/account' |
23 | import { UserModel } from '../user/user' | 23 | import { UserModel } from '../user/user' |
24 | import { getChannelSyncSort, throwIfNotValid } from '../utils' | 24 | import { getChannelSyncSort, throwIfNotValid } from '../shared' |
25 | import { VideoChannelModel } from './video-channel' | 25 | import { VideoChannelModel } from './video-channel' |
26 | 26 | ||
27 | @DefaultScope(() => ({ | 27 | @DefaultScope(() => ({ |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 132c8f021..b71f5a197 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -43,8 +43,14 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' | |||
43 | import { ActorFollowModel } from '../actor/actor-follow' | 43 | import { ActorFollowModel } from '../actor/actor-follow' |
44 | import { ActorImageModel } from '../actor/actor-image' | 44 | import { ActorImageModel } from '../actor/actor-image' |
45 | import { ServerModel } from '../server/server' | 45 | import { ServerModel } from '../server/server' |
46 | import { setAsUpdated } from '../shared' | 46 | import { |
47 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 47 | buildServerIdsFollowedBy, |
48 | buildTrigramSearchIndex, | ||
49 | createSimilarityAttribute, | ||
50 | getSort, | ||
51 | setAsUpdated, | ||
52 | throwIfNotValid | ||
53 | } from '../shared' | ||
48 | import { VideoModel } from './video' | 54 | import { VideoModel } from './video' |
49 | import { VideoPlaylistModel } from './video-playlist' | 55 | import { VideoPlaylistModel } from './video-playlist' |
50 | 56 | ||
@@ -831,6 +837,6 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel | |||
831 | } | 837 | } |
832 | 838 | ||
833 | setAsUpdated (transaction?: Transaction) { | 839 | setAsUpdated (transaction?: Transaction) { |
834 | return setAsUpdated('videoChannel', this.id, transaction) | 840 | return setAsUpdated({ sequelize: this.sequelize, table: 'videoChannel', id: this.id, transaction }) |
835 | } | 841 | } |
836 | } | 842 | } |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index af9614d30..ff5142809 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 1 | import { FindOptions, Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, | 3 | AllowNull, |
4 | BelongsTo, | 4 | BelongsTo, |
@@ -13,11 +13,9 @@ import { | |||
13 | Table, | 13 | Table, |
14 | UpdatedAt | 14 | UpdatedAt |
15 | } from 'sequelize-typescript' | 15 | } from 'sequelize-typescript' |
16 | import { exists } from '@server/helpers/custom-validators/misc' | ||
17 | import { getServerActor } from '@server/models/application/application' | 16 | import { getServerActor } from '@server/models/application/application' |
18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 17 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
19 | import { uniqify } from '@shared/core-utils' | 18 | import { pick, uniqify } from '@shared/core-utils' |
20 | import { VideoPrivacy } from '@shared/models' | ||
21 | import { AttributesOnly } from '@shared/typescript-utils' | 19 | import { AttributesOnly } from '@shared/typescript-utils' |
22 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | 20 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
23 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 21 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
@@ -41,61 +39,19 @@ import { | |||
41 | } from '../../types/models/video' | 39 | } from '../../types/models/video' |
42 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | 40 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
43 | import { AccountModel } from '../account/account' | 41 | import { AccountModel } from '../account/account' |
44 | import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' | 42 | import { ActorModel } from '../actor/actor' |
45 | import { | 43 | import { buildLocalAccountIdsIn, buildSQLAttributes, throwIfNotValid } from '../shared' |
46 | buildBlockedAccountSQL, | 44 | import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder' |
47 | buildBlockedAccountSQLOptimized, | ||
48 | buildLocalAccountIdsIn, | ||
49 | getCommentSort, | ||
50 | searchAttribute, | ||
51 | throwIfNotValid | ||
52 | } from '../utils' | ||
53 | import { VideoModel } from './video' | 45 | import { VideoModel } from './video' |
54 | import { VideoChannelModel } from './video-channel' | 46 | import { VideoChannelModel } from './video-channel' |
55 | 47 | ||
56 | export enum ScopeNames { | 48 | export enum ScopeNames { |
57 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 49 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
58 | WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API', | ||
59 | WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', | 50 | WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', |
60 | WITH_VIDEO = 'WITH_VIDEO', | 51 | WITH_VIDEO = 'WITH_VIDEO' |
61 | ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' | ||
62 | } | 52 | } |
63 | 53 | ||
64 | @Scopes(() => ({ | 54 | @Scopes(() => ({ |
65 | [ScopeNames.ATTRIBUTES_FOR_API]: (blockerAccountIds: number[]) => { | ||
66 | return { | ||
67 | attributes: { | ||
68 | include: [ | ||
69 | [ | ||
70 | Sequelize.literal( | ||
71 | '(' + | ||
72 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' + | ||
73 | 'SELECT COUNT("replies"."id") ' + | ||
74 | 'FROM "videoComment" AS "replies" ' + | ||
75 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
76 | 'AND "deletedAt" IS NULL ' + | ||
77 | 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' + | ||
78 | ')' | ||
79 | ), | ||
80 | 'totalReplies' | ||
81 | ], | ||
82 | [ | ||
83 | Sequelize.literal( | ||
84 | '(' + | ||
85 | 'SELECT COUNT("replies"."id") ' + | ||
86 | 'FROM "videoComment" AS "replies" ' + | ||
87 | 'INNER JOIN "video" ON "video"."id" = "replies"."videoId" ' + | ||
88 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
89 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
90 | 'AND "replies"."accountId" = "videoChannel"."accountId"' + | ||
91 | ')' | ||
92 | ), | ||
93 | 'totalRepliesFromVideoAuthor' | ||
94 | ] | ||
95 | ] | ||
96 | } | ||
97 | } as FindOptions | ||
98 | }, | ||
99 | [ScopeNames.WITH_ACCOUNT]: { | 55 | [ScopeNames.WITH_ACCOUNT]: { |
100 | include: [ | 56 | include: [ |
101 | { | 57 | { |
@@ -103,22 +59,6 @@ export enum ScopeNames { | |||
103 | } | 59 | } |
104 | ] | 60 | ] |
105 | }, | 61 | }, |
106 | [ScopeNames.WITH_ACCOUNT_FOR_API]: { | ||
107 | include: [ | ||
108 | { | ||
109 | model: AccountModel.unscoped(), | ||
110 | include: [ | ||
111 | { | ||
112 | attributes: { | ||
113 | exclude: unusedActorAttributesForAPI | ||
114 | }, | ||
115 | model: ActorModel, // Default scope includes avatar and server | ||
116 | required: true | ||
117 | } | ||
118 | ] | ||
119 | } | ||
120 | ] | ||
121 | }, | ||
122 | [ScopeNames.WITH_IN_REPLY_TO]: { | 62 | [ScopeNames.WITH_IN_REPLY_TO]: { |
123 | include: [ | 63 | include: [ |
124 | { | 64 | { |
@@ -252,6 +192,18 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
252 | }) | 192 | }) |
253 | CommentAbuses: VideoCommentAbuseModel[] | 193 | CommentAbuses: VideoCommentAbuseModel[] |
254 | 194 | ||
195 | // --------------------------------------------------------------------------- | ||
196 | |||
197 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
198 | return buildSQLAttributes({ | ||
199 | model: this, | ||
200 | tableName, | ||
201 | aliasPrefix | ||
202 | }) | ||
203 | } | ||
204 | |||
205 | // --------------------------------------------------------------------------- | ||
206 | |||
255 | static loadById (id: number, t?: Transaction): Promise<MComment> { | 207 | static loadById (id: number, t?: Transaction): Promise<MComment> { |
256 | const query: FindOptions = { | 208 | const query: FindOptions = { |
257 | where: { | 209 | where: { |
@@ -319,93 +271,19 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
319 | searchAccount?: string | 271 | searchAccount?: string |
320 | searchVideo?: string | 272 | searchVideo?: string |
321 | }) { | 273 | }) { |
322 | const { start, count, sort, isLocal, search, searchAccount, searchVideo, onLocalVideo } = parameters | 274 | const queryOptions: ListVideoCommentsOptions = { |
275 | ...pick(parameters, [ 'start', 'count', 'sort', 'isLocal', 'search', 'searchVideo', 'searchAccount', 'onLocalVideo' ]), | ||
323 | 276 | ||
324 | const where: WhereOptions = { | 277 | selectType: 'api', |
325 | deletedAt: null | 278 | notDeleted: true |
326 | } | ||
327 | |||
328 | const whereAccount: WhereOptions = {} | ||
329 | const whereActor: WhereOptions = {} | ||
330 | const whereVideo: WhereOptions = {} | ||
331 | |||
332 | if (isLocal === true) { | ||
333 | Object.assign(whereActor, { | ||
334 | serverId: null | ||
335 | }) | ||
336 | } else if (isLocal === false) { | ||
337 | Object.assign(whereActor, { | ||
338 | serverId: { | ||
339 | [Op.ne]: null | ||
340 | } | ||
341 | }) | ||
342 | } | ||
343 | |||
344 | if (search) { | ||
345 | Object.assign(where, { | ||
346 | [Op.or]: [ | ||
347 | searchAttribute(search, 'text'), | ||
348 | searchAttribute(search, '$Account.Actor.preferredUsername$'), | ||
349 | searchAttribute(search, '$Account.name$'), | ||
350 | searchAttribute(search, '$Video.name$') | ||
351 | ] | ||
352 | }) | ||
353 | } | ||
354 | |||
355 | if (searchAccount) { | ||
356 | Object.assign(whereActor, { | ||
357 | [Op.or]: [ | ||
358 | searchAttribute(searchAccount, '$Account.Actor.preferredUsername$'), | ||
359 | searchAttribute(searchAccount, '$Account.name$') | ||
360 | ] | ||
361 | }) | ||
362 | } | ||
363 | |||
364 | if (searchVideo) { | ||
365 | Object.assign(whereVideo, searchAttribute(searchVideo, 'name')) | ||
366 | } | ||
367 | |||
368 | if (exists(onLocalVideo)) { | ||
369 | Object.assign(whereVideo, { remote: !onLocalVideo }) | ||
370 | } | ||
371 | |||
372 | const getQuery = (forCount: boolean) => { | ||
373 | return { | ||
374 | offset: start, | ||
375 | limit: count, | ||
376 | order: getCommentSort(sort), | ||
377 | where, | ||
378 | include: [ | ||
379 | { | ||
380 | model: AccountModel.unscoped(), | ||
381 | required: true, | ||
382 | where: whereAccount, | ||
383 | include: [ | ||
384 | { | ||
385 | attributes: { | ||
386 | exclude: unusedActorAttributesForAPI | ||
387 | }, | ||
388 | model: forCount === true | ||
389 | ? ActorModel.unscoped() // Default scope includes avatar and server | ||
390 | : ActorModel, | ||
391 | required: true, | ||
392 | where: whereActor | ||
393 | } | ||
394 | ] | ||
395 | }, | ||
396 | { | ||
397 | model: VideoModel.unscoped(), | ||
398 | required: true, | ||
399 | where: whereVideo | ||
400 | } | ||
401 | ] | ||
402 | } | ||
403 | } | 279 | } |
404 | 280 | ||
405 | return Promise.all([ | 281 | return Promise.all([ |
406 | VideoCommentModel.count(getQuery(true)), | 282 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(), |
407 | VideoCommentModel.findAll(getQuery(false)) | 283 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments() |
408 | ]).then(([ total, data ]) => ({ total, data })) | 284 | ]).then(([ rows, count ]) => { |
285 | return { total: count, data: rows } | ||
286 | }) | ||
409 | } | 287 | } |
410 | 288 | ||
411 | static async listThreadsForApi (parameters: { | 289 | static async listThreadsForApi (parameters: { |
@@ -416,67 +294,40 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
416 | sort: string | 294 | sort: string |
417 | user?: MUserAccountId | 295 | user?: MUserAccountId |
418 | }) { | 296 | }) { |
419 | const { videoId, isVideoOwned, start, count, sort, user } = parameters | 297 | const { videoId, user } = parameters |
420 | 298 | ||
421 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) | 299 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user }) |
422 | 300 | ||
423 | const accountBlockedWhere = { | 301 | const commonOptions: ListVideoCommentsOptions = { |
424 | accountId: { | 302 | selectType: 'api', |
425 | [Op.notIn]: Sequelize.literal( | 303 | videoId, |
426 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | 304 | blockerAccountIds |
427 | ) | ||
428 | } | ||
429 | } | 305 | } |
430 | 306 | ||
431 | const queryList = { | 307 | const listOptions: ListVideoCommentsOptions = { |
432 | offset: start, | 308 | ...commonOptions, |
433 | limit: count, | 309 | ...pick(parameters, [ 'sort', 'start', 'count' ]), |
434 | order: getCommentSort(sort), | 310 | |
435 | where: { | 311 | isThread: true, |
436 | [Op.and]: [ | 312 | includeReplyCounters: true |
437 | { | ||
438 | videoId | ||
439 | }, | ||
440 | { | ||
441 | inReplyToCommentId: null | ||
442 | }, | ||
443 | { | ||
444 | [Op.or]: [ | ||
445 | accountBlockedWhere, | ||
446 | { | ||
447 | accountId: null | ||
448 | } | ||
449 | ] | ||
450 | } | ||
451 | ] | ||
452 | } | ||
453 | } | 313 | } |
454 | 314 | ||
455 | const findScopesList: (string | ScopeOptions)[] = [ | 315 | const countOptions: ListVideoCommentsOptions = { |
456 | ScopeNames.WITH_ACCOUNT_FOR_API, | 316 | ...commonOptions, |
457 | { | ||
458 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | ||
459 | } | ||
460 | ] | ||
461 | 317 | ||
462 | const countScopesList: ScopeOptions[] = [ | 318 | isThread: true |
463 | { | 319 | } |
464 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | ||
465 | } | ||
466 | ] | ||
467 | 320 | ||
468 | const notDeletedQueryCount = { | 321 | const notDeletedCountOptions: ListVideoCommentsOptions = { |
469 | where: { | 322 | ...commonOptions, |
470 | videoId, | 323 | |
471 | deletedAt: null, | 324 | notDeleted: true |
472 | ...accountBlockedWhere | ||
473 | } | ||
474 | } | 325 | } |
475 | 326 | ||
476 | return Promise.all([ | 327 | return Promise.all([ |
477 | VideoCommentModel.scope(findScopesList).findAll(queryList), | 328 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, listOptions).listComments<MCommentAdminFormattable>(), |
478 | VideoCommentModel.scope(countScopesList).count(queryList), | 329 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, countOptions).countComments(), |
479 | VideoCommentModel.count(notDeletedQueryCount) | 330 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, notDeletedCountOptions).countComments() |
480 | ]).then(([ rows, count, totalNotDeletedComments ]) => { | 331 | ]).then(([ rows, count, totalNotDeletedComments ]) => { |
481 | return { total: count, data: rows, totalNotDeletedComments } | 332 | return { total: count, data: rows, totalNotDeletedComments } |
482 | }) | 333 | }) |
@@ -484,54 +335,29 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
484 | 335 | ||
485 | static async listThreadCommentsForApi (parameters: { | 336 | static async listThreadCommentsForApi (parameters: { |
486 | videoId: number | 337 | videoId: number |
487 | isVideoOwned: boolean | ||
488 | threadId: number | 338 | threadId: number |
489 | user?: MUserAccountId | 339 | user?: MUserAccountId |
490 | }) { | 340 | }) { |
491 | const { videoId, threadId, user, isVideoOwned } = parameters | 341 | const { user } = parameters |
492 | 342 | ||
493 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) | 343 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user }) |
494 | 344 | ||
495 | const query = { | 345 | const queryOptions: ListVideoCommentsOptions = { |
496 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, | 346 | ...pick(parameters, [ 'videoId', 'threadId' ]), |
497 | where: { | ||
498 | videoId, | ||
499 | [Op.and]: [ | ||
500 | { | ||
501 | [Op.or]: [ | ||
502 | { id: threadId }, | ||
503 | { originCommentId: threadId } | ||
504 | ] | ||
505 | }, | ||
506 | { | ||
507 | [Op.or]: [ | ||
508 | { | ||
509 | accountId: { | ||
510 | [Op.notIn]: Sequelize.literal( | ||
511 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | ||
512 | ) | ||
513 | } | ||
514 | }, | ||
515 | { | ||
516 | accountId: null | ||
517 | } | ||
518 | ] | ||
519 | } | ||
520 | ] | ||
521 | } | ||
522 | } | ||
523 | 347 | ||
524 | const scopes: any[] = [ | 348 | selectType: 'api', |
525 | ScopeNames.WITH_ACCOUNT_FOR_API, | 349 | sort: 'createdAt', |
526 | { | 350 | |
527 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | 351 | blockerAccountIds, |
528 | } | 352 | includeReplyCounters: true |
529 | ] | 353 | } |
530 | 354 | ||
531 | return Promise.all([ | 355 | return Promise.all([ |
532 | VideoCommentModel.count(query), | 356 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(), |
533 | VideoCommentModel.scope(scopes).findAll(query) | 357 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments() |
534 | ]).then(([ total, data ]) => ({ total, data })) | 358 | ]).then(([ rows, count ]) => { |
359 | return { total: count, data: rows } | ||
360 | }) | ||
535 | } | 361 | } |
536 | 362 | ||
537 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { | 363 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { |
@@ -559,31 +385,31 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
559 | .findAll(query) | 385 | .findAll(query) |
560 | } | 386 | } |
561 | 387 | ||
562 | static async listAndCountByVideoForAP (video: MVideoImmutable, start: number, count: number, t?: Transaction) { | 388 | static async listAndCountByVideoForAP (parameters: { |
563 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ | 389 | video: MVideoImmutable |
390 | start: number | ||
391 | count: number | ||
392 | }) { | ||
393 | const { video } = parameters | ||
394 | |||
395 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null }) | ||
396 | |||
397 | const queryOptions: ListVideoCommentsOptions = { | ||
398 | ...pick(parameters, [ 'start', 'count' ]), | ||
399 | |||
400 | selectType: 'comment-only', | ||
564 | videoId: video.id, | 401 | videoId: video.id, |
565 | isVideoOwned: video.isOwned() | 402 | sort: 'createdAt', |
566 | }) | ||
567 | 403 | ||
568 | const query = { | 404 | blockerAccountIds |
569 | order: [ [ 'createdAt', 'ASC' ] ] as Order, | ||
570 | offset: start, | ||
571 | limit: count, | ||
572 | where: { | ||
573 | videoId: video.id, | ||
574 | accountId: { | ||
575 | [Op.notIn]: Sequelize.literal( | ||
576 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | ||
577 | ) | ||
578 | } | ||
579 | }, | ||
580 | transaction: t | ||
581 | } | 405 | } |
582 | 406 | ||
583 | return Promise.all([ | 407 | return Promise.all([ |
584 | VideoCommentModel.count(query), | 408 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>(), |
585 | VideoCommentModel.findAll<MComment>(query) | 409 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments() |
586 | ]).then(([ total, data ]) => ({ total, data })) | 410 | ]).then(([ rows, count ]) => { |
411 | return { total: count, data: rows } | ||
412 | }) | ||
587 | } | 413 | } |
588 | 414 | ||
589 | static async listForFeed (parameters: { | 415 | static async listForFeed (parameters: { |
@@ -592,97 +418,36 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
592 | videoId?: number | 418 | videoId?: number |
593 | accountId?: number | 419 | accountId?: number |
594 | videoChannelId?: number | 420 | videoChannelId?: number |
595 | }): Promise<MCommentOwnerVideoFeed[]> { | 421 | }) { |
596 | const serverActor = await getServerActor() | 422 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null }) |
597 | const { start, count, videoId, accountId, videoChannelId } = parameters | ||
598 | |||
599 | const whereAnd: WhereOptions[] = buildBlockedAccountSQLOptimized( | ||
600 | '"VideoCommentModel"."accountId"', | ||
601 | [ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ] | ||
602 | ) | ||
603 | 423 | ||
604 | if (accountId) { | 424 | const queryOptions: ListVideoCommentsOptions = { |
605 | whereAnd.push({ | 425 | ...pick(parameters, [ 'start', 'count', 'accountId', 'videoId', 'videoChannelId' ]), |
606 | accountId | ||
607 | }) | ||
608 | } | ||
609 | 426 | ||
610 | const accountWhere = { | 427 | selectType: 'feed', |
611 | [Op.and]: whereAnd | ||
612 | } | ||
613 | 428 | ||
614 | const videoChannelWhere = videoChannelId ? { id: videoChannelId } : undefined | 429 | sort: '-createdAt', |
430 | onPublicVideo: true, | ||
431 | notDeleted: true, | ||
615 | 432 | ||
616 | const query = { | 433 | blockerAccountIds |
617 | order: [ [ 'createdAt', 'DESC' ] ] as Order, | ||
618 | offset: start, | ||
619 | limit: count, | ||
620 | where: { | ||
621 | deletedAt: null, | ||
622 | accountId: accountWhere | ||
623 | }, | ||
624 | include: [ | ||
625 | { | ||
626 | attributes: [ 'name', 'uuid' ], | ||
627 | model: VideoModel.unscoped(), | ||
628 | required: true, | ||
629 | where: { | ||
630 | privacy: VideoPrivacy.PUBLIC | ||
631 | }, | ||
632 | include: [ | ||
633 | { | ||
634 | attributes: [ 'accountId' ], | ||
635 | model: VideoChannelModel.unscoped(), | ||
636 | required: true, | ||
637 | where: videoChannelWhere | ||
638 | } | ||
639 | ] | ||
640 | } | ||
641 | ] | ||
642 | } | 434 | } |
643 | 435 | ||
644 | if (videoId) query.where['videoId'] = videoId | 436 | return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentOwnerVideoFeed>() |
645 | |||
646 | return VideoCommentModel | ||
647 | .scope([ ScopeNames.WITH_ACCOUNT ]) | ||
648 | .findAll(query) | ||
649 | } | 437 | } |
650 | 438 | ||
651 | static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { | 439 | static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { |
652 | const accountWhere = filter.onVideosOfAccount | 440 | const queryOptions: ListVideoCommentsOptions = { |
653 | ? { id: filter.onVideosOfAccount.id } | 441 | selectType: 'comment-only', |
654 | : {} | ||
655 | 442 | ||
656 | const query = { | 443 | accountId: ofAccount.id, |
657 | limit: 1000, | 444 | videoAccountOwnerId: filter.onVideosOfAccount?.id, |
658 | where: { | 445 | |
659 | deletedAt: null, | 446 | notDeleted: true, |
660 | accountId: ofAccount.id | 447 | count: 5000 |
661 | }, | ||
662 | include: [ | ||
663 | { | ||
664 | model: VideoModel, | ||
665 | required: true, | ||
666 | include: [ | ||
667 | { | ||
668 | model: VideoChannelModel, | ||
669 | required: true, | ||
670 | include: [ | ||
671 | { | ||
672 | model: AccountModel, | ||
673 | required: true, | ||
674 | where: accountWhere | ||
675 | } | ||
676 | ] | ||
677 | } | ||
678 | ] | ||
679 | } | ||
680 | ] | ||
681 | } | 448 | } |
682 | 449 | ||
683 | return VideoCommentModel | 450 | return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>() |
684 | .scope([ ScopeNames.WITH_ACCOUNT ]) | ||
685 | .findAll(query) | ||
686 | } | 451 | } |
687 | 452 | ||
688 | static async getStats () { | 453 | static async getStats () { |
@@ -750,9 +515,7 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
750 | } | 515 | } |
751 | 516 | ||
752 | isOwned () { | 517 | isOwned () { |
753 | if (!this.Account) { | 518 | if (!this.Account) return false |
754 | return false | ||
755 | } | ||
756 | 519 | ||
757 | return this.Account.isOwned() | 520 | return this.Account.isOwned() |
758 | } | 521 | } |
@@ -906,22 +669,15 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
906 | } | 669 | } |
907 | 670 | ||
908 | private static async buildBlockerAccountIds (options: { | 671 | private static async buildBlockerAccountIds (options: { |
909 | videoId: number | 672 | user: MUserAccountId |
910 | isVideoOwned: boolean | 673 | }): Promise<number[]> { |
911 | user?: MUserAccountId | 674 | const { user } = options |
912 | }) { | ||
913 | const { videoId, user, isVideoOwned } = options | ||
914 | 675 | ||
915 | const serverActor = await getServerActor() | 676 | const serverActor = await getServerActor() |
916 | const blockerAccountIds = [ serverActor.Account.id ] | 677 | const blockerAccountIds = [ serverActor.Account.id ] |
917 | 678 | ||
918 | if (user) blockerAccountIds.push(user.Account.id) | 679 | if (user) blockerAccountIds.push(user.Account.id) |
919 | 680 | ||
920 | if (isVideoOwned) { | ||
921 | const videoOwnerAccount = await AccountModel.loadAccountIdFromVideo(videoId) | ||
922 | if (videoOwnerAccount) blockerAccountIds.push(videoOwnerAccount.id) | ||
923 | } | ||
924 | |||
925 | return blockerAccountIds | 681 | return blockerAccountIds |
926 | } | 682 | } |
927 | } | 683 | } |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 9c4e6d078..07bc13de1 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -21,6 +21,7 @@ import { | |||
21 | import validator from 'validator' | 21 | import validator from 'validator' |
22 | import { logger } from '@server/helpers/logger' | 22 | import { logger } from '@server/helpers/logger' |
23 | import { extractVideo } from '@server/helpers/video' | 23 | import { extractVideo } from '@server/helpers/video' |
24 | import { CONFIG } from '@server/initializers/config' | ||
24 | import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' | 25 | import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' |
25 | import { | 26 | import { |
26 | getHLSPrivateFileUrl, | 27 | getHLSPrivateFileUrl, |
@@ -50,11 +51,9 @@ import { | |||
50 | } from '../../initializers/constants' | 51 | } from '../../initializers/constants' |
51 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' | 52 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' |
52 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 53 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
53 | import { doesExist } from '../shared' | 54 | import { doesExist, parseAggregateResult, throwIfNotValid } from '../shared' |
54 | import { parseAggregateResult, throwIfNotValid } from '../utils' | ||
55 | import { VideoModel } from './video' | 55 | import { VideoModel } from './video' |
56 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 56 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
57 | import { CONFIG } from '@server/initializers/config' | ||
58 | 57 | ||
59 | export enum ScopeNames { | 58 | export enum ScopeNames { |
60 | WITH_VIDEO = 'WITH_VIDEO', | 59 | WITH_VIDEO = 'WITH_VIDEO', |
@@ -266,7 +265,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
266 | static doesInfohashExist (infoHash: string) { | 265 | static doesInfohashExist (infoHash: string) { |
267 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' | 266 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' |
268 | 267 | ||
269 | return doesExist(query, { infoHash }) | 268 | return doesExist(this.sequelize, query, { infoHash }) |
270 | } | 269 | } |
271 | 270 | ||
272 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { | 271 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { |
@@ -282,14 +281,14 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
282 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + | 281 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + |
283 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' | 282 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' |
284 | 283 | ||
285 | return doesExist(query, { filename }) | 284 | return doesExist(this.sequelize, query, { filename }) |
286 | } | 285 | } |
287 | 286 | ||
288 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { | 287 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { |
289 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + | 288 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + |
290 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` | 289 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` |
291 | 290 | ||
292 | return doesExist(query, { filename }) | 291 | return doesExist(this.sequelize, query, { filename }) |
293 | } | 292 | } |
294 | 293 | ||
295 | static loadByFilename (filename: string) { | 294 | static loadByFilename (filename: string) { |
@@ -439,7 +438,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
439 | if (!element) return videoFile.save({ transaction }) | 438 | if (!element) return videoFile.save({ transaction }) |
440 | 439 | ||
441 | for (const k of Object.keys(videoFile.toJSON())) { | 440 | for (const k of Object.keys(videoFile.toJSON())) { |
442 | element[k] = videoFile[k] | 441 | element.set(k, videoFile[k]) |
443 | } | 442 | } |
444 | 443 | ||
445 | return element.save({ transaction }) | 444 | return element.save({ transaction }) |
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index da6b92c7a..c040e0fda 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -22,7 +22,7 @@ import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../help | |||
22 | import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' | 22 | import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' |
23 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' | 23 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' |
24 | import { UserModel } from '../user/user' | 24 | import { UserModel } from '../user/user' |
25 | import { getSort, searchAttribute, throwIfNotValid } from '../utils' | 25 | import { getSort, searchAttribute, throwIfNotValid } from '../shared' |
26 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' | 26 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' |
27 | import { VideoChannelSyncModel } from './video-channel-sync' | 27 | import { VideoChannelSyncModel } from './video-channel-sync' |
28 | 28 | ||
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index 7181b5599..b832f9768 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -31,7 +31,7 @@ import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/ | |||
31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
32 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 32 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
33 | import { AccountModel } from '../account/account' | 33 | import { AccountModel } from '../account/account' |
34 | import { getSort, throwIfNotValid } from '../utils' | 34 | import { getSort, throwIfNotValid } from '../shared' |
35 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' | 35 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' |
36 | import { VideoPlaylistModel } from './video-playlist' | 36 | import { VideoPlaylistModel } from './video-playlist' |
37 | 37 | ||
@@ -309,7 +309,23 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide | |||
309 | return VideoPlaylistElementModel.increment({ position: by }, query) | 309 | return VideoPlaylistElementModel.increment({ position: by }, query) |
310 | } | 310 | } |
311 | 311 | ||
312 | getType (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) { | 312 | toFormattedJSON ( |
313 | this: MVideoPlaylistElementFormattable, | ||
314 | options: { accountId?: number } = {} | ||
315 | ): VideoPlaylistElement { | ||
316 | return { | ||
317 | id: this.id, | ||
318 | position: this.position, | ||
319 | startTimestamp: this.startTimestamp, | ||
320 | stopTimestamp: this.stopTimestamp, | ||
321 | |||
322 | type: this.getType(options.accountId), | ||
323 | |||
324 | video: this.getVideoElement(options.accountId) | ||
325 | } | ||
326 | } | ||
327 | |||
328 | getType (this: MVideoPlaylistElementFormattable, accountId?: number) { | ||
313 | const video = this.Video | 329 | const video = this.Video |
314 | 330 | ||
315 | if (!video) return VideoPlaylistElementType.DELETED | 331 | if (!video) return VideoPlaylistElementType.DELETED |
@@ -323,34 +339,17 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide | |||
323 | if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE | 339 | if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE |
324 | 340 | ||
325 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE | 341 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE |
326 | if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE | ||
327 | 342 | ||
328 | return VideoPlaylistElementType.REGULAR | 343 | return VideoPlaylistElementType.REGULAR |
329 | } | 344 | } |
330 | 345 | ||
331 | getVideoElement (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) { | 346 | getVideoElement (this: MVideoPlaylistElementFormattable, accountId?: number) { |
332 | if (!this.Video) return null | 347 | if (!this.Video) return null |
333 | if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null | 348 | if (this.getType(accountId) !== VideoPlaylistElementType.REGULAR) return null |
334 | 349 | ||
335 | return this.Video.toFormattedJSON() | 350 | return this.Video.toFormattedJSON() |
336 | } | 351 | } |
337 | 352 | ||
338 | toFormattedJSON ( | ||
339 | this: MVideoPlaylistElementFormattable, | ||
340 | options: { displayNSFW?: boolean, accountId?: number } = {} | ||
341 | ): VideoPlaylistElement { | ||
342 | return { | ||
343 | id: this.id, | ||
344 | position: this.position, | ||
345 | startTimestamp: this.startTimestamp, | ||
346 | stopTimestamp: this.stopTimestamp, | ||
347 | |||
348 | type: this.getType(options.displayNSFW, options.accountId), | ||
349 | |||
350 | video: this.getVideoElement(options.displayNSFW, options.accountId) | ||
351 | } | ||
352 | } | ||
353 | |||
354 | toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject { | 353 | toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject { |
355 | const base: PlaylistElementObject = { | 354 | const base: PlaylistElementObject = { |
356 | id: this.url, | 355 | id: this.url, |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 8bbe54c49..faf4bea78 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -21,12 +21,8 @@ import { activityPubCollectionPagination } from '@server/lib/activitypub/collect | |||
21 | import { MAccountId, MChannelId } from '@server/types/models' | 21 | import { MAccountId, MChannelId } from '@server/types/models' |
22 | import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' | 22 | import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' |
23 | import { buildUUID, uuidToShort } from '@shared/extra-utils' | 23 | import { buildUUID, uuidToShort } from '@shared/extra-utils' |
24 | import { ActivityIconObject, PlaylistObject, VideoPlaylist, VideoPlaylistPrivacy, VideoPlaylistType } from '@shared/models' | ||
24 | import { AttributesOnly } from '@shared/typescript-utils' | 25 | import { AttributesOnly } from '@shared/typescript-utils' |
25 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | ||
26 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | ||
27 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
28 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' | ||
29 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' | ||
30 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 26 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
31 | import { | 27 | import { |
32 | isVideoPlaylistDescriptionValid, | 28 | isVideoPlaylistDescriptionValid, |
@@ -53,7 +49,6 @@ import { | |||
53 | } from '../../types/models/video/video-playlist' | 49 | } from '../../types/models/video/video-playlist' |
54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | 50 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
55 | import { ActorModel } from '../actor/actor' | 51 | import { ActorModel } from '../actor/actor' |
56 | import { setAsUpdated } from '../shared' | ||
57 | import { | 52 | import { |
58 | buildServerIdsFollowedBy, | 53 | buildServerIdsFollowedBy, |
59 | buildTrigramSearchIndex, | 54 | buildTrigramSearchIndex, |
@@ -61,8 +56,9 @@ import { | |||
61 | createSimilarityAttribute, | 56 | createSimilarityAttribute, |
62 | getPlaylistSort, | 57 | getPlaylistSort, |
63 | isOutdated, | 58 | isOutdated, |
59 | setAsUpdated, | ||
64 | throwIfNotValid | 60 | throwIfNotValid |
65 | } from '../utils' | 61 | } from '../shared' |
66 | import { ThumbnailModel } from './thumbnail' | 62 | import { ThumbnailModel } from './thumbnail' |
67 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 63 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
68 | import { VideoPlaylistElementModel } from './video-playlist-element' | 64 | import { VideoPlaylistElementModel } from './video-playlist-element' |
@@ -641,7 +637,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
641 | } | 637 | } |
642 | 638 | ||
643 | setAsRefreshed () { | 639 | setAsRefreshed () { |
644 | return setAsUpdated('videoPlaylist', this.id) | 640 | return setAsUpdated({ sequelize: this.sequelize, table: 'videoPlaylist', id: this.id }) |
645 | } | 641 | } |
646 | 642 | ||
647 | setVideosLength (videosLength: number) { | 643 | setVideosLength (videosLength: number) { |
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index f2190037e..b4de2b20f 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -7,7 +7,7 @@ import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | |||
7 | import { MActorDefault, MActorFollowersUrl, MActorId } from '../../types/models' | 7 | import { MActorDefault, MActorFollowersUrl, MActorId } from '../../types/models' |
8 | import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' | 8 | import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' |
9 | import { ActorModel } from '../actor/actor' | 9 | import { ActorModel } from '../actor/actor' |
10 | import { buildLocalActorIdsIn, throwIfNotValid } from '../utils' | 10 | import { buildLocalActorIdsIn, throwIfNotValid } from '../shared' |
11 | import { VideoModel } from './video' | 11 | import { VideoModel } from './video' |
12 | 12 | ||
13 | enum ScopeNames { | 13 | enum ScopeNames { |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index 0386edf28..a85c79c9f 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -37,8 +37,7 @@ import { | |||
37 | WEBSERVER | 37 | WEBSERVER |
38 | } from '../../initializers/constants' | 38 | } from '../../initializers/constants' |
39 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 39 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
40 | import { doesExist } from '../shared' | 40 | import { doesExist, throwIfNotValid } from '../shared' |
41 | import { throwIfNotValid } from '../utils' | ||
42 | import { VideoModel } from './video' | 41 | import { VideoModel } from './video' |
43 | 42 | ||
44 | @Table({ | 43 | @Table({ |
@@ -138,7 +137,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
138 | static doesInfohashExist (infoHash: string) { | 137 | static doesInfohashExist (infoHash: string) { |
139 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' | 138 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' |
140 | 139 | ||
141 | return doesExist(query, { infoHash }) | 140 | return doesExist(this.sequelize, query, { infoHash }) |
142 | } | 141 | } |
143 | 142 | ||
144 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { | 143 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { |
@@ -237,7 +236,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
237 | `AND "video"."remote" IS FALSE AND "video"."uuid" = $videoUUID ` + | 236 | `AND "video"."remote" IS FALSE AND "video"."uuid" = $videoUUID ` + |
238 | `AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` | 237 | `AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` |
239 | 238 | ||
240 | return doesExist(query, { videoUUID }) | 239 | return doesExist(this.sequelize, query, { videoUUID }) |
241 | } | 240 | } |
242 | 241 | ||
243 | assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { | 242 | assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 56cc45cfe..1a10d2da2 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -32,7 +32,7 @@ import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFil | |||
32 | import { VideoPathManager } from '@server/lib/video-path-manager' | 32 | import { VideoPathManager } from '@server/lib/video-path-manager' |
33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | 33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' |
34 | import { getServerActor } from '@server/models/application/application' | 34 | import { getServerActor } from '@server/models/application/application' |
35 | import { ModelCache } from '@server/models/model-cache' | 35 | import { ModelCache } from '@server/models/shared/model-cache' |
36 | import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' | 36 | import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' |
37 | import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils' | 37 | import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils' |
38 | import { | 38 | import { |
@@ -103,10 +103,9 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy' | |||
103 | import { ServerModel } from '../server/server' | 103 | import { ServerModel } from '../server/server' |
104 | import { TrackerModel } from '../server/tracker' | 104 | import { TrackerModel } from '../server/tracker' |
105 | import { VideoTrackerModel } from '../server/video-tracker' | 105 | import { VideoTrackerModel } from '../server/video-tracker' |
106 | import { setAsUpdated } from '../shared' | 106 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, setAsUpdated, throwIfNotValid } from '../shared' |
107 | import { UserModel } from '../user/user' | 107 | import { UserModel } from '../user/user' |
108 | import { UserVideoHistoryModel } from '../user/user-video-history' | 108 | import { UserVideoHistoryModel } from '../user/user-video-history' |
109 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | ||
110 | import { VideoViewModel } from '../view/video-view' | 109 | import { VideoViewModel } from '../view/video-view' |
111 | import { | 110 | import { |
112 | videoFilesModelToFormattedJSON, | 111 | videoFilesModelToFormattedJSON, |
@@ -1871,7 +1870,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1871 | } | 1870 | } |
1872 | 1871 | ||
1873 | setAsRefreshed (transaction?: Transaction) { | 1872 | setAsRefreshed (transaction?: Transaction) { |
1874 | return setAsUpdated('video', this.id, transaction) | 1873 | return setAsUpdated({ sequelize: this.sequelize, table: 'video', id: this.id, transaction }) |
1875 | } | 1874 | } |
1876 | 1875 | ||
1877 | // --------------------------------------------------------------------------- | 1876 | // --------------------------------------------------------------------------- |
diff --git a/server/models/view/local-video-viewer.ts b/server/models/view/local-video-viewer.ts index 9d0d89a59..274117e86 100644 --- a/server/models/view/local-video-viewer.ts +++ b/server/models/view/local-video-viewer.ts | |||
@@ -21,6 +21,10 @@ import { LocalVideoViewerWatchSectionModel } from './local-video-viewer-watch-se | |||
21 | indexes: [ | 21 | indexes: [ |
22 | { | 22 | { |
23 | fields: [ 'videoId' ] | 23 | fields: [ 'videoId' ] |
24 | }, | ||
25 | { | ||
26 | fields: [ 'url' ], | ||
27 | unique: true | ||
24 | } | 28 | } |
25 | ] | 29 | ] |
26 | }) | 30 | }) |
diff --git a/server/tests/api/activitypub/cleaner.ts b/server/tests/api/activitypub/cleaner.ts index eb6779123..1c1495022 100644 --- a/server/tests/api/activitypub/cleaner.ts +++ b/server/tests/api/activitypub/cleaner.ts | |||
@@ -148,7 +148,7 @@ describe('Test AP cleaner', function () { | |||
148 | it('Should destroy server 3 internal shares and correctly clean them', async function () { | 148 | it('Should destroy server 3 internal shares and correctly clean them', async function () { |
149 | this.timeout(20000) | 149 | this.timeout(20000) |
150 | 150 | ||
151 | const preCount = await servers[0].sql.getCount('videoShare') | 151 | const preCount = await servers[0].sql.getVideoShareCount() |
152 | expect(preCount).to.equal(6) | 152 | expect(preCount).to.equal(6) |
153 | 153 | ||
154 | await servers[2].sql.deleteAll('videoShare') | 154 | await servers[2].sql.deleteAll('videoShare') |
@@ -156,7 +156,7 @@ describe('Test AP cleaner', function () { | |||
156 | await waitJobs(servers) | 156 | await waitJobs(servers) |
157 | 157 | ||
158 | // Still 6 because we don't have remote shares on local videos | 158 | // Still 6 because we don't have remote shares on local videos |
159 | const postCount = await servers[0].sql.getCount('videoShare') | 159 | const postCount = await servers[0].sql.getVideoShareCount() |
160 | expect(postCount).to.equal(6) | 160 | expect(postCount).to.equal(6) |
161 | }) | 161 | }) |
162 | 162 | ||
@@ -185,7 +185,7 @@ describe('Test AP cleaner', function () { | |||
185 | async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') { | 185 | async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') { |
186 | const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` + | 186 | const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` + |
187 | `INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'` | 187 | `INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'` |
188 | const res = await servers[0].sql.selectQuery(query) | 188 | const res = await servers[0].sql.selectQuery<{ url: string }>(query) |
189 | 189 | ||
190 | for (const rate of res) { | 190 | for (const rate of res) { |
191 | const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`) | 191 | const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`) |
@@ -231,7 +231,7 @@ describe('Test AP cleaner', function () { | |||
231 | const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` + | 231 | const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` + |
232 | `INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'` | 232 | `INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'` |
233 | 233 | ||
234 | const res = await servers[0].sql.selectQuery(query) | 234 | const res = await servers[0].sql.selectQuery<{ url: string, videoUUID: string }>(query) |
235 | 235 | ||
236 | for (const comment of res) { | 236 | for (const comment of res) { |
237 | const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`) | 237 | const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`) |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 3415625ca..93a3f3eb9 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -79,6 +79,7 @@ describe('Test config API validators', function () { | |||
79 | signup: { | 79 | signup: { |
80 | enabled: false, | 80 | enabled: false, |
81 | limit: 5, | 81 | limit: 5, |
82 | requiresApproval: false, | ||
82 | requiresEmailVerification: false, | 83 | requiresEmailVerification: false, |
83 | minimumAge: 16 | 84 | minimumAge: 16 |
84 | }, | 85 | }, |
@@ -313,6 +314,7 @@ describe('Test config API validators', function () { | |||
313 | signup: { | 314 | signup: { |
314 | enabled: true, | 315 | enabled: true, |
315 | limit: 5, | 316 | limit: 5, |
317 | requiresApproval: true, | ||
316 | requiresEmailVerification: true | 318 | requiresEmailVerification: true |
317 | } | 319 | } |
318 | } | 320 | } |
diff --git a/server/tests/api/check-params/contact-form.ts b/server/tests/api/check-params/contact-form.ts index 7968ef802..f0f8819b9 100644 --- a/server/tests/api/check-params/contact-form.ts +++ b/server/tests/api/check-params/contact-form.ts | |||
@@ -2,7 +2,14 @@ | |||
2 | 2 | ||
3 | import { MockSmtpServer } from '@server/tests/shared' | 3 | import { MockSmtpServer } from '@server/tests/shared' |
4 | import { HttpStatusCode } from '@shared/models' | 4 | import { HttpStatusCode } from '@shared/models' |
5 | import { cleanupTests, ContactFormCommand, createSingleServer, killallServers, PeerTubeServer } from '@shared/server-commands' | 5 | import { |
6 | cleanupTests, | ||
7 | ConfigCommand, | ||
8 | ContactFormCommand, | ||
9 | createSingleServer, | ||
10 | killallServers, | ||
11 | PeerTubeServer | ||
12 | } from '@shared/server-commands' | ||
6 | 13 | ||
7 | describe('Test contact form API validators', function () { | 14 | describe('Test contact form API validators', function () { |
8 | let server: PeerTubeServer | 15 | let server: PeerTubeServer |
@@ -38,7 +45,7 @@ describe('Test contact form API validators', function () { | |||
38 | await killallServers([ server ]) | 45 | await killallServers([ server ]) |
39 | 46 | ||
40 | // Contact form is disabled | 47 | // Contact form is disabled |
41 | await server.run({ smtp: { hostname: '127.0.0.1', port: emailPort }, contact_form: { enabled: false } }) | 48 | await server.run({ ...ConfigCommand.getEmailOverrideConfig(emailPort), contact_form: { enabled: false } }) |
42 | await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 }) | 49 | await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 }) |
43 | }) | 50 | }) |
44 | 51 | ||
@@ -48,7 +55,7 @@ describe('Test contact form API validators', function () { | |||
48 | await killallServers([ server ]) | 55 | await killallServers([ server ]) |
49 | 56 | ||
50 | // Email & contact form enabled | 57 | // Email & contact form enabled |
51 | await server.run({ smtp: { hostname: '127.0.0.1', port: emailPort } }) | 58 | await server.run(ConfigCommand.getEmailOverrideConfig(emailPort)) |
52 | 59 | ||
53 | await command.send({ ...defaultBody, fromEmail: 'badEmail', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 60 | await command.send({ ...defaultBody, fromEmail: 'badEmail', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
54 | await command.send({ ...defaultBody, fromEmail: 'badEmail@', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 61 | await command.send({ ...defaultBody, fromEmail: 'badEmail@', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 961093bb5..ddbcb42f8 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -15,6 +15,7 @@ import './metrics' | |||
15 | import './my-user' | 15 | import './my-user' |
16 | import './plugins' | 16 | import './plugins' |
17 | import './redundancy' | 17 | import './redundancy' |
18 | import './registrations' | ||
18 | import './search' | 19 | import './search' |
19 | import './services' | 20 | import './services' |
20 | import './transcoding' | 21 | import './transcoding' |
@@ -23,7 +24,7 @@ import './upload-quota' | |||
23 | import './user-notifications' | 24 | import './user-notifications' |
24 | import './user-subscriptions' | 25 | import './user-subscriptions' |
25 | import './users-admin' | 26 | import './users-admin' |
26 | import './users' | 27 | import './users-emails' |
27 | import './video-blacklist' | 28 | import './video-blacklist' |
28 | import './video-captions' | 29 | import './video-captions' |
29 | import './video-channel-syncs' | 30 | import './video-channel-syncs' |
diff --git a/server/tests/api/check-params/redundancy.ts b/server/tests/api/check-params/redundancy.ts index 908407b9a..73dfd489d 100644 --- a/server/tests/api/check-params/redundancy.ts +++ b/server/tests/api/check-params/redundancy.ts | |||
@@ -24,7 +24,7 @@ describe('Test server redundancy API validators', function () { | |||
24 | // --------------------------------------------------------------- | 24 | // --------------------------------------------------------------- |
25 | 25 | ||
26 | before(async function () { | 26 | before(async function () { |
27 | this.timeout(80000) | 27 | this.timeout(160000) |
28 | 28 | ||
29 | servers = await createMultipleServers(2) | 29 | servers = await createMultipleServers(2) |
30 | 30 | ||
diff --git a/server/tests/api/check-params/registrations.ts b/server/tests/api/check-params/registrations.ts new file mode 100644 index 000000000..fe16ebd93 --- /dev/null +++ b/server/tests/api/check-params/registrations.ts | |||
@@ -0,0 +1,433 @@ | |||
1 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared' | ||
2 | import { omit } from '@shared/core-utils' | ||
3 | import { HttpStatusCode, UserRole } from '@shared/models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makePostBodyRequest, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultAccountAvatar, | ||
11 | setDefaultChannelAvatar | ||
12 | } from '@shared/server-commands' | ||
13 | |||
14 | describe('Test registrations API validators', function () { | ||
15 | let server: PeerTubeServer | ||
16 | let userToken: string | ||
17 | let moderatorToken: string | ||
18 | |||
19 | // --------------------------------------------------------------- | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(30000) | ||
23 | |||
24 | server = await createSingleServer(1) | ||
25 | |||
26 | await setAccessTokensToServers([ server ]) | ||
27 | await setDefaultAccountAvatar([ server ]) | ||
28 | await setDefaultChannelAvatar([ server ]) | ||
29 | |||
30 | await server.config.enableSignup(false); | ||
31 | |||
32 | ({ token: moderatorToken } = await server.users.generate('moderator', UserRole.MODERATOR)); | ||
33 | ({ token: userToken } = await server.users.generate('user', UserRole.USER)) | ||
34 | }) | ||
35 | |||
36 | describe('Register', function () { | ||
37 | const registrationPath = '/api/v1/users/register' | ||
38 | const registrationRequestPath = '/api/v1/users/registrations/request' | ||
39 | |||
40 | const baseCorrectParams = { | ||
41 | username: 'user3', | ||
42 | displayName: 'super user', | ||
43 | email: 'test3@example.com', | ||
44 | password: 'my super password', | ||
45 | registrationReason: 'my super registration reason' | ||
46 | } | ||
47 | |||
48 | describe('When registering a new user or requesting user registration', function () { | ||
49 | |||
50 | async function check (fields: any, expectedStatus = HttpStatusCode.BAD_REQUEST_400) { | ||
51 | await server.config.enableSignup(false) | ||
52 | await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus }) | ||
53 | |||
54 | await server.config.enableSignup(true) | ||
55 | await makePostBodyRequest({ url: server.url, path: registrationRequestPath, fields, expectedStatus }) | ||
56 | } | ||
57 | |||
58 | it('Should fail with a too small username', async function () { | ||
59 | const fields = { ...baseCorrectParams, username: '' } | ||
60 | |||
61 | await check(fields) | ||
62 | }) | ||
63 | |||
64 | it('Should fail with a too long username', async function () { | ||
65 | const fields = { ...baseCorrectParams, username: 'super'.repeat(50) } | ||
66 | |||
67 | await check(fields) | ||
68 | }) | ||
69 | |||
70 | it('Should fail with an incorrect username', async function () { | ||
71 | const fields = { ...baseCorrectParams, username: 'my username' } | ||
72 | |||
73 | await check(fields) | ||
74 | }) | ||
75 | |||
76 | it('Should fail with a missing email', async function () { | ||
77 | const fields = omit(baseCorrectParams, [ 'email' ]) | ||
78 | |||
79 | await check(fields) | ||
80 | }) | ||
81 | |||
82 | it('Should fail with an invalid email', async function () { | ||
83 | const fields = { ...baseCorrectParams, email: 'test_example.com' } | ||
84 | |||
85 | await check(fields) | ||
86 | }) | ||
87 | |||
88 | it('Should fail with a too small password', async function () { | ||
89 | const fields = { ...baseCorrectParams, password: 'bla' } | ||
90 | |||
91 | await check(fields) | ||
92 | }) | ||
93 | |||
94 | it('Should fail with a too long password', async function () { | ||
95 | const fields = { ...baseCorrectParams, password: 'super'.repeat(61) } | ||
96 | |||
97 | await check(fields) | ||
98 | }) | ||
99 | |||
100 | it('Should fail if we register a user with the same username', async function () { | ||
101 | const fields = { ...baseCorrectParams, username: 'root' } | ||
102 | |||
103 | await check(fields, HttpStatusCode.CONFLICT_409) | ||
104 | }) | ||
105 | |||
106 | it('Should fail with a "peertube" username', async function () { | ||
107 | const fields = { ...baseCorrectParams, username: 'peertube' } | ||
108 | |||
109 | await check(fields, HttpStatusCode.CONFLICT_409) | ||
110 | }) | ||
111 | |||
112 | it('Should fail if we register a user with the same email', async function () { | ||
113 | const fields = { ...baseCorrectParams, email: 'admin' + server.internalServerNumber + '@example.com' } | ||
114 | |||
115 | await check(fields, HttpStatusCode.CONFLICT_409) | ||
116 | }) | ||
117 | |||
118 | it('Should fail with a bad display name', async function () { | ||
119 | const fields = { ...baseCorrectParams, displayName: 'a'.repeat(150) } | ||
120 | |||
121 | await check(fields) | ||
122 | }) | ||
123 | |||
124 | it('Should fail with a bad channel name', async function () { | ||
125 | const fields = { ...baseCorrectParams, channel: { name: '[]azf', displayName: 'toto' } } | ||
126 | |||
127 | await check(fields) | ||
128 | }) | ||
129 | |||
130 | it('Should fail with a bad channel display name', async function () { | ||
131 | const fields = { ...baseCorrectParams, channel: { name: 'toto', displayName: '' } } | ||
132 | |||
133 | await check(fields) | ||
134 | }) | ||
135 | |||
136 | it('Should fail with a channel name that is the same as username', async function () { | ||
137 | const source = { username: 'super_user', channel: { name: 'super_user', displayName: 'display name' } } | ||
138 | const fields = { ...baseCorrectParams, ...source } | ||
139 | |||
140 | await check(fields) | ||
141 | }) | ||
142 | |||
143 | it('Should fail with an existing channel', async function () { | ||
144 | const attributes = { name: 'existing_channel', displayName: 'hello', description: 'super description' } | ||
145 | await server.channels.create({ attributes }) | ||
146 | |||
147 | const fields = { ...baseCorrectParams, channel: { name: 'existing_channel', displayName: 'toto' } } | ||
148 | |||
149 | await check(fields, HttpStatusCode.CONFLICT_409) | ||
150 | }) | ||
151 | |||
152 | it('Should fail on a server with registration disabled', async function () { | ||
153 | this.timeout(60000) | ||
154 | |||
155 | await server.config.updateExistingSubConfig({ | ||
156 | newConfig: { | ||
157 | signup: { | ||
158 | enabled: false | ||
159 | } | ||
160 | } | ||
161 | }) | ||
162 | |||
163 | await server.registrations.register({ username: 'user4', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
164 | await server.registrations.requestRegistration({ | ||
165 | username: 'user4', | ||
166 | registrationReason: 'reason', | ||
167 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
168 | }) | ||
169 | }) | ||
170 | |||
171 | it('Should fail if the user limit is reached', async function () { | ||
172 | this.timeout(60000) | ||
173 | |||
174 | const { total } = await server.users.list() | ||
175 | |||
176 | await server.config.enableSignup(false, total) | ||
177 | await server.registrations.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
178 | |||
179 | await server.config.enableSignup(true, total) | ||
180 | await server.registrations.requestRegistration({ | ||
181 | username: 'user42', | ||
182 | registrationReason: 'reason', | ||
183 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
184 | }) | ||
185 | }) | ||
186 | |||
187 | it('Should succeed if the user limit is not reached', async function () { | ||
188 | this.timeout(60000) | ||
189 | |||
190 | const { total } = await server.users.list() | ||
191 | |||
192 | await server.config.enableSignup(false, total + 1) | ||
193 | await server.registrations.register({ username: 'user43', expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
194 | |||
195 | await server.config.enableSignup(true, total + 2) | ||
196 | await server.registrations.requestRegistration({ | ||
197 | username: 'user44', | ||
198 | registrationReason: 'reason', | ||
199 | expectedStatus: HttpStatusCode.OK_200 | ||
200 | }) | ||
201 | }) | ||
202 | }) | ||
203 | |||
204 | describe('On direct registration', function () { | ||
205 | |||
206 | it('Should succeed with the correct params', async function () { | ||
207 | await server.config.enableSignup(false) | ||
208 | |||
209 | const fields = { | ||
210 | username: 'user_direct_1', | ||
211 | displayName: 'super user direct 1', | ||
212 | email: 'user_direct_1@example.com', | ||
213 | password: 'my super password', | ||
214 | channel: { name: 'super_user_direct_1_channel', displayName: 'super user direct 1 channel' } | ||
215 | } | ||
216 | |||
217 | await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
218 | }) | ||
219 | |||
220 | it('Should fail if the instance requires approval', async function () { | ||
221 | this.timeout(60000) | ||
222 | |||
223 | await server.config.enableSignup(true) | ||
224 | await server.registrations.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
225 | }) | ||
226 | }) | ||
227 | |||
228 | describe('On registration request', function () { | ||
229 | |||
230 | before(async function () { | ||
231 | this.timeout(60000) | ||
232 | |||
233 | await server.config.enableSignup(true) | ||
234 | }) | ||
235 | |||
236 | it('Should fail with an invalid registration reason', async function () { | ||
237 | for (const registrationReason of [ '', 't', 't'.repeat(5000) ]) { | ||
238 | await server.registrations.requestRegistration({ | ||
239 | username: 'user_request_1', | ||
240 | registrationReason, | ||
241 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
242 | }) | ||
243 | } | ||
244 | }) | ||
245 | |||
246 | it('Should succeed with the correct params', async function () { | ||
247 | await server.registrations.requestRegistration({ | ||
248 | username: 'user_request_2', | ||
249 | registrationReason: 'tt', | ||
250 | channel: { | ||
251 | displayName: 'my user request 2 channel', | ||
252 | name: 'user_request_2_channel' | ||
253 | } | ||
254 | }) | ||
255 | }) | ||
256 | |||
257 | it('Should fail if the user is already awaiting registration approval', async function () { | ||
258 | await server.registrations.requestRegistration({ | ||
259 | username: 'user_request_2', | ||
260 | registrationReason: 'tt', | ||
261 | channel: { | ||
262 | displayName: 'my user request 42 channel', | ||
263 | name: 'user_request_42_channel' | ||
264 | }, | ||
265 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
266 | }) | ||
267 | }) | ||
268 | |||
269 | it('Should fail if the channel is already awaiting registration approval', async function () { | ||
270 | await server.registrations.requestRegistration({ | ||
271 | username: 'user42', | ||
272 | registrationReason: 'tt', | ||
273 | channel: { | ||
274 | displayName: 'my user request 2 channel', | ||
275 | name: 'user_request_2_channel' | ||
276 | }, | ||
277 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
278 | }) | ||
279 | }) | ||
280 | |||
281 | it('Should fail if the instance does not require approval', async function () { | ||
282 | this.timeout(60000) | ||
283 | |||
284 | await server.config.enableSignup(false) | ||
285 | |||
286 | await server.registrations.requestRegistration({ | ||
287 | username: 'user42', | ||
288 | registrationReason: 'toto', | ||
289 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
290 | }) | ||
291 | }) | ||
292 | }) | ||
293 | }) | ||
294 | |||
295 | describe('Registrations accept/reject', function () { | ||
296 | let id1: number | ||
297 | let id2: number | ||
298 | |||
299 | before(async function () { | ||
300 | this.timeout(60000) | ||
301 | |||
302 | await server.config.enableSignup(true); | ||
303 | |||
304 | ({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_2', registrationReason: 'toto' })); | ||
305 | ({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_3', registrationReason: 'toto' })) | ||
306 | }) | ||
307 | |||
308 | it('Should fail to accept/reject registration without token', async function () { | ||
309 | const options = { id: id1, moderationResponse: 'tt', token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 } | ||
310 | await server.registrations.accept(options) | ||
311 | await server.registrations.reject(options) | ||
312 | }) | ||
313 | |||
314 | it('Should fail to accept/reject registration with a non moderator user', async function () { | ||
315 | const options = { id: id1, moderationResponse: 'tt', token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 } | ||
316 | await server.registrations.accept(options) | ||
317 | await server.registrations.reject(options) | ||
318 | }) | ||
319 | |||
320 | it('Should fail to accept/reject registration with a bad registration id', async function () { | ||
321 | { | ||
322 | const options = { id: 't' as any, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } | ||
323 | await server.registrations.accept(options) | ||
324 | await server.registrations.reject(options) | ||
325 | } | ||
326 | |||
327 | { | ||
328 | const options = { id: 42, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 } | ||
329 | await server.registrations.accept(options) | ||
330 | await server.registrations.reject(options) | ||
331 | } | ||
332 | }) | ||
333 | |||
334 | it('Should fail to accept/reject registration with a bad moderation resposne', async function () { | ||
335 | for (const moderationResponse of [ '', 't', 't'.repeat(5000) ]) { | ||
336 | const options = { id: id1, moderationResponse, token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } | ||
337 | await server.registrations.accept(options) | ||
338 | await server.registrations.reject(options) | ||
339 | } | ||
340 | }) | ||
341 | |||
342 | it('Should succeed to accept a registration', async function () { | ||
343 | await server.registrations.accept({ id: id1, moderationResponse: 'tt', token: moderatorToken }) | ||
344 | }) | ||
345 | |||
346 | it('Should succeed to reject a registration', async function () { | ||
347 | await server.registrations.reject({ id: id2, moderationResponse: 'tt', token: moderatorToken }) | ||
348 | }) | ||
349 | |||
350 | it('Should fail to accept/reject a registration that was already accepted/rejected', async function () { | ||
351 | for (const id of [ id1, id2 ]) { | ||
352 | const options = { id, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.CONFLICT_409 } | ||
353 | await server.registrations.accept(options) | ||
354 | await server.registrations.reject(options) | ||
355 | } | ||
356 | }) | ||
357 | }) | ||
358 | |||
359 | describe('Registrations deletion', function () { | ||
360 | let id1: number | ||
361 | let id2: number | ||
362 | let id3: number | ||
363 | |||
364 | before(async function () { | ||
365 | ({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_4', registrationReason: 'toto' })); | ||
366 | ({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_5', registrationReason: 'toto' })); | ||
367 | ({ id: id3 } = await server.registrations.requestRegistration({ username: 'request_6', registrationReason: 'toto' })) | ||
368 | |||
369 | await server.registrations.accept({ id: id2, moderationResponse: 'tt' }) | ||
370 | await server.registrations.reject({ id: id3, moderationResponse: 'tt' }) | ||
371 | }) | ||
372 | |||
373 | it('Should fail to delete registration without token', async function () { | ||
374 | await server.registrations.delete({ id: id1, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
375 | }) | ||
376 | |||
377 | it('Should fail to delete registration with a non moderator user', async function () { | ||
378 | await server.registrations.delete({ id: id1, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
379 | }) | ||
380 | |||
381 | it('Should fail to delete registration with a bad registration id', async function () { | ||
382 | await server.registrations.delete({ id: 't' as any, token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
383 | await server.registrations.delete({ id: 42, token: moderatorToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
384 | }) | ||
385 | |||
386 | it('Should succeed with the correct params', async function () { | ||
387 | await server.registrations.delete({ id: id1, token: moderatorToken }) | ||
388 | await server.registrations.delete({ id: id2, token: moderatorToken }) | ||
389 | await server.registrations.delete({ id: id3, token: moderatorToken }) | ||
390 | }) | ||
391 | }) | ||
392 | |||
393 | describe('Listing registrations', function () { | ||
394 | const path = '/api/v1/users/registrations' | ||
395 | |||
396 | it('Should fail with a bad start pagination', async function () { | ||
397 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
398 | }) | ||
399 | |||
400 | it('Should fail with a bad count pagination', async function () { | ||
401 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
402 | }) | ||
403 | |||
404 | it('Should fail with an incorrect sort', async function () { | ||
405 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
406 | }) | ||
407 | |||
408 | it('Should fail with a non authenticated user', async function () { | ||
409 | await server.registrations.list({ | ||
410 | token: null, | ||
411 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
412 | }) | ||
413 | }) | ||
414 | |||
415 | it('Should fail with a non admin user', async function () { | ||
416 | await server.registrations.list({ | ||
417 | token: userToken, | ||
418 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
419 | }) | ||
420 | }) | ||
421 | |||
422 | it('Should succeed with the correct params', async function () { | ||
423 | await server.registrations.list({ | ||
424 | token: moderatorToken, | ||
425 | search: 'toto' | ||
426 | }) | ||
427 | }) | ||
428 | }) | ||
429 | |||
430 | after(async function () { | ||
431 | await cleanupTests([ server ]) | ||
432 | }) | ||
433 | }) | ||
diff --git a/server/tests/api/check-params/upload-quota.ts b/server/tests/api/check-params/upload-quota.ts index 70e6f4af9..fdc711bd5 100644 --- a/server/tests/api/check-params/upload-quota.ts +++ b/server/tests/api/check-params/upload-quota.ts | |||
@@ -42,7 +42,7 @@ describe('Test upload quota', function () { | |||
42 | this.timeout(30000) | 42 | this.timeout(30000) |
43 | 43 | ||
44 | const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } | 44 | const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } |
45 | await server.users.register(user) | 45 | await server.registrations.register(user) |
46 | const userToken = await server.login.getAccessToken(user) | 46 | const userToken = await server.login.getAccessToken(user) |
47 | 47 | ||
48 | const attributes = { fixture: 'video_short2.webm' } | 48 | const attributes = { fixture: 'video_short2.webm' } |
@@ -57,7 +57,7 @@ describe('Test upload quota', function () { | |||
57 | this.timeout(30000) | 57 | this.timeout(30000) |
58 | 58 | ||
59 | const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } | 59 | const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } |
60 | await server.users.register(user) | 60 | await server.registrations.register(user) |
61 | const userToken = await server.login.getAccessToken(user) | 61 | const userToken = await server.login.getAccessToken(user) |
62 | 62 | ||
63 | const attributes = { fixture: 'video_short2.webm' } | 63 | const attributes = { fixture: 'video_short2.webm' } |
diff --git a/server/tests/api/check-params/users-admin.ts b/server/tests/api/check-params/users-admin.ts index 7ba709c4a..be2496bb4 100644 --- a/server/tests/api/check-params/users-admin.ts +++ b/server/tests/api/check-params/users-admin.ts | |||
@@ -5,6 +5,7 @@ import { omit } from '@shared/core-utils' | |||
5 | import { HttpStatusCode, UserAdminFlag, UserRole } from '@shared/models' | 5 | import { HttpStatusCode, UserAdminFlag, UserRole } from '@shared/models' |
6 | import { | 6 | import { |
7 | cleanupTests, | 7 | cleanupTests, |
8 | ConfigCommand, | ||
8 | createSingleServer, | 9 | createSingleServer, |
9 | killallServers, | 10 | killallServers, |
10 | makeGetRequest, | 11 | makeGetRequest, |
@@ -156,13 +157,7 @@ describe('Test users admin API validators', function () { | |||
156 | 157 | ||
157 | await killallServers([ server ]) | 158 | await killallServers([ server ]) |
158 | 159 | ||
159 | const config = { | 160 | await server.run(ConfigCommand.getEmailOverrideConfig(emailPort)) |
160 | smtp: { | ||
161 | hostname: '127.0.0.1', | ||
162 | port: emailPort | ||
163 | } | ||
164 | } | ||
165 | await server.run(config) | ||
166 | 161 | ||
167 | const fields = { | 162 | const fields = { |
168 | ...baseCorrectParams, | 163 | ...baseCorrectParams, |
diff --git a/server/tests/api/check-params/users-emails.ts b/server/tests/api/check-params/users-emails.ts new file mode 100644 index 000000000..8cfb1d15f --- /dev/null +++ b/server/tests/api/check-params/users-emails.ts | |||
@@ -0,0 +1,119 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | import { MockSmtpServer } from '@server/tests/shared' | ||
3 | import { HttpStatusCode, UserRole } from '@shared/models' | ||
4 | import { cleanupTests, createSingleServer, makePostBodyRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
5 | |||
6 | describe('Test users API validators', function () { | ||
7 | let server: PeerTubeServer | ||
8 | |||
9 | // --------------------------------------------------------------- | ||
10 | |||
11 | before(async function () { | ||
12 | this.timeout(30000) | ||
13 | |||
14 | server = await createSingleServer(1, { | ||
15 | rates_limit: { | ||
16 | ask_send_email: { | ||
17 | max: 10 | ||
18 | } | ||
19 | } | ||
20 | }) | ||
21 | |||
22 | await setAccessTokensToServers([ server ]) | ||
23 | await server.config.enableSignup(true) | ||
24 | |||
25 | await server.users.generate('moderator2', UserRole.MODERATOR) | ||
26 | |||
27 | await server.registrations.requestRegistration({ | ||
28 | username: 'request1', | ||
29 | registrationReason: 'tt' | ||
30 | }) | ||
31 | }) | ||
32 | |||
33 | describe('When asking a password reset', function () { | ||
34 | const path = '/api/v1/users/ask-reset-password' | ||
35 | |||
36 | it('Should fail with a missing email', async function () { | ||
37 | const fields = {} | ||
38 | |||
39 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
40 | }) | ||
41 | |||
42 | it('Should fail with an invalid email', async function () { | ||
43 | const fields = { email: 'hello' } | ||
44 | |||
45 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
46 | }) | ||
47 | |||
48 | it('Should success with the correct params', async function () { | ||
49 | const fields = { email: 'admin@example.com' } | ||
50 | |||
51 | await makePostBodyRequest({ | ||
52 | url: server.url, | ||
53 | path, | ||
54 | fields, | ||
55 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
56 | }) | ||
57 | }) | ||
58 | }) | ||
59 | |||
60 | describe('When asking for an account verification email', function () { | ||
61 | const path = '/api/v1/users/ask-send-verify-email' | ||
62 | |||
63 | it('Should fail with a missing email', async function () { | ||
64 | const fields = {} | ||
65 | |||
66 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
67 | }) | ||
68 | |||
69 | it('Should fail with an invalid email', async function () { | ||
70 | const fields = { email: 'hello' } | ||
71 | |||
72 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
73 | }) | ||
74 | |||
75 | it('Should succeed with the correct params', async function () { | ||
76 | const fields = { email: 'admin@example.com' } | ||
77 | |||
78 | await makePostBodyRequest({ | ||
79 | url: server.url, | ||
80 | path, | ||
81 | fields, | ||
82 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
83 | }) | ||
84 | }) | ||
85 | }) | ||
86 | |||
87 | describe('When asking for a registration verification email', function () { | ||
88 | const path = '/api/v1/users/registrations/ask-send-verify-email' | ||
89 | |||
90 | it('Should fail with a missing email', async function () { | ||
91 | const fields = {} | ||
92 | |||
93 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
94 | }) | ||
95 | |||
96 | it('Should fail with an invalid email', async function () { | ||
97 | const fields = { email: 'hello' } | ||
98 | |||
99 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
100 | }) | ||
101 | |||
102 | it('Should succeed with the correct params', async function () { | ||
103 | const fields = { email: 'request1@example.com' } | ||
104 | |||
105 | await makePostBodyRequest({ | ||
106 | url: server.url, | ||
107 | path, | ||
108 | fields, | ||
109 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
110 | }) | ||
111 | }) | ||
112 | }) | ||
113 | |||
114 | after(async function () { | ||
115 | MockSmtpServer.Instance.kill() | ||
116 | |||
117 | await cleanupTests([ server ]) | ||
118 | }) | ||
119 | }) | ||
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts deleted file mode 100644 index 7acfd8c2c..000000000 --- a/server/tests/api/check-params/users.ts +++ /dev/null | |||
@@ -1,255 +0,0 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | import { MockSmtpServer } from '@server/tests/shared' | ||
3 | import { omit } from '@shared/core-utils' | ||
4 | import { HttpStatusCode, UserRole } from '@shared/models' | ||
5 | import { cleanupTests, createSingleServer, makePostBodyRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
6 | |||
7 | describe('Test users API validators', function () { | ||
8 | const path = '/api/v1/users/' | ||
9 | let server: PeerTubeServer | ||
10 | let serverWithRegistrationDisabled: PeerTubeServer | ||
11 | |||
12 | // --------------------------------------------------------------- | ||
13 | |||
14 | before(async function () { | ||
15 | this.timeout(30000) | ||
16 | |||
17 | const res = await Promise.all([ | ||
18 | createSingleServer(1, { signup: { limit: 3 } }), | ||
19 | createSingleServer(2) | ||
20 | ]) | ||
21 | |||
22 | server = res[0] | ||
23 | serverWithRegistrationDisabled = res[1] | ||
24 | |||
25 | await setAccessTokensToServers([ server ]) | ||
26 | |||
27 | await server.users.generate('moderator2', UserRole.MODERATOR) | ||
28 | }) | ||
29 | |||
30 | describe('When registering a new user', function () { | ||
31 | const registrationPath = path + '/register' | ||
32 | const baseCorrectParams = { | ||
33 | username: 'user3', | ||
34 | displayName: 'super user', | ||
35 | email: 'test3@example.com', | ||
36 | password: 'my super password' | ||
37 | } | ||
38 | |||
39 | it('Should fail with a too small username', async function () { | ||
40 | const fields = { ...baseCorrectParams, username: '' } | ||
41 | |||
42 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
43 | }) | ||
44 | |||
45 | it('Should fail with a too long username', async function () { | ||
46 | const fields = { ...baseCorrectParams, username: 'super'.repeat(50) } | ||
47 | |||
48 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
49 | }) | ||
50 | |||
51 | it('Should fail with an incorrect username', async function () { | ||
52 | const fields = { ...baseCorrectParams, username: 'my username' } | ||
53 | |||
54 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
55 | }) | ||
56 | |||
57 | it('Should fail with a missing email', async function () { | ||
58 | const fields = omit(baseCorrectParams, [ 'email' ]) | ||
59 | |||
60 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
61 | }) | ||
62 | |||
63 | it('Should fail with an invalid email', async function () { | ||
64 | const fields = { ...baseCorrectParams, email: 'test_example.com' } | ||
65 | |||
66 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
67 | }) | ||
68 | |||
69 | it('Should fail with a too small password', async function () { | ||
70 | const fields = { ...baseCorrectParams, password: 'bla' } | ||
71 | |||
72 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
73 | }) | ||
74 | |||
75 | it('Should fail with a too long password', async function () { | ||
76 | const fields = { ...baseCorrectParams, password: 'super'.repeat(61) } | ||
77 | |||
78 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
79 | }) | ||
80 | |||
81 | it('Should fail if we register a user with the same username', async function () { | ||
82 | const fields = { ...baseCorrectParams, username: 'root' } | ||
83 | |||
84 | await makePostBodyRequest({ | ||
85 | url: server.url, | ||
86 | path: registrationPath, | ||
87 | token: server.accessToken, | ||
88 | fields, | ||
89 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
90 | }) | ||
91 | }) | ||
92 | |||
93 | it('Should fail with a "peertube" username', async function () { | ||
94 | const fields = { ...baseCorrectParams, username: 'peertube' } | ||
95 | |||
96 | await makePostBodyRequest({ | ||
97 | url: server.url, | ||
98 | path: registrationPath, | ||
99 | token: server.accessToken, | ||
100 | fields, | ||
101 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
102 | }) | ||
103 | }) | ||
104 | |||
105 | it('Should fail if we register a user with the same email', async function () { | ||
106 | const fields = { ...baseCorrectParams, email: 'admin' + server.internalServerNumber + '@example.com' } | ||
107 | |||
108 | await makePostBodyRequest({ | ||
109 | url: server.url, | ||
110 | path: registrationPath, | ||
111 | token: server.accessToken, | ||
112 | fields, | ||
113 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
114 | }) | ||
115 | }) | ||
116 | |||
117 | it('Should fail with a bad display name', async function () { | ||
118 | const fields = { ...baseCorrectParams, displayName: 'a'.repeat(150) } | ||
119 | |||
120 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
121 | }) | ||
122 | |||
123 | it('Should fail with a bad channel name', async function () { | ||
124 | const fields = { ...baseCorrectParams, channel: { name: '[]azf', displayName: 'toto' } } | ||
125 | |||
126 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
127 | }) | ||
128 | |||
129 | it('Should fail with a bad channel display name', async function () { | ||
130 | const fields = { ...baseCorrectParams, channel: { name: 'toto', displayName: '' } } | ||
131 | |||
132 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
133 | }) | ||
134 | |||
135 | it('Should fail with a channel name that is the same as username', async function () { | ||
136 | const source = { username: 'super_user', channel: { name: 'super_user', displayName: 'display name' } } | ||
137 | const fields = { ...baseCorrectParams, ...source } | ||
138 | |||
139 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
140 | }) | ||
141 | |||
142 | it('Should fail with an existing channel', async function () { | ||
143 | const attributes = { name: 'existing_channel', displayName: 'hello', description: 'super description' } | ||
144 | await server.channels.create({ attributes }) | ||
145 | |||
146 | const fields = { ...baseCorrectParams, channel: { name: 'existing_channel', displayName: 'toto' } } | ||
147 | |||
148 | await makePostBodyRequest({ | ||
149 | url: server.url, | ||
150 | path: registrationPath, | ||
151 | token: server.accessToken, | ||
152 | fields, | ||
153 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
154 | }) | ||
155 | }) | ||
156 | |||
157 | it('Should succeed with the correct params', async function () { | ||
158 | const fields = { ...baseCorrectParams, channel: { name: 'super_channel', displayName: 'toto' } } | ||
159 | |||
160 | await makePostBodyRequest({ | ||
161 | url: server.url, | ||
162 | path: registrationPath, | ||
163 | token: server.accessToken, | ||
164 | fields, | ||
165 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
166 | }) | ||
167 | }) | ||
168 | |||
169 | it('Should fail on a server with registration disabled', async function () { | ||
170 | const fields = { | ||
171 | username: 'user4', | ||
172 | email: 'test4@example.com', | ||
173 | password: 'my super password 4' | ||
174 | } | ||
175 | |||
176 | await makePostBodyRequest({ | ||
177 | url: serverWithRegistrationDisabled.url, | ||
178 | path: registrationPath, | ||
179 | token: serverWithRegistrationDisabled.accessToken, | ||
180 | fields, | ||
181 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
182 | }) | ||
183 | }) | ||
184 | }) | ||
185 | |||
186 | describe('When registering multiple users on a server with users limit', function () { | ||
187 | |||
188 | it('Should fail when after 3 registrations', async function () { | ||
189 | await server.users.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
190 | }) | ||
191 | |||
192 | }) | ||
193 | |||
194 | describe('When asking a password reset', function () { | ||
195 | const path = '/api/v1/users/ask-reset-password' | ||
196 | |||
197 | it('Should fail with a missing email', async function () { | ||
198 | const fields = {} | ||
199 | |||
200 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
201 | }) | ||
202 | |||
203 | it('Should fail with an invalid email', async function () { | ||
204 | const fields = { email: 'hello' } | ||
205 | |||
206 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
207 | }) | ||
208 | |||
209 | it('Should success with the correct params', async function () { | ||
210 | const fields = { email: 'admin@example.com' } | ||
211 | |||
212 | await makePostBodyRequest({ | ||
213 | url: server.url, | ||
214 | path, | ||
215 | token: server.accessToken, | ||
216 | fields, | ||
217 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
218 | }) | ||
219 | }) | ||
220 | }) | ||
221 | |||
222 | describe('When asking for an account verification email', function () { | ||
223 | const path = '/api/v1/users/ask-send-verify-email' | ||
224 | |||
225 | it('Should fail with a missing email', async function () { | ||
226 | const fields = {} | ||
227 | |||
228 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
229 | }) | ||
230 | |||
231 | it('Should fail with an invalid email', async function () { | ||
232 | const fields = { email: 'hello' } | ||
233 | |||
234 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
235 | }) | ||
236 | |||
237 | it('Should succeed with the correct params', async function () { | ||
238 | const fields = { email: 'admin@example.com' } | ||
239 | |||
240 | await makePostBodyRequest({ | ||
241 | url: server.url, | ||
242 | path, | ||
243 | token: server.accessToken, | ||
244 | fields, | ||
245 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
246 | }) | ||
247 | }) | ||
248 | }) | ||
249 | |||
250 | after(async function () { | ||
251 | MockSmtpServer.Instance.kill() | ||
252 | |||
253 | await cleanupTests([ server, serverWithRegistrationDisabled ]) | ||
254 | }) | ||
255 | }) | ||
diff --git a/server/tests/api/live/live-fast-restream.ts b/server/tests/api/live/live-fast-restream.ts index c0bb8d529..f6959b83c 100644 --- a/server/tests/api/live/live-fast-restream.ts +++ b/server/tests/api/live/live-fast-restream.ts | |||
@@ -78,9 +78,15 @@ describe('Fast restream in live', function () { | |||
78 | const video = await server.videos.get({ id: liveId }) | 78 | const video = await server.videos.get({ id: liveId }) |
79 | expect(video.streamingPlaylists).to.have.lengthOf(1) | 79 | expect(video.streamingPlaylists).to.have.lengthOf(1) |
80 | 80 | ||
81 | await server.live.getSegmentFile({ videoUUID: liveId, segment: 0, playlistNumber: 0 }) | 81 | try { |
82 | await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | 82 | await server.live.getSegmentFile({ videoUUID: liveId, segment: 0, playlistNumber: 0 }) |
83 | await makeRawRequest({ url: video.streamingPlaylists[0].segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | 83 | await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) |
84 | await makeRawRequest({ url: video.streamingPlaylists[0].segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
85 | } catch (err) { | ||
86 | // FIXME: try to debug error in CI "Unexpected end of JSON input" | ||
87 | console.error(err) | ||
88 | throw err | ||
89 | } | ||
84 | 90 | ||
85 | await wait(100) | 91 | await wait(100) |
86 | } | 92 | } |
@@ -129,7 +135,7 @@ describe('Fast restream in live', function () { | |||
129 | await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' }) | 135 | await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' }) |
130 | }) | 136 | }) |
131 | 137 | ||
132 | it('Should correctly fast reastream in a permanent live with and without save replay', async function () { | 138 | it('Should correctly fast restream in a permanent live with and without save replay', async function () { |
133 | this.timeout(480000) | 139 | this.timeout(480000) |
134 | 140 | ||
135 | // A test can take a long time, so prefer to run them in parallel | 141 | // A test can take a long time, so prefer to run them in parallel |
diff --git a/server/tests/api/notifications/index.ts b/server/tests/api/notifications/index.ts index 8caa30a3d..c0216b74f 100644 --- a/server/tests/api/notifications/index.ts +++ b/server/tests/api/notifications/index.ts | |||
@@ -2,4 +2,5 @@ import './admin-notifications' | |||
2 | import './comments-notifications' | 2 | import './comments-notifications' |
3 | import './moderation-notifications' | 3 | import './moderation-notifications' |
4 | import './notifications-api' | 4 | import './notifications-api' |
5 | import './registrations-notifications' | ||
5 | import './user-notifications' | 6 | import './user-notifications' |
diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts index b127a7a31..bb11a08aa 100644 --- a/server/tests/api/notifications/moderation-notifications.ts +++ b/server/tests/api/notifications/moderation-notifications.ts | |||
@@ -11,7 +11,6 @@ import { | |||
11 | checkNewInstanceFollower, | 11 | checkNewInstanceFollower, |
12 | checkNewVideoAbuseForModerators, | 12 | checkNewVideoAbuseForModerators, |
13 | checkNewVideoFromSubscription, | 13 | checkNewVideoFromSubscription, |
14 | checkUserRegistered, | ||
15 | checkVideoAutoBlacklistForModerators, | 14 | checkVideoAutoBlacklistForModerators, |
16 | checkVideoIsPublished, | 15 | checkVideoIsPublished, |
17 | MockInstancesIndex, | 16 | MockInstancesIndex, |
@@ -34,7 +33,7 @@ describe('Test moderation notifications', function () { | |||
34 | let emails: object[] = [] | 33 | let emails: object[] = [] |
35 | 34 | ||
36 | before(async function () { | 35 | before(async function () { |
37 | this.timeout(120000) | 36 | this.timeout(50000) |
38 | 37 | ||
39 | const res = await prepareNotificationsTest(3) | 38 | const res = await prepareNotificationsTest(3) |
40 | emails = res.emails | 39 | emails = res.emails |
@@ -60,7 +59,7 @@ describe('Test moderation notifications', function () { | |||
60 | }) | 59 | }) |
61 | 60 | ||
62 | it('Should not send a notification to moderators on local abuse reported by an admin', async function () { | 61 | it('Should not send a notification to moderators on local abuse reported by an admin', async function () { |
63 | this.timeout(20000) | 62 | this.timeout(50000) |
64 | 63 | ||
65 | const name = 'video for abuse ' + buildUUID() | 64 | const name = 'video for abuse ' + buildUUID() |
66 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | 65 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) |
@@ -72,7 +71,7 @@ describe('Test moderation notifications', function () { | |||
72 | }) | 71 | }) |
73 | 72 | ||
74 | it('Should send a notification to moderators on local video abuse', async function () { | 73 | it('Should send a notification to moderators on local video abuse', async function () { |
75 | this.timeout(20000) | 74 | this.timeout(50000) |
76 | 75 | ||
77 | const name = 'video for abuse ' + buildUUID() | 76 | const name = 'video for abuse ' + buildUUID() |
78 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | 77 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) |
@@ -84,7 +83,7 @@ describe('Test moderation notifications', function () { | |||
84 | }) | 83 | }) |
85 | 84 | ||
86 | it('Should send a notification to moderators on remote video abuse', async function () { | 85 | it('Should send a notification to moderators on remote video abuse', async function () { |
87 | this.timeout(20000) | 86 | this.timeout(50000) |
88 | 87 | ||
89 | const name = 'video for abuse ' + buildUUID() | 88 | const name = 'video for abuse ' + buildUUID() |
90 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | 89 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) |
@@ -99,7 +98,7 @@ describe('Test moderation notifications', function () { | |||
99 | }) | 98 | }) |
100 | 99 | ||
101 | it('Should send a notification to moderators on local comment abuse', async function () { | 100 | it('Should send a notification to moderators on local comment abuse', async function () { |
102 | this.timeout(20000) | 101 | this.timeout(50000) |
103 | 102 | ||
104 | const name = 'video for abuse ' + buildUUID() | 103 | const name = 'video for abuse ' + buildUUID() |
105 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | 104 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) |
@@ -118,7 +117,7 @@ describe('Test moderation notifications', function () { | |||
118 | }) | 117 | }) |
119 | 118 | ||
120 | it('Should send a notification to moderators on remote comment abuse', async function () { | 119 | it('Should send a notification to moderators on remote comment abuse', async function () { |
121 | this.timeout(20000) | 120 | this.timeout(50000) |
122 | 121 | ||
123 | const name = 'video for abuse ' + buildUUID() | 122 | const name = 'video for abuse ' + buildUUID() |
124 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | 123 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) |
@@ -140,7 +139,7 @@ describe('Test moderation notifications', function () { | |||
140 | }) | 139 | }) |
141 | 140 | ||
142 | it('Should send a notification to moderators on local account abuse', async function () { | 141 | it('Should send a notification to moderators on local account abuse', async function () { |
143 | this.timeout(20000) | 142 | this.timeout(50000) |
144 | 143 | ||
145 | const username = 'user' + new Date().getTime() | 144 | const username = 'user' + new Date().getTime() |
146 | const { account } = await servers[0].users.create({ username, password: 'donald' }) | 145 | const { account } = await servers[0].users.create({ username, password: 'donald' }) |
@@ -153,7 +152,7 @@ describe('Test moderation notifications', function () { | |||
153 | }) | 152 | }) |
154 | 153 | ||
155 | it('Should send a notification to moderators on remote account abuse', async function () { | 154 | it('Should send a notification to moderators on remote account abuse', async function () { |
156 | this.timeout(20000) | 155 | this.timeout(50000) |
157 | 156 | ||
158 | const username = 'user' + new Date().getTime() | 157 | const username = 'user' + new Date().getTime() |
159 | const tmpToken = await servers[0].users.generateUserAndToken(username) | 158 | const tmpToken = await servers[0].users.generateUserAndToken(username) |
@@ -327,32 +326,6 @@ describe('Test moderation notifications', function () { | |||
327 | }) | 326 | }) |
328 | }) | 327 | }) |
329 | 328 | ||
330 | describe('New registration', function () { | ||
331 | let baseParams: CheckerBaseParams | ||
332 | |||
333 | before(() => { | ||
334 | baseParams = { | ||
335 | server: servers[0], | ||
336 | emails, | ||
337 | socketNotifications: adminNotifications, | ||
338 | token: servers[0].accessToken | ||
339 | } | ||
340 | }) | ||
341 | |||
342 | it('Should send a notification only to moderators when a user registers on the instance', async function () { | ||
343 | this.timeout(10000) | ||
344 | |||
345 | await servers[0].users.register({ username: 'user_45' }) | ||
346 | |||
347 | await waitJobs(servers) | ||
348 | |||
349 | await checkUserRegistered({ ...baseParams, username: 'user_45', checkType: 'presence' }) | ||
350 | |||
351 | const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } } | ||
352 | await checkUserRegistered({ ...baseParams, ...userOverride, username: 'user_45', checkType: 'absence' }) | ||
353 | }) | ||
354 | }) | ||
355 | |||
356 | describe('New instance follows', function () { | 329 | describe('New instance follows', function () { |
357 | const instanceIndexServer = new MockInstancesIndex() | 330 | const instanceIndexServer = new MockInstancesIndex() |
358 | let config: any | 331 | let config: any |
@@ -512,10 +485,14 @@ describe('Test moderation notifications', function () { | |||
512 | }) | 485 | }) |
513 | 486 | ||
514 | it('Should not send video publish notification if auto-blacklisted', async function () { | 487 | it('Should not send video publish notification if auto-blacklisted', async function () { |
488 | this.timeout(120000) | ||
489 | |||
515 | await checkVideoIsPublished({ ...userBaseParams, videoName, shortUUID, checkType: 'absence' }) | 490 | await checkVideoIsPublished({ ...userBaseParams, videoName, shortUUID, checkType: 'absence' }) |
516 | }) | 491 | }) |
517 | 492 | ||
518 | it('Should not send a local user subscription notification if auto-blacklisted', async function () { | 493 | it('Should not send a local user subscription notification if auto-blacklisted', async function () { |
494 | this.timeout(120000) | ||
495 | |||
519 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'absence' }) | 496 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'absence' }) |
520 | }) | 497 | }) |
521 | 498 | ||
@@ -524,7 +501,7 @@ describe('Test moderation notifications', function () { | |||
524 | }) | 501 | }) |
525 | 502 | ||
526 | it('Should send video published and unblacklist after video unblacklisted', async function () { | 503 | it('Should send video published and unblacklist after video unblacklisted', async function () { |
527 | this.timeout(40000) | 504 | this.timeout(120000) |
528 | 505 | ||
529 | await servers[0].blacklist.remove({ videoId: uuid }) | 506 | await servers[0].blacklist.remove({ videoId: uuid }) |
530 | 507 | ||
@@ -537,10 +514,14 @@ describe('Test moderation notifications', function () { | |||
537 | }) | 514 | }) |
538 | 515 | ||
539 | it('Should send a local user subscription notification after removed from blacklist', async function () { | 516 | it('Should send a local user subscription notification after removed from blacklist', async function () { |
517 | this.timeout(120000) | ||
518 | |||
540 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'presence' }) | 519 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'presence' }) |
541 | }) | 520 | }) |
542 | 521 | ||
543 | it('Should send a remote user subscription notification after removed from blacklist', async function () { | 522 | it('Should send a remote user subscription notification after removed from blacklist', async function () { |
523 | this.timeout(120000) | ||
524 | |||
544 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'presence' }) | 525 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'presence' }) |
545 | }) | 526 | }) |
546 | 527 | ||
@@ -576,7 +557,7 @@ describe('Test moderation notifications', function () { | |||
576 | }) | 557 | }) |
577 | 558 | ||
578 | it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () { | 559 | it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () { |
579 | this.timeout(40000) | 560 | this.timeout(120000) |
580 | 561 | ||
581 | // In 2 seconds | 562 | // In 2 seconds |
582 | const updateAt = new Date(new Date().getTime() + 2000) | 563 | const updateAt = new Date(new Date().getTime() + 2000) |
diff --git a/server/tests/api/notifications/registrations-notifications.ts b/server/tests/api/notifications/registrations-notifications.ts new file mode 100644 index 000000000..b5a7c2bb5 --- /dev/null +++ b/server/tests/api/notifications/registrations-notifications.ts | |||
@@ -0,0 +1,88 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { | ||
4 | CheckerBaseParams, | ||
5 | checkRegistrationRequest, | ||
6 | checkUserRegistered, | ||
7 | MockSmtpServer, | ||
8 | prepareNotificationsTest | ||
9 | } from '@server/tests/shared' | ||
10 | import { UserNotification } from '@shared/models' | ||
11 | import { cleanupTests, PeerTubeServer, waitJobs } from '@shared/server-commands' | ||
12 | |||
13 | describe('Test registrations notifications', function () { | ||
14 | let server: PeerTubeServer | ||
15 | let userToken1: string | ||
16 | |||
17 | let userNotifications: UserNotification[] = [] | ||
18 | let adminNotifications: UserNotification[] = [] | ||
19 | let emails: object[] = [] | ||
20 | |||
21 | let baseParams: CheckerBaseParams | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(50000) | ||
25 | |||
26 | const res = await prepareNotificationsTest(1) | ||
27 | |||
28 | server = res.servers[0] | ||
29 | emails = res.emails | ||
30 | userToken1 = res.userAccessToken | ||
31 | adminNotifications = res.adminNotifications | ||
32 | userNotifications = res.userNotifications | ||
33 | |||
34 | baseParams = { | ||
35 | server, | ||
36 | emails, | ||
37 | socketNotifications: adminNotifications, | ||
38 | token: server.accessToken | ||
39 | } | ||
40 | }) | ||
41 | |||
42 | describe('New direct registration for moderators', function () { | ||
43 | |||
44 | before(async function () { | ||
45 | await server.config.enableSignup(false) | ||
46 | }) | ||
47 | |||
48 | it('Should send a notification only to moderators when a user registers on the instance', async function () { | ||
49 | this.timeout(50000) | ||
50 | |||
51 | await server.registrations.register({ username: 'user_10' }) | ||
52 | |||
53 | await waitJobs([ server ]) | ||
54 | |||
55 | await checkUserRegistered({ ...baseParams, username: 'user_10', checkType: 'presence' }) | ||
56 | |||
57 | const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } } | ||
58 | await checkUserRegistered({ ...baseParams, ...userOverride, username: 'user_10', checkType: 'absence' }) | ||
59 | }) | ||
60 | }) | ||
61 | |||
62 | describe('New registration request for moderators', function () { | ||
63 | |||
64 | before(async function () { | ||
65 | await server.config.enableSignup(true) | ||
66 | }) | ||
67 | |||
68 | it('Should send a notification on new registration request', async function () { | ||
69 | this.timeout(50000) | ||
70 | |||
71 | const registrationReason = 'my reason' | ||
72 | await server.registrations.requestRegistration({ username: 'user_11', registrationReason }) | ||
73 | |||
74 | await waitJobs([ server ]) | ||
75 | |||
76 | await checkRegistrationRequest({ ...baseParams, username: 'user_11', registrationReason, checkType: 'presence' }) | ||
77 | |||
78 | const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } } | ||
79 | await checkRegistrationRequest({ ...baseParams, ...userOverride, username: 'user_11', registrationReason, checkType: 'absence' }) | ||
80 | }) | ||
81 | }) | ||
82 | |||
83 | after(async function () { | ||
84 | MockSmtpServer.Instance.kill() | ||
85 | |||
86 | await cleanupTests([ server ]) | ||
87 | }) | ||
88 | }) | ||
diff --git a/server/tests/api/object-storage/video-static-file-privacy.ts b/server/tests/api/object-storage/video-static-file-privacy.ts index 71ad35a43..869d437d5 100644 --- a/server/tests/api/object-storage/video-static-file-privacy.ts +++ b/server/tests/api/object-storage/video-static-file-privacy.ts | |||
@@ -120,7 +120,7 @@ describe('Object storage for video static file privacy', function () { | |||
120 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
121 | 121 | ||
122 | it('Should upload a private video and have appropriate object storage ACL', async function () { | 122 | it('Should upload a private video and have appropriate object storage ACL', async function () { |
123 | this.timeout(60000) | 123 | this.timeout(120000) |
124 | 124 | ||
125 | { | 125 | { |
126 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | 126 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) |
@@ -138,7 +138,7 @@ describe('Object storage for video static file privacy', function () { | |||
138 | }) | 138 | }) |
139 | 139 | ||
140 | it('Should upload a public video and have appropriate object storage ACL', async function () { | 140 | it('Should upload a public video and have appropriate object storage ACL', async function () { |
141 | this.timeout(60000) | 141 | this.timeout(120000) |
142 | 142 | ||
143 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.UNLISTED }) | 143 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.UNLISTED }) |
144 | await waitJobs([ server ]) | 144 | await waitJobs([ server ]) |
diff --git a/server/tests/api/server/config-defaults.ts b/server/tests/api/server/config-defaults.ts index 4fa37d0e2..d3b3a2447 100644 --- a/server/tests/api/server/config-defaults.ts +++ b/server/tests/api/server/config-defaults.ts | |||
@@ -149,7 +149,7 @@ describe('Test config defaults', function () { | |||
149 | }) | 149 | }) |
150 | 150 | ||
151 | it('Should register a user with this default setting', async function () { | 151 | it('Should register a user with this default setting', async function () { |
152 | await server.users.register({ username: 'user_p2p_2' }) | 152 | await server.registrations.register({ username: 'user_p2p_2' }) |
153 | 153 | ||
154 | const userToken = await server.login.getAccessToken('user_p2p_2') | 154 | const userToken = await server.login.getAccessToken('user_p2p_2') |
155 | 155 | ||
@@ -194,7 +194,7 @@ describe('Test config defaults', function () { | |||
194 | }) | 194 | }) |
195 | 195 | ||
196 | it('Should register a user with this default setting', async function () { | 196 | it('Should register a user with this default setting', async function () { |
197 | await server.users.register({ username: 'user_p2p_4' }) | 197 | await server.registrations.register({ username: 'user_p2p_4' }) |
198 | 198 | ||
199 | const userToken = await server.login.getAccessToken('user_p2p_4') | 199 | const userToken = await server.login.getAccessToken('user_p2p_4') |
200 | 200 | ||
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index 22446fe0c..b91519660 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -50,6 +50,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { | |||
50 | expect(data.signup.enabled).to.be.true | 50 | expect(data.signup.enabled).to.be.true |
51 | expect(data.signup.limit).to.equal(4) | 51 | expect(data.signup.limit).to.equal(4) |
52 | expect(data.signup.minimumAge).to.equal(16) | 52 | expect(data.signup.minimumAge).to.equal(16) |
53 | expect(data.signup.requiresApproval).to.be.false | ||
53 | expect(data.signup.requiresEmailVerification).to.be.false | 54 | expect(data.signup.requiresEmailVerification).to.be.false |
54 | 55 | ||
55 | expect(data.admin.email).to.equal('admin' + server.internalServerNumber + '@example.com') | 56 | expect(data.admin.email).to.equal('admin' + server.internalServerNumber + '@example.com') |
@@ -152,6 +153,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
152 | 153 | ||
153 | expect(data.signup.enabled).to.be.false | 154 | expect(data.signup.enabled).to.be.false |
154 | expect(data.signup.limit).to.equal(5) | 155 | expect(data.signup.limit).to.equal(5) |
156 | expect(data.signup.requiresApproval).to.be.false | ||
155 | expect(data.signup.requiresEmailVerification).to.be.false | 157 | expect(data.signup.requiresEmailVerification).to.be.false |
156 | expect(data.signup.minimumAge).to.equal(10) | 158 | expect(data.signup.minimumAge).to.equal(10) |
157 | 159 | ||
@@ -285,6 +287,7 @@ const newCustomConfig: CustomConfig = { | |||
285 | signup: { | 287 | signup: { |
286 | enabled: false, | 288 | enabled: false, |
287 | limit: 5, | 289 | limit: 5, |
290 | requiresApproval: false, | ||
288 | requiresEmailVerification: false, | 291 | requiresEmailVerification: false, |
289 | minimumAge: 10 | 292 | minimumAge: 10 |
290 | }, | 293 | }, |
@@ -468,9 +471,9 @@ describe('Test config', function () { | |||
468 | this.timeout(5000) | 471 | this.timeout(5000) |
469 | 472 | ||
470 | await Promise.all([ | 473 | await Promise.all([ |
471 | server.users.register({ username: 'user1' }), | 474 | server.registrations.register({ username: 'user1' }), |
472 | server.users.register({ username: 'user2' }), | 475 | server.registrations.register({ username: 'user2' }), |
473 | server.users.register({ username: 'user3' }) | 476 | server.registrations.register({ username: 'user3' }) |
474 | ]) | 477 | ]) |
475 | 478 | ||
476 | const data = await server.config.getConfig() | 479 | const data = await server.config.getConfig() |
diff --git a/server/tests/api/server/contact-form.ts b/server/tests/api/server/contact-form.ts index 325218008..dd971203a 100644 --- a/server/tests/api/server/contact-form.ts +++ b/server/tests/api/server/contact-form.ts | |||
@@ -6,6 +6,7 @@ import { wait } from '@shared/core-utils' | |||
6 | import { HttpStatusCode } from '@shared/models' | 6 | import { HttpStatusCode } from '@shared/models' |
7 | import { | 7 | import { |
8 | cleanupTests, | 8 | cleanupTests, |
9 | ConfigCommand, | ||
9 | ContactFormCommand, | 10 | ContactFormCommand, |
10 | createSingleServer, | 11 | createSingleServer, |
11 | PeerTubeServer, | 12 | PeerTubeServer, |
@@ -23,13 +24,7 @@ describe('Test contact form', function () { | |||
23 | 24 | ||
24 | const port = await MockSmtpServer.Instance.collectEmails(emails) | 25 | const port = await MockSmtpServer.Instance.collectEmails(emails) |
25 | 26 | ||
26 | const overrideConfig = { | 27 | server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(port)) |
27 | smtp: { | ||
28 | hostname: '127.0.0.1', | ||
29 | port | ||
30 | } | ||
31 | } | ||
32 | server = await createSingleServer(1, overrideConfig) | ||
33 | await setAccessTokensToServers([ server ]) | 28 | await setAccessTokensToServers([ server ]) |
34 | 29 | ||
35 | command = server.contactForm | 30 | command = server.contactForm |
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts index 4ab5463fe..db7aa65bd 100644 --- a/server/tests/api/server/email.ts +++ b/server/tests/api/server/email.ts | |||
@@ -3,7 +3,14 @@ | |||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { MockSmtpServer } from '@server/tests/shared' | 4 | import { MockSmtpServer } from '@server/tests/shared' |
5 | import { HttpStatusCode } from '@shared/models' | 5 | import { HttpStatusCode } from '@shared/models' |
6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands' | 6 | import { |
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@shared/server-commands' | ||
7 | 14 | ||
8 | describe('Test emails', function () { | 15 | describe('Test emails', function () { |
9 | let server: PeerTubeServer | 16 | let server: PeerTubeServer |
@@ -24,21 +31,15 @@ describe('Test emails', function () { | |||
24 | username: 'user_1', | 31 | username: 'user_1', |
25 | password: 'super_password' | 32 | password: 'super_password' |
26 | } | 33 | } |
27 | let emailPort: number | ||
28 | 34 | ||
29 | before(async function () { | 35 | before(async function () { |
30 | this.timeout(50000) | 36 | this.timeout(50000) |
31 | 37 | ||
32 | emailPort = await MockSmtpServer.Instance.collectEmails(emails) | 38 | const emailPort = await MockSmtpServer.Instance.collectEmails(emails) |
39 | server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(emailPort)) | ||
33 | 40 | ||
34 | const overrideConfig = { | ||
35 | smtp: { | ||
36 | hostname: '127.0.0.1', | ||
37 | port: emailPort | ||
38 | } | ||
39 | } | ||
40 | server = await createSingleServer(1, overrideConfig) | ||
41 | await setAccessTokensToServers([ server ]) | 41 | await setAccessTokensToServers([ server ]) |
42 | await server.config.enableSignup(true) | ||
42 | 43 | ||
43 | { | 44 | { |
44 | const created = await server.users.create({ username: user.username, password: user.password }) | 45 | const created = await server.users.create({ username: user.username, password: user.password }) |
@@ -322,6 +323,62 @@ describe('Test emails', function () { | |||
322 | }) | 323 | }) |
323 | }) | 324 | }) |
324 | 325 | ||
326 | describe('When verifying a registration email', function () { | ||
327 | let registrationId: number | ||
328 | let registrationIdEmail: number | ||
329 | |||
330 | before(async function () { | ||
331 | const { id } = await server.registrations.requestRegistration({ | ||
332 | username: 'request_1', | ||
333 | email: 'request_1@example.com', | ||
334 | registrationReason: 'tt' | ||
335 | }) | ||
336 | registrationId = id | ||
337 | }) | ||
338 | |||
339 | it('Should ask to send the verification email', async function () { | ||
340 | this.timeout(10000) | ||
341 | |||
342 | await server.registrations.askSendVerifyEmail({ email: 'request_1@example.com' }) | ||
343 | |||
344 | await waitJobs(server) | ||
345 | expect(emails).to.have.lengthOf(9) | ||
346 | |||
347 | const email = emails[8] | ||
348 | |||
349 | expect(email['from'][0]['name']).equal('PeerTube') | ||
350 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
351 | expect(email['to'][0]['address']).equal('request_1@example.com') | ||
352 | expect(email['subject']).contains('Verify') | ||
353 | |||
354 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
355 | expect(verificationStringMatches).not.to.be.null | ||
356 | |||
357 | verificationString = verificationStringMatches[1] | ||
358 | expect(verificationString).to.not.be.undefined | ||
359 | expect(verificationString).to.have.length.above(2) | ||
360 | |||
361 | const registrationIdMatches = /registrationId=([0-9]+)/.exec(email['text']) | ||
362 | expect(registrationIdMatches).not.to.be.null | ||
363 | |||
364 | registrationIdEmail = parseInt(registrationIdMatches[1], 10) | ||
365 | |||
366 | expect(registrationId).to.equal(registrationIdEmail) | ||
367 | }) | ||
368 | |||
369 | it('Should not verify the email with an invalid verification string', async function () { | ||
370 | await server.registrations.verifyEmail({ | ||
371 | registrationId: registrationIdEmail, | ||
372 | verificationString: verificationString + 'b', | ||
373 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
374 | }) | ||
375 | }) | ||
376 | |||
377 | it('Should verify the email', async function () { | ||
378 | await server.registrations.verifyEmail({ registrationId: registrationIdEmail, verificationString }) | ||
379 | }) | ||
380 | }) | ||
381 | |||
325 | after(async function () { | 382 | after(async function () { |
326 | MockSmtpServer.Instance.kill() | 383 | MockSmtpServer.Instance.kill() |
327 | 384 | ||
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts index d882f0bde..11c96c4b5 100644 --- a/server/tests/api/server/reverse-proxy.ts +++ b/server/tests/api/server/reverse-proxy.ts | |||
@@ -106,13 +106,13 @@ describe('Test application behind a reverse proxy', function () { | |||
106 | it('Should rate limit signup', async function () { | 106 | it('Should rate limit signup', async function () { |
107 | for (let i = 0; i < 10; i++) { | 107 | for (let i = 0; i < 10; i++) { |
108 | try { | 108 | try { |
109 | await server.users.register({ username: 'test' + i }) | 109 | await server.registrations.register({ username: 'test' + i }) |
110 | } catch { | 110 | } catch { |
111 | // empty | 111 | // empty |
112 | } | 112 | } |
113 | } | 113 | } |
114 | 114 | ||
115 | await server.users.register({ username: 'test42', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | 115 | await server.registrations.register({ username: 'test42', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) |
116 | }) | 116 | }) |
117 | 117 | ||
118 | it('Should not rate limit failed signup', async function () { | 118 | it('Should not rate limit failed signup', async function () { |
@@ -121,10 +121,10 @@ describe('Test application behind a reverse proxy', function () { | |||
121 | await wait(7000) | 121 | await wait(7000) |
122 | 122 | ||
123 | for (let i = 0; i < 3; i++) { | 123 | for (let i = 0; i < 3; i++) { |
124 | await server.users.register({ username: 'test' + i, expectedStatus: HttpStatusCode.CONFLICT_409 }) | 124 | await server.registrations.register({ username: 'test' + i, expectedStatus: HttpStatusCode.CONFLICT_409 }) |
125 | } | 125 | } |
126 | 126 | ||
127 | await server.users.register({ username: 'test43', expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | 127 | await server.registrations.register({ username: 'test43', expectedStatus: HttpStatusCode.NO_CONTENT_204 }) |
128 | 128 | ||
129 | }) | 129 | }) |
130 | 130 | ||
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts index 643f1a531..a4443a8ec 100644 --- a/server/tests/api/users/index.ts +++ b/server/tests/api/users/index.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import './oauth' | ||
2 | import './registrations`' | ||
1 | import './two-factor' | 3 | import './two-factor' |
2 | import './user-subscriptions' | 4 | import './user-subscriptions' |
3 | import './user-videos' | 5 | import './user-videos' |
4 | import './users' | 6 | import './users' |
5 | import './users-multiple-servers' | 7 | import './users-multiple-servers' |
6 | import './users-verification' | 8 | import './users-email-verification' |
diff --git a/server/tests/api/users/oauth.ts b/server/tests/api/users/oauth.ts new file mode 100644 index 000000000..6a3da5ea2 --- /dev/null +++ b/server/tests/api/users/oauth.ts | |||
@@ -0,0 +1,192 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@shared/core-utils' | ||
5 | import { HttpStatusCode, OAuth2ErrorCode, PeerTubeProblemDocument } from '@shared/models' | ||
6 | import { cleanupTests, createSingleServer, killallServers, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
7 | |||
8 | describe('Test oauth', function () { | ||
9 | let server: PeerTubeServer | ||
10 | |||
11 | before(async function () { | ||
12 | this.timeout(30000) | ||
13 | |||
14 | server = await createSingleServer(1, { | ||
15 | rates_limit: { | ||
16 | login: { | ||
17 | max: 30 | ||
18 | } | ||
19 | } | ||
20 | }) | ||
21 | |||
22 | await setAccessTokensToServers([ server ]) | ||
23 | }) | ||
24 | |||
25 | describe('OAuth client', function () { | ||
26 | |||
27 | function expectInvalidClient (body: PeerTubeProblemDocument) { | ||
28 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) | ||
29 | expect(body.error).to.contain('client is invalid') | ||
30 | expect(body.type.startsWith('https://')).to.be.true | ||
31 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
32 | } | ||
33 | |||
34 | it('Should create a new client') | ||
35 | |||
36 | it('Should return the first client') | ||
37 | |||
38 | it('Should remove the last client') | ||
39 | |||
40 | it('Should not login with an invalid client id', async function () { | ||
41 | const client = { id: 'client', secret: server.store.client.secret } | ||
42 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
43 | |||
44 | expectInvalidClient(body) | ||
45 | }) | ||
46 | |||
47 | it('Should not login with an invalid client secret', async function () { | ||
48 | const client = { id: server.store.client.id, secret: 'coucou' } | ||
49 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
50 | |||
51 | expectInvalidClient(body) | ||
52 | }) | ||
53 | }) | ||
54 | |||
55 | describe('Login', function () { | ||
56 | |||
57 | function expectInvalidCredentials (body: PeerTubeProblemDocument) { | ||
58 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) | ||
59 | expect(body.error).to.contain('credentials are invalid') | ||
60 | expect(body.type.startsWith('https://')).to.be.true | ||
61 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
62 | } | ||
63 | |||
64 | it('Should not login with an invalid username', async function () { | ||
65 | const user = { username: 'captain crochet', password: server.store.user.password } | ||
66 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
67 | |||
68 | expectInvalidCredentials(body) | ||
69 | }) | ||
70 | |||
71 | it('Should not login with an invalid password', async function () { | ||
72 | const user = { username: server.store.user.username, password: 'mew_three' } | ||
73 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
74 | |||
75 | expectInvalidCredentials(body) | ||
76 | }) | ||
77 | |||
78 | it('Should be able to login', async function () { | ||
79 | await server.login.login({ expectedStatus: HttpStatusCode.OK_200 }) | ||
80 | }) | ||
81 | |||
82 | it('Should be able to login with an insensitive username', async function () { | ||
83 | const user = { username: 'RoOt', password: server.store.user.password } | ||
84 | await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 }) | ||
85 | |||
86 | const user2 = { username: 'rOoT', password: server.store.user.password } | ||
87 | await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 }) | ||
88 | |||
89 | const user3 = { username: 'ROOt', password: server.store.user.password } | ||
90 | await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 }) | ||
91 | }) | ||
92 | }) | ||
93 | |||
94 | describe('Logout', function () { | ||
95 | |||
96 | it('Should logout (revoke token)', async function () { | ||
97 | await server.login.logout({ token: server.accessToken }) | ||
98 | }) | ||
99 | |||
100 | it('Should not be able to get the user information', async function () { | ||
101 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
102 | }) | ||
103 | |||
104 | it('Should not be able to upload a video', async function () { | ||
105 | await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
106 | }) | ||
107 | |||
108 | it('Should be able to login again', async function () { | ||
109 | const body = await server.login.login() | ||
110 | server.accessToken = body.access_token | ||
111 | server.refreshToken = body.refresh_token | ||
112 | }) | ||
113 | |||
114 | it('Should be able to get my user information again', async function () { | ||
115 | await server.users.getMyInfo() | ||
116 | }) | ||
117 | |||
118 | it('Should have an expired access token', async function () { | ||
119 | this.timeout(60000) | ||
120 | |||
121 | await server.sql.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString()) | ||
122 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString()) | ||
123 | |||
124 | await killallServers([ server ]) | ||
125 | await server.run() | ||
126 | |||
127 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
128 | }) | ||
129 | |||
130 | it('Should not be able to refresh an access token with an expired refresh token', async function () { | ||
131 | await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
132 | }) | ||
133 | |||
134 | it('Should refresh the token', async function () { | ||
135 | this.timeout(50000) | ||
136 | |||
137 | const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString() | ||
138 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate) | ||
139 | |||
140 | await killallServers([ server ]) | ||
141 | await server.run() | ||
142 | |||
143 | const res = await server.login.refreshToken({ refreshToken: server.refreshToken }) | ||
144 | server.accessToken = res.body.access_token | ||
145 | server.refreshToken = res.body.refresh_token | ||
146 | }) | ||
147 | |||
148 | it('Should be able to get my user information again', async function () { | ||
149 | await server.users.getMyInfo() | ||
150 | }) | ||
151 | }) | ||
152 | |||
153 | describe('Custom token lifetime', function () { | ||
154 | before(async function () { | ||
155 | this.timeout(120_000) | ||
156 | |||
157 | await server.kill() | ||
158 | await server.run({ | ||
159 | oauth2: { | ||
160 | token_lifetime: { | ||
161 | access_token: '2 seconds', | ||
162 | refresh_token: '2 seconds' | ||
163 | } | ||
164 | } | ||
165 | }) | ||
166 | }) | ||
167 | |||
168 | it('Should have a very short access token lifetime', async function () { | ||
169 | this.timeout(50000) | ||
170 | |||
171 | const { access_token: accessToken } = await server.login.login() | ||
172 | await server.users.getMyInfo({ token: accessToken }) | ||
173 | |||
174 | await wait(3000) | ||
175 | await server.users.getMyInfo({ token: accessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
176 | }) | ||
177 | |||
178 | it('Should have a very short refresh token lifetime', async function () { | ||
179 | this.timeout(50000) | ||
180 | |||
181 | const { refresh_token: refreshToken } = await server.login.login() | ||
182 | await server.login.refreshToken({ refreshToken }) | ||
183 | |||
184 | await wait(3000) | ||
185 | await server.login.refreshToken({ refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
186 | }) | ||
187 | }) | ||
188 | |||
189 | after(async function () { | ||
190 | await cleanupTests([ server ]) | ||
191 | }) | ||
192 | }) | ||
diff --git a/server/tests/api/users/registrations.ts b/server/tests/api/users/registrations.ts new file mode 100644 index 000000000..e6524f07d --- /dev/null +++ b/server/tests/api/users/registrations.ts | |||
@@ -0,0 +1,415 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockSmtpServer } from '@server/tests/shared' | ||
5 | import { UserRegistrationState, UserRole } from '@shared/models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@shared/server-commands' | ||
14 | |||
15 | describe('Test registrations', function () { | ||
16 | let server: PeerTubeServer | ||
17 | |||
18 | const emails: object[] = [] | ||
19 | let emailPort: number | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(30000) | ||
23 | |||
24 | emailPort = await MockSmtpServer.Instance.collectEmails(emails) | ||
25 | |||
26 | server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(emailPort)) | ||
27 | |||
28 | await setAccessTokensToServers([ server ]) | ||
29 | await server.config.enableSignup(false) | ||
30 | }) | ||
31 | |||
32 | describe('Direct registrations of a new user', function () { | ||
33 | let user1Token: string | ||
34 | |||
35 | it('Should register a new user', async function () { | ||
36 | const user = { displayName: 'super user 1', username: 'user_1', password: 'my super password' } | ||
37 | const channel = { name: 'my_user_1_channel', displayName: 'my channel rocks' } | ||
38 | |||
39 | await server.registrations.register({ ...user, channel }) | ||
40 | }) | ||
41 | |||
42 | it('Should be able to login with this registered user', async function () { | ||
43 | const user1 = { username: 'user_1', password: 'my super password' } | ||
44 | |||
45 | user1Token = await server.login.getAccessToken(user1) | ||
46 | }) | ||
47 | |||
48 | it('Should have the correct display name', async function () { | ||
49 | const user = await server.users.getMyInfo({ token: user1Token }) | ||
50 | expect(user.account.displayName).to.equal('super user 1') | ||
51 | }) | ||
52 | |||
53 | it('Should have the correct video quota', async function () { | ||
54 | const user = await server.users.getMyInfo({ token: user1Token }) | ||
55 | expect(user.videoQuota).to.equal(5 * 1024 * 1024) | ||
56 | }) | ||
57 | |||
58 | it('Should have created the channel', async function () { | ||
59 | const { displayName } = await server.channels.get({ channelName: 'my_user_1_channel' }) | ||
60 | |||
61 | expect(displayName).to.equal('my channel rocks') | ||
62 | }) | ||
63 | |||
64 | it('Should remove me', async function () { | ||
65 | { | ||
66 | const { data } = await server.users.list() | ||
67 | expect(data.find(u => u.username === 'user_1')).to.not.be.undefined | ||
68 | } | ||
69 | |||
70 | await server.users.deleteMe({ token: user1Token }) | ||
71 | |||
72 | { | ||
73 | const { data } = await server.users.list() | ||
74 | expect(data.find(u => u.username === 'user_1')).to.be.undefined | ||
75 | } | ||
76 | }) | ||
77 | }) | ||
78 | |||
79 | describe('Registration requests', function () { | ||
80 | let id2: number | ||
81 | let id3: number | ||
82 | let id4: number | ||
83 | |||
84 | let user2Token: string | ||
85 | let user3Token: string | ||
86 | |||
87 | before(async function () { | ||
88 | this.timeout(60000) | ||
89 | |||
90 | await server.config.enableSignup(true) | ||
91 | |||
92 | { | ||
93 | const { id } = await server.registrations.requestRegistration({ | ||
94 | username: 'user4', | ||
95 | registrationReason: 'registration reason 4' | ||
96 | }) | ||
97 | |||
98 | id4 = id | ||
99 | } | ||
100 | }) | ||
101 | |||
102 | it('Should request a registration without a channel', async function () { | ||
103 | { | ||
104 | const { id } = await server.registrations.requestRegistration({ | ||
105 | username: 'user2', | ||
106 | displayName: 'my super user 2', | ||
107 | email: 'user2@example.com', | ||
108 | password: 'user2password', | ||
109 | registrationReason: 'registration reason 2' | ||
110 | }) | ||
111 | |||
112 | id2 = id | ||
113 | } | ||
114 | }) | ||
115 | |||
116 | it('Should request a registration with a channel', async function () { | ||
117 | const { id } = await server.registrations.requestRegistration({ | ||
118 | username: 'user3', | ||
119 | displayName: 'my super user 3', | ||
120 | channel: { | ||
121 | displayName: 'my user 3 channel', | ||
122 | name: 'super_user3_channel' | ||
123 | }, | ||
124 | email: 'user3@example.com', | ||
125 | password: 'user3password', | ||
126 | registrationReason: 'registration reason 3' | ||
127 | }) | ||
128 | |||
129 | id3 = id | ||
130 | }) | ||
131 | |||
132 | it('Should list these registration requests', async function () { | ||
133 | { | ||
134 | const { total, data } = await server.registrations.list({ sort: '-createdAt' }) | ||
135 | expect(total).to.equal(3) | ||
136 | expect(data).to.have.lengthOf(3) | ||
137 | |||
138 | { | ||
139 | expect(data[0].id).to.equal(id3) | ||
140 | expect(data[0].username).to.equal('user3') | ||
141 | expect(data[0].accountDisplayName).to.equal('my super user 3') | ||
142 | |||
143 | expect(data[0].channelDisplayName).to.equal('my user 3 channel') | ||
144 | expect(data[0].channelHandle).to.equal('super_user3_channel') | ||
145 | |||
146 | expect(data[0].createdAt).to.exist | ||
147 | expect(data[0].updatedAt).to.exist | ||
148 | |||
149 | expect(data[0].email).to.equal('user3@example.com') | ||
150 | expect(data[0].emailVerified).to.be.null | ||
151 | |||
152 | expect(data[0].moderationResponse).to.be.null | ||
153 | expect(data[0].registrationReason).to.equal('registration reason 3') | ||
154 | expect(data[0].state.id).to.equal(UserRegistrationState.PENDING) | ||
155 | expect(data[0].state.label).to.equal('Pending') | ||
156 | expect(data[0].user).to.be.null | ||
157 | } | ||
158 | |||
159 | { | ||
160 | expect(data[1].id).to.equal(id2) | ||
161 | expect(data[1].username).to.equal('user2') | ||
162 | expect(data[1].accountDisplayName).to.equal('my super user 2') | ||
163 | |||
164 | expect(data[1].channelDisplayName).to.be.null | ||
165 | expect(data[1].channelHandle).to.be.null | ||
166 | |||
167 | expect(data[1].createdAt).to.exist | ||
168 | expect(data[1].updatedAt).to.exist | ||
169 | |||
170 | expect(data[1].email).to.equal('user2@example.com') | ||
171 | expect(data[1].emailVerified).to.be.null | ||
172 | |||
173 | expect(data[1].moderationResponse).to.be.null | ||
174 | expect(data[1].registrationReason).to.equal('registration reason 2') | ||
175 | expect(data[1].state.id).to.equal(UserRegistrationState.PENDING) | ||
176 | expect(data[1].state.label).to.equal('Pending') | ||
177 | expect(data[1].user).to.be.null | ||
178 | } | ||
179 | |||
180 | { | ||
181 | expect(data[2].username).to.equal('user4') | ||
182 | } | ||
183 | } | ||
184 | |||
185 | { | ||
186 | const { total, data } = await server.registrations.list({ count: 1, start: 1, sort: 'createdAt' }) | ||
187 | |||
188 | expect(total).to.equal(3) | ||
189 | expect(data).to.have.lengthOf(1) | ||
190 | expect(data[0].id).to.equal(id2) | ||
191 | } | ||
192 | |||
193 | { | ||
194 | const { total, data } = await server.registrations.list({ search: 'user3' }) | ||
195 | expect(total).to.equal(1) | ||
196 | expect(data).to.have.lengthOf(1) | ||
197 | expect(data[0].id).to.equal(id3) | ||
198 | } | ||
199 | }) | ||
200 | |||
201 | it('Should reject a registration request', async function () { | ||
202 | await server.registrations.reject({ id: id4, moderationResponse: 'I do not want id 4 on this instance' }) | ||
203 | }) | ||
204 | |||
205 | it('Should have sent an email to the user explanining the registration has been rejected', async function () { | ||
206 | this.timeout(50000) | ||
207 | |||
208 | await waitJobs([ server ]) | ||
209 | |||
210 | const email = emails.find(e => e['to'][0]['address'] === 'user4@example.com') | ||
211 | expect(email).to.exist | ||
212 | |||
213 | expect(email['subject']).to.contain('been rejected') | ||
214 | expect(email['text']).to.contain('been rejected') | ||
215 | expect(email['text']).to.contain('I do not want id 4 on this instance') | ||
216 | }) | ||
217 | |||
218 | it('Should accept registration requests', async function () { | ||
219 | await server.registrations.accept({ id: id2, moderationResponse: 'Welcome id 2' }) | ||
220 | await server.registrations.accept({ id: id3, moderationResponse: 'Welcome id 3' }) | ||
221 | }) | ||
222 | |||
223 | it('Should have sent an email to the user explanining the registration has been accepted', async function () { | ||
224 | this.timeout(50000) | ||
225 | |||
226 | await waitJobs([ server ]) | ||
227 | |||
228 | { | ||
229 | const email = emails.find(e => e['to'][0]['address'] === 'user2@example.com') | ||
230 | expect(email).to.exist | ||
231 | |||
232 | expect(email['subject']).to.contain('been accepted') | ||
233 | expect(email['text']).to.contain('been accepted') | ||
234 | expect(email['text']).to.contain('Welcome id 2') | ||
235 | } | ||
236 | |||
237 | { | ||
238 | const email = emails.find(e => e['to'][0]['address'] === 'user3@example.com') | ||
239 | expect(email).to.exist | ||
240 | |||
241 | expect(email['subject']).to.contain('been accepted') | ||
242 | expect(email['text']).to.contain('been accepted') | ||
243 | expect(email['text']).to.contain('Welcome id 3') | ||
244 | } | ||
245 | }) | ||
246 | |||
247 | it('Should login with these users', async function () { | ||
248 | user2Token = await server.login.getAccessToken({ username: 'user2', password: 'user2password' }) | ||
249 | user3Token = await server.login.getAccessToken({ username: 'user3', password: 'user3password' }) | ||
250 | }) | ||
251 | |||
252 | it('Should have created the appropriate attributes for user 2', async function () { | ||
253 | const me = await server.users.getMyInfo({ token: user2Token }) | ||
254 | |||
255 | expect(me.username).to.equal('user2') | ||
256 | expect(me.account.displayName).to.equal('my super user 2') | ||
257 | expect(me.videoQuota).to.equal(5 * 1024 * 1024) | ||
258 | expect(me.videoChannels[0].name).to.equal('user2_channel') | ||
259 | expect(me.videoChannels[0].displayName).to.equal('Main user2 channel') | ||
260 | expect(me.role.id).to.equal(UserRole.USER) | ||
261 | expect(me.email).to.equal('user2@example.com') | ||
262 | }) | ||
263 | |||
264 | it('Should have created the appropriate attributes for user 3', async function () { | ||
265 | const me = await server.users.getMyInfo({ token: user3Token }) | ||
266 | |||
267 | expect(me.username).to.equal('user3') | ||
268 | expect(me.account.displayName).to.equal('my super user 3') | ||
269 | expect(me.videoQuota).to.equal(5 * 1024 * 1024) | ||
270 | expect(me.videoChannels[0].name).to.equal('super_user3_channel') | ||
271 | expect(me.videoChannels[0].displayName).to.equal('my user 3 channel') | ||
272 | expect(me.role.id).to.equal(UserRole.USER) | ||
273 | expect(me.email).to.equal('user3@example.com') | ||
274 | }) | ||
275 | |||
276 | it('Should list these accepted/rejected registration requests', async function () { | ||
277 | const { data } = await server.registrations.list({ sort: 'createdAt' }) | ||
278 | const { data: users } = await server.users.list() | ||
279 | |||
280 | { | ||
281 | expect(data[0].id).to.equal(id4) | ||
282 | expect(data[0].state.id).to.equal(UserRegistrationState.REJECTED) | ||
283 | expect(data[0].state.label).to.equal('Rejected') | ||
284 | |||
285 | expect(data[0].moderationResponse).to.equal('I do not want id 4 on this instance') | ||
286 | expect(data[0].user).to.be.null | ||
287 | |||
288 | expect(users.find(u => u.username === 'user4')).to.not.exist | ||
289 | } | ||
290 | |||
291 | { | ||
292 | expect(data[1].id).to.equal(id2) | ||
293 | expect(data[1].state.id).to.equal(UserRegistrationState.ACCEPTED) | ||
294 | expect(data[1].state.label).to.equal('Accepted') | ||
295 | |||
296 | expect(data[1].moderationResponse).to.equal('Welcome id 2') | ||
297 | expect(data[1].user).to.exist | ||
298 | |||
299 | const user2 = users.find(u => u.username === 'user2') | ||
300 | expect(data[1].user.id).to.equal(user2.id) | ||
301 | } | ||
302 | |||
303 | { | ||
304 | expect(data[2].id).to.equal(id3) | ||
305 | expect(data[2].state.id).to.equal(UserRegistrationState.ACCEPTED) | ||
306 | expect(data[2].state.label).to.equal('Accepted') | ||
307 | |||
308 | expect(data[2].moderationResponse).to.equal('Welcome id 3') | ||
309 | expect(data[2].user).to.exist | ||
310 | |||
311 | const user3 = users.find(u => u.username === 'user3') | ||
312 | expect(data[2].user.id).to.equal(user3.id) | ||
313 | } | ||
314 | }) | ||
315 | |||
316 | it('Shoulde delete a registration', async function () { | ||
317 | await server.registrations.delete({ id: id2 }) | ||
318 | await server.registrations.delete({ id: id3 }) | ||
319 | |||
320 | const { total, data } = await server.registrations.list() | ||
321 | expect(total).to.equal(1) | ||
322 | expect(data).to.have.lengthOf(1) | ||
323 | expect(data[0].id).to.equal(id4) | ||
324 | |||
325 | const { data: users } = await server.users.list() | ||
326 | |||
327 | for (const username of [ 'user2', 'user3' ]) { | ||
328 | expect(users.find(u => u.username === username)).to.exist | ||
329 | } | ||
330 | }) | ||
331 | |||
332 | it('Should be able to prevent email delivery on accept/reject', async function () { | ||
333 | this.timeout(50000) | ||
334 | |||
335 | let id1: number | ||
336 | let id2: number | ||
337 | |||
338 | { | ||
339 | const { id } = await server.registrations.requestRegistration({ | ||
340 | username: 'user7', | ||
341 | email: 'user7@example.com', | ||
342 | registrationReason: 'tt' | ||
343 | }) | ||
344 | id1 = id | ||
345 | } | ||
346 | { | ||
347 | const { id } = await server.registrations.requestRegistration({ | ||
348 | username: 'user8', | ||
349 | email: 'user8@example.com', | ||
350 | registrationReason: 'tt' | ||
351 | }) | ||
352 | id2 = id | ||
353 | } | ||
354 | |||
355 | await server.registrations.accept({ id: id1, moderationResponse: 'tt', preventEmailDelivery: true }) | ||
356 | await server.registrations.reject({ id: id2, moderationResponse: 'tt', preventEmailDelivery: true }) | ||
357 | |||
358 | await waitJobs([ server ]) | ||
359 | |||
360 | const filtered = emails.filter(e => { | ||
361 | const address = e['to'][0]['address'] | ||
362 | return address === 'user7@example.com' || address === 'user8@example.com' | ||
363 | }) | ||
364 | |||
365 | expect(filtered).to.have.lengthOf(0) | ||
366 | }) | ||
367 | |||
368 | it('Should request a registration without a channel, that will conflict with an already existing channel', async function () { | ||
369 | let id1: number | ||
370 | let id2: number | ||
371 | |||
372 | { | ||
373 | const { id } = await server.registrations.requestRegistration({ | ||
374 | registrationReason: 'tt', | ||
375 | username: 'user5', | ||
376 | password: 'user5password', | ||
377 | channel: { | ||
378 | displayName: 'channel 6', | ||
379 | name: 'user6_channel' | ||
380 | } | ||
381 | }) | ||
382 | |||
383 | id1 = id | ||
384 | } | ||
385 | |||
386 | { | ||
387 | const { id } = await server.registrations.requestRegistration({ | ||
388 | registrationReason: 'tt', | ||
389 | username: 'user6', | ||
390 | password: 'user6password' | ||
391 | }) | ||
392 | |||
393 | id2 = id | ||
394 | } | ||
395 | |||
396 | await server.registrations.accept({ id: id1, moderationResponse: 'tt' }) | ||
397 | await server.registrations.accept({ id: id2, moderationResponse: 'tt' }) | ||
398 | |||
399 | const user5Token = await server.login.getAccessToken('user5', 'user5password') | ||
400 | const user6Token = await server.login.getAccessToken('user6', 'user6password') | ||
401 | |||
402 | const user5 = await server.users.getMyInfo({ token: user5Token }) | ||
403 | const user6 = await server.users.getMyInfo({ token: user6Token }) | ||
404 | |||
405 | expect(user5.videoChannels[0].name).to.equal('user6_channel') | ||
406 | expect(user6.videoChannels[0].name).to.equal('user6_channel-1') | ||
407 | }) | ||
408 | }) | ||
409 | |||
410 | after(async function () { | ||
411 | MockSmtpServer.Instance.kill() | ||
412 | |||
413 | await cleanupTests([ server ]) | ||
414 | }) | ||
415 | }) | ||
diff --git a/server/tests/api/users/users-verification.ts b/server/tests/api/users/users-email-verification.ts index 19a8df9e1..cb84dc758 100644 --- a/server/tests/api/users/users-verification.ts +++ b/server/tests/api/users/users-email-verification.ts | |||
@@ -3,9 +3,16 @@ | |||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { MockSmtpServer } from '@server/tests/shared' | 4 | import { MockSmtpServer } from '@server/tests/shared' |
5 | import { HttpStatusCode } from '@shared/models' | 5 | import { HttpStatusCode } from '@shared/models' |
6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands' | 6 | import { |
7 | 7 | cleanupTests, | |
8 | describe('Test users account verification', function () { | 8 | ConfigCommand, |
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@shared/server-commands' | ||
14 | |||
15 | describe('Test users email verification', function () { | ||
9 | let server: PeerTubeServer | 16 | let server: PeerTubeServer |
10 | let userId: number | 17 | let userId: number |
11 | let userAccessToken: string | 18 | let userAccessToken: string |
@@ -25,14 +32,7 @@ describe('Test users account verification', function () { | |||
25 | this.timeout(30000) | 32 | this.timeout(30000) |
26 | 33 | ||
27 | const port = await MockSmtpServer.Instance.collectEmails(emails) | 34 | const port = await MockSmtpServer.Instance.collectEmails(emails) |
28 | 35 | server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(port)) | |
29 | const overrideConfig = { | ||
30 | smtp: { | ||
31 | hostname: '127.0.0.1', | ||
32 | port | ||
33 | } | ||
34 | } | ||
35 | server = await createSingleServer(1, overrideConfig) | ||
36 | 36 | ||
37 | await setAccessTokensToServers([ server ]) | 37 | await setAccessTokensToServers([ server ]) |
38 | }) | 38 | }) |
@@ -40,17 +40,18 @@ describe('Test users account verification', function () { | |||
40 | it('Should register user and send verification email if verification required', async function () { | 40 | it('Should register user and send verification email if verification required', async function () { |
41 | this.timeout(30000) | 41 | this.timeout(30000) |
42 | 42 | ||
43 | await server.config.updateCustomSubConfig({ | 43 | await server.config.updateExistingSubConfig({ |
44 | newConfig: { | 44 | newConfig: { |
45 | signup: { | 45 | signup: { |
46 | enabled: true, | 46 | enabled: true, |
47 | requiresApproval: false, | ||
47 | requiresEmailVerification: true, | 48 | requiresEmailVerification: true, |
48 | limit: 10 | 49 | limit: 10 |
49 | } | 50 | } |
50 | } | 51 | } |
51 | }) | 52 | }) |
52 | 53 | ||
53 | await server.users.register(user1) | 54 | await server.registrations.register(user1) |
54 | 55 | ||
55 | await waitJobs(server) | 56 | await waitJobs(server) |
56 | expectedEmailsLength++ | 57 | expectedEmailsLength++ |
@@ -127,17 +128,15 @@ describe('Test users account verification', function () { | |||
127 | 128 | ||
128 | it('Should register user not requiring email verification if setting not enabled', async function () { | 129 | it('Should register user not requiring email verification if setting not enabled', async function () { |
129 | this.timeout(5000) | 130 | this.timeout(5000) |
130 | await server.config.updateCustomSubConfig({ | 131 | await server.config.updateExistingSubConfig({ |
131 | newConfig: { | 132 | newConfig: { |
132 | signup: { | 133 | signup: { |
133 | enabled: true, | 134 | requiresEmailVerification: false |
134 | requiresEmailVerification: false, | ||
135 | limit: 10 | ||
136 | } | 135 | } |
137 | } | 136 | } |
138 | }) | 137 | }) |
139 | 138 | ||
140 | await server.users.register(user2) | 139 | await server.registrations.register(user2) |
141 | 140 | ||
142 | await waitJobs(server) | 141 | await waitJobs(server) |
143 | expect(emails).to.have.lengthOf(expectedEmailsLength) | 142 | expect(emails).to.have.lengthOf(expectedEmailsLength) |
@@ -152,9 +151,7 @@ describe('Test users account verification', function () { | |||
152 | await server.config.updateCustomSubConfig({ | 151 | await server.config.updateCustomSubConfig({ |
153 | newConfig: { | 152 | newConfig: { |
154 | signup: { | 153 | signup: { |
155 | enabled: true, | 154 | requiresEmailVerification: true |
156 | requiresEmailVerification: true, | ||
157 | limit: 10 | ||
158 | } | 155 | } |
159 | } | 156 | } |
160 | }) | 157 | }) |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 421b3ce16..f1e170971 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -2,15 +2,8 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { testImage } from '@server/tests/shared' | 4 | import { testImage } from '@server/tests/shared' |
5 | import { AbuseState, HttpStatusCode, OAuth2ErrorCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@shared/models' | 5 | import { AbuseState, HttpStatusCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@shared/models' |
6 | import { | 6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' |
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | killallServers, | ||
10 | makePutBodyRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@shared/server-commands' | ||
14 | 7 | ||
15 | describe('Test users', function () { | 8 | describe('Test users', function () { |
16 | let server: PeerTubeServer | 9 | let server: PeerTubeServer |
@@ -39,166 +32,6 @@ describe('Test users', function () { | |||
39 | await server.plugins.install({ npmName: 'peertube-theme-background-red' }) | 32 | await server.plugins.install({ npmName: 'peertube-theme-background-red' }) |
40 | }) | 33 | }) |
41 | 34 | ||
42 | describe('OAuth client', function () { | ||
43 | it('Should create a new client') | ||
44 | |||
45 | it('Should return the first client') | ||
46 | |||
47 | it('Should remove the last client') | ||
48 | |||
49 | it('Should not login with an invalid client id', async function () { | ||
50 | const client = { id: 'client', secret: server.store.client.secret } | ||
51 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
52 | |||
53 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) | ||
54 | expect(body.error).to.contain('client is invalid') | ||
55 | expect(body.type.startsWith('https://')).to.be.true | ||
56 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
57 | }) | ||
58 | |||
59 | it('Should not login with an invalid client secret', async function () { | ||
60 | const client = { id: server.store.client.id, secret: 'coucou' } | ||
61 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
62 | |||
63 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) | ||
64 | expect(body.error).to.contain('client is invalid') | ||
65 | expect(body.type.startsWith('https://')).to.be.true | ||
66 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
67 | }) | ||
68 | }) | ||
69 | |||
70 | describe('Login', function () { | ||
71 | |||
72 | it('Should not login with an invalid username', async function () { | ||
73 | const user = { username: 'captain crochet', password: server.store.user.password } | ||
74 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
75 | |||
76 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) | ||
77 | expect(body.error).to.contain('credentials are invalid') | ||
78 | expect(body.type.startsWith('https://')).to.be.true | ||
79 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
80 | }) | ||
81 | |||
82 | it('Should not login with an invalid password', async function () { | ||
83 | const user = { username: server.store.user.username, password: 'mew_three' } | ||
84 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
85 | |||
86 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) | ||
87 | expect(body.error).to.contain('credentials are invalid') | ||
88 | expect(body.type.startsWith('https://')).to.be.true | ||
89 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
90 | }) | ||
91 | |||
92 | it('Should not be able to upload a video', async function () { | ||
93 | token = 'my_super_token' | ||
94 | |||
95 | await server.videos.upload({ token, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
96 | }) | ||
97 | |||
98 | it('Should not be able to follow', async function () { | ||
99 | token = 'my_super_token' | ||
100 | |||
101 | await server.follows.follow({ | ||
102 | hosts: [ 'http://example.com' ], | ||
103 | token, | ||
104 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
105 | }) | ||
106 | }) | ||
107 | |||
108 | it('Should not be able to unfollow') | ||
109 | |||
110 | it('Should be able to login', async function () { | ||
111 | const body = await server.login.login({ expectedStatus: HttpStatusCode.OK_200 }) | ||
112 | |||
113 | token = body.access_token | ||
114 | }) | ||
115 | |||
116 | it('Should be able to login with an insensitive username', async function () { | ||
117 | const user = { username: 'RoOt', password: server.store.user.password } | ||
118 | await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 }) | ||
119 | |||
120 | const user2 = { username: 'rOoT', password: server.store.user.password } | ||
121 | await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 }) | ||
122 | |||
123 | const user3 = { username: 'ROOt', password: server.store.user.password } | ||
124 | await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 }) | ||
125 | }) | ||
126 | }) | ||
127 | |||
128 | describe('Logout', function () { | ||
129 | it('Should logout (revoke token)', async function () { | ||
130 | await server.login.logout({ token: server.accessToken }) | ||
131 | }) | ||
132 | |||
133 | it('Should not be able to get the user information', async function () { | ||
134 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
135 | }) | ||
136 | |||
137 | it('Should not be able to upload a video', async function () { | ||
138 | await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
139 | }) | ||
140 | |||
141 | it('Should not be able to rate a video', async function () { | ||
142 | const path = '/api/v1/videos/' | ||
143 | const data = { | ||
144 | rating: 'likes' | ||
145 | } | ||
146 | |||
147 | const options = { | ||
148 | url: server.url, | ||
149 | path: path + videoId, | ||
150 | token: 'wrong token', | ||
151 | fields: data, | ||
152 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
153 | } | ||
154 | await makePutBodyRequest(options) | ||
155 | }) | ||
156 | |||
157 | it('Should be able to login again', async function () { | ||
158 | const body = await server.login.login() | ||
159 | server.accessToken = body.access_token | ||
160 | server.refreshToken = body.refresh_token | ||
161 | }) | ||
162 | |||
163 | it('Should be able to get my user information again', async function () { | ||
164 | await server.users.getMyInfo() | ||
165 | }) | ||
166 | |||
167 | it('Should have an expired access token', async function () { | ||
168 | this.timeout(60000) | ||
169 | |||
170 | await server.sql.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString()) | ||
171 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString()) | ||
172 | |||
173 | await killallServers([ server ]) | ||
174 | await server.run() | ||
175 | |||
176 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
177 | }) | ||
178 | |||
179 | it('Should not be able to refresh an access token with an expired refresh token', async function () { | ||
180 | await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
181 | }) | ||
182 | |||
183 | it('Should refresh the token', async function () { | ||
184 | this.timeout(50000) | ||
185 | |||
186 | const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString() | ||
187 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate) | ||
188 | |||
189 | await killallServers([ server ]) | ||
190 | await server.run() | ||
191 | |||
192 | const res = await server.login.refreshToken({ refreshToken: server.refreshToken }) | ||
193 | server.accessToken = res.body.access_token | ||
194 | server.refreshToken = res.body.refresh_token | ||
195 | }) | ||
196 | |||
197 | it('Should be able to get my user information again', async function () { | ||
198 | await server.users.getMyInfo() | ||
199 | }) | ||
200 | }) | ||
201 | |||
202 | describe('Creating a user', function () { | 35 | describe('Creating a user', function () { |
203 | 36 | ||
204 | it('Should be able to create a new user', async function () { | 37 | it('Should be able to create a new user', async function () { |
@@ -512,6 +345,7 @@ describe('Test users', function () { | |||
512 | }) | 345 | }) |
513 | 346 | ||
514 | describe('Updating another user', function () { | 347 | describe('Updating another user', function () { |
348 | |||
515 | it('Should be able to update another user', async function () { | 349 | it('Should be able to update another user', async function () { |
516 | await server.users.update({ | 350 | await server.users.update({ |
517 | userId, | 351 | userId, |
@@ -562,13 +396,6 @@ describe('Test users', function () { | |||
562 | }) | 396 | }) |
563 | }) | 397 | }) |
564 | 398 | ||
565 | describe('Video blacklists', function () { | ||
566 | |||
567 | it('Should be able to list my video blacklist', async function () { | ||
568 | await server.blacklist.list({ token: userToken }) | ||
569 | }) | ||
570 | }) | ||
571 | |||
572 | describe('Remove a user', function () { | 399 | describe('Remove a user', function () { |
573 | 400 | ||
574 | before(async function () { | 401 | before(async function () { |
@@ -602,59 +429,10 @@ describe('Test users', function () { | |||
602 | }) | 429 | }) |
603 | }) | 430 | }) |
604 | 431 | ||
605 | describe('Registering a new user', function () { | ||
606 | let user15AccessToken: string | ||
607 | |||
608 | it('Should register a new user', async function () { | ||
609 | const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' } | ||
610 | const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' } | ||
611 | |||
612 | await server.users.register({ ...user, channel }) | ||
613 | }) | ||
614 | |||
615 | it('Should be able to login with this registered user', async function () { | ||
616 | const user15 = { | ||
617 | username: 'user_15', | ||
618 | password: 'my super password' | ||
619 | } | ||
620 | |||
621 | user15AccessToken = await server.login.getAccessToken(user15) | ||
622 | }) | ||
623 | |||
624 | it('Should have the correct display name', async function () { | ||
625 | const user = await server.users.getMyInfo({ token: user15AccessToken }) | ||
626 | expect(user.account.displayName).to.equal('super user 15') | ||
627 | }) | ||
628 | |||
629 | it('Should have the correct video quota', async function () { | ||
630 | const user = await server.users.getMyInfo({ token: user15AccessToken }) | ||
631 | expect(user.videoQuota).to.equal(5 * 1024 * 1024) | ||
632 | }) | ||
633 | |||
634 | it('Should have created the channel', async function () { | ||
635 | const { displayName } = await server.channels.get({ channelName: 'my_user_15_channel' }) | ||
636 | |||
637 | expect(displayName).to.equal('my channel rocks') | ||
638 | }) | ||
639 | |||
640 | it('Should remove me', async function () { | ||
641 | { | ||
642 | const { data } = await server.users.list() | ||
643 | expect(data.find(u => u.username === 'user_15')).to.not.be.undefined | ||
644 | } | ||
645 | |||
646 | await server.users.deleteMe({ token: user15AccessToken }) | ||
647 | |||
648 | { | ||
649 | const { data } = await server.users.list() | ||
650 | expect(data.find(u => u.username === 'user_15')).to.be.undefined | ||
651 | } | ||
652 | }) | ||
653 | }) | ||
654 | |||
655 | describe('User blocking', function () { | 432 | describe('User blocking', function () { |
656 | let user16Id | 433 | let user16Id: number |
657 | let user16AccessToken | 434 | let user16AccessToken: string |
435 | |||
658 | const user16 = { | 436 | const user16 = { |
659 | username: 'user_16', | 437 | username: 'user_16', |
660 | password: 'my super password' | 438 | password: 'my super password' |
diff --git a/server/tests/api/videos/video-channel-syncs.ts b/server/tests/api/videos/video-channel-syncs.ts index 91291524d..dd483f95e 100644 --- a/server/tests/api/videos/video-channel-syncs.ts +++ b/server/tests/api/videos/video-channel-syncs.ts | |||
@@ -307,6 +307,7 @@ describe('Test channel synchronizations', function () { | |||
307 | }) | 307 | }) |
308 | } | 308 | } |
309 | 309 | ||
310 | runSuite('youtube-dl') | 310 | // FIXME: suite is broken with youtube-dl |
311 | // runSuite('youtube-dl') | ||
311 | runSuite('yt-dlp') | 312 | runSuite('yt-dlp') |
312 | }) | 313 | }) |
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts index dc47f8a4a..e35500b0b 100644 --- a/server/tests/api/videos/video-comments.ts +++ b/server/tests/api/videos/video-comments.ts | |||
@@ -38,6 +38,8 @@ describe('Test video comments', function () { | |||
38 | await setDefaultAccountAvatar(server) | 38 | await setDefaultAccountAvatar(server) |
39 | 39 | ||
40 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') | 40 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') |
41 | await setDefaultChannelAvatar(server, 'user1_channel') | ||
42 | await setDefaultAccountAvatar(server, userAccessTokenServer1) | ||
41 | 43 | ||
42 | command = server.comments | 44 | command = server.comments |
43 | }) | 45 | }) |
@@ -167,6 +169,13 @@ describe('Test video comments', function () { | |||
167 | expect(body.data[2].totalReplies).to.equal(0) | 169 | expect(body.data[2].totalReplies).to.equal(0) |
168 | }) | 170 | }) |
169 | 171 | ||
172 | it('Should list the and sort them by total replies', async function () { | ||
173 | const body = await command.listThreads({ videoId: videoUUID, sort: 'totalReplies' }) | ||
174 | |||
175 | expect(body.data[2].text).to.equal('my super first comment') | ||
176 | expect(body.data[2].totalReplies).to.equal(3) | ||
177 | }) | ||
178 | |||
170 | it('Should delete a reply', async function () { | 179 | it('Should delete a reply', async function () { |
171 | await command.delete({ videoId, commentId: replyToDeleteId }) | 180 | await command.delete({ videoId, commentId: replyToDeleteId }) |
172 | 181 | ||
@@ -232,16 +241,34 @@ describe('Test video comments', function () { | |||
232 | await command.addReply({ videoId, toCommentId: threadId2, text: text3 }) | 241 | await command.addReply({ videoId, toCommentId: threadId2, text: text3 }) |
233 | 242 | ||
234 | const tree = await command.getThread({ videoId: videoUUID, threadId: threadId2 }) | 243 | const tree = await command.getThread({ videoId: videoUUID, threadId: threadId2 }) |
235 | expect(tree.comment.totalReplies).to.equal(tree.comment.totalRepliesFromVideoAuthor + 1) | 244 | expect(tree.comment.totalRepliesFromVideoAuthor).to.equal(1) |
245 | expect(tree.comment.totalReplies).to.equal(2) | ||
236 | }) | 246 | }) |
237 | }) | 247 | }) |
238 | 248 | ||
239 | describe('All instance comments', function () { | 249 | describe('All instance comments', function () { |
240 | 250 | ||
241 | it('Should list instance comments as admin', async function () { | 251 | it('Should list instance comments as admin', async function () { |
242 | const { data } = await command.listForAdmin({ start: 0, count: 1 }) | 252 | { |
253 | const { data, total } = await command.listForAdmin({ start: 0, count: 1 }) | ||
254 | |||
255 | expect(total).to.equal(7) | ||
256 | expect(data).to.have.lengthOf(1) | ||
257 | expect(data[0].text).to.equal('my second answer to thread 4') | ||
258 | expect(data[0].account.name).to.equal('root') | ||
259 | expect(data[0].account.displayName).to.equal('root') | ||
260 | expect(data[0].account.avatars).to.have.lengthOf(2) | ||
261 | } | ||
262 | |||
263 | { | ||
264 | const { data, total } = await command.listForAdmin({ start: 1, count: 2 }) | ||
243 | 265 | ||
244 | expect(data[0].text).to.equal('my second answer to thread 4') | 266 | expect(total).to.equal(7) |
267 | expect(data).to.have.lengthOf(2) | ||
268 | |||
269 | expect(data[0].account.avatars).to.have.lengthOf(2) | ||
270 | expect(data[1].account.avatars).to.have.lengthOf(2) | ||
271 | } | ||
245 | }) | 272 | }) |
246 | 273 | ||
247 | it('Should filter instance comments by isLocal', async function () { | 274 | it('Should filter instance comments by isLocal', async function () { |
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts index 0583134b2..5636de45f 100644 --- a/server/tests/api/videos/video-imports.ts +++ b/server/tests/api/videos/video-imports.ts | |||
@@ -41,7 +41,7 @@ async function checkVideosServer1 (server: PeerTubeServer, idHttp: string, idMag | |||
41 | const videoTorrent = await server.videos.get({ id: idTorrent }) | 41 | const videoTorrent = await server.videos.get({ id: idTorrent }) |
42 | 42 | ||
43 | for (const video of [ videoMagnet, videoTorrent ]) { | 43 | for (const video of [ videoMagnet, videoTorrent ]) { |
44 | expect(video.category.label).to.equal('Misc') | 44 | expect(video.category.label).to.equal('Unknown') |
45 | expect(video.licence.label).to.equal('Unknown') | 45 | expect(video.licence.label).to.equal('Unknown') |
46 | expect(video.language.label).to.equal('Unknown') | 46 | expect(video.language.label).to.equal('Unknown') |
47 | expect(video.nsfw).to.be.false | 47 | expect(video.nsfw).to.be.false |
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index 6a18cf26a..e8e653382 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts | |||
@@ -3,6 +3,7 @@ | |||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { checkPlaylistFilesWereRemoved, testImage } from '@server/tests/shared' | 4 | import { checkPlaylistFilesWereRemoved, testImage } from '@server/tests/shared' |
5 | import { wait } from '@shared/core-utils' | 5 | import { wait } from '@shared/core-utils' |
6 | import { uuidToShort } from '@shared/extra-utils' | ||
6 | import { | 7 | import { |
7 | HttpStatusCode, | 8 | HttpStatusCode, |
8 | VideoPlaylist, | 9 | VideoPlaylist, |
@@ -23,7 +24,6 @@ import { | |||
23 | setDefaultVideoChannel, | 24 | setDefaultVideoChannel, |
24 | waitJobs | 25 | waitJobs |
25 | } from '@shared/server-commands' | 26 | } from '@shared/server-commands' |
26 | import { uuidToShort } from '@shared/extra-utils' | ||
27 | 27 | ||
28 | async function checkPlaylistElementType ( | 28 | async function checkPlaylistElementType ( |
29 | servers: PeerTubeServer[], | 29 | servers: PeerTubeServer[], |
@@ -752,19 +752,6 @@ describe('Test video playlists', function () { | |||
752 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | 752 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
753 | } | 753 | } |
754 | }) | 754 | }) |
755 | |||
756 | it('Should hide the video if it is NSFW', async function () { | ||
757 | const body = await commands[0].listVideos({ token: userTokenServer1, playlistId: playlistServer1UUID2, query: { nsfw: 'false' } }) | ||
758 | expect(body.total).to.equal(3) | ||
759 | |||
760 | const elements = body.data | ||
761 | const element = elements.find(e => e.position === 3) | ||
762 | |||
763 | expect(element).to.exist | ||
764 | expect(element.video).to.be.null | ||
765 | expect(element.type).to.equal(VideoPlaylistElementType.UNAVAILABLE) | ||
766 | }) | ||
767 | |||
768 | }) | 755 | }) |
769 | 756 | ||
770 | describe('Managing playlist elements', function () { | 757 | describe('Managing playlist elements', function () { |
diff --git a/server/tests/external-plugins/akismet.ts b/server/tests/external-plugins/akismet.ts index 974bf0011..e964bf0c2 100644 --- a/server/tests/external-plugins/akismet.ts +++ b/server/tests/external-plugins/akismet.ts | |||
@@ -138,14 +138,14 @@ describe('Official plugin Akismet', function () { | |||
138 | }) | 138 | }) |
139 | 139 | ||
140 | it('Should allow signup', async function () { | 140 | it('Should allow signup', async function () { |
141 | await servers[0].users.register({ | 141 | await servers[0].registrations.register({ |
142 | username: 'user1', | 142 | username: 'user1', |
143 | displayName: 'user 1' | 143 | displayName: 'user 1' |
144 | }) | 144 | }) |
145 | }) | 145 | }) |
146 | 146 | ||
147 | it('Should detect a signup as SPAM', async function () { | 147 | it('Should detect a signup as SPAM', async function () { |
148 | await servers[0].users.register({ | 148 | await servers[0].registrations.register({ |
149 | username: 'user2', | 149 | username: 'user2', |
150 | displayName: 'user 2', | 150 | displayName: 'user 2', |
151 | email: 'akismet-guaranteed-spam@example.com', | 151 | email: 'akismet-guaranteed-spam@example.com', |
diff --git a/server/tests/external-plugins/auto-block-videos.ts b/server/tests/external-plugins/auto-block-videos.ts index d14587c38..cadd02e8d 100644 --- a/server/tests/external-plugins/auto-block-videos.ts +++ b/server/tests/external-plugins/auto-block-videos.ts | |||
@@ -30,7 +30,7 @@ describe('Official plugin auto-block videos', function () { | |||
30 | let port: number | 30 | let port: number |
31 | 31 | ||
32 | before(async function () { | 32 | before(async function () { |
33 | this.timeout(60000) | 33 | this.timeout(120000) |
34 | 34 | ||
35 | servers = await createMultipleServers(2) | 35 | servers = await createMultipleServers(2) |
36 | await setAccessTokensToServers(servers) | 36 | await setAccessTokensToServers(servers) |
diff --git a/server/tests/external-plugins/auto-mute.ts b/server/tests/external-plugins/auto-mute.ts index 440b58bfd..cfed76e88 100644 --- a/server/tests/external-plugins/auto-mute.ts +++ b/server/tests/external-plugins/auto-mute.ts | |||
@@ -21,7 +21,7 @@ describe('Official plugin auto-mute', function () { | |||
21 | let port: number | 21 | let port: number |
22 | 22 | ||
23 | before(async function () { | 23 | before(async function () { |
24 | this.timeout(30000) | 24 | this.timeout(120000) |
25 | 25 | ||
26 | servers = await createMultipleServers(2) | 26 | servers = await createMultipleServers(2) |
27 | await setAccessTokensToServers(servers) | 27 | await setAccessTokensToServers(servers) |
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index 906dab1a3..7345f728a 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -189,7 +189,7 @@ describe('Test syndication feeds', () => { | |||
189 | const jsonObj = JSON.parse(json) | 189 | const jsonObj = JSON.parse(json) |
190 | expect(jsonObj.items.length).to.be.equal(1) | 190 | expect(jsonObj.items.length).to.be.equal(1) |
191 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') | 191 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') |
192 | expect(jsonObj.items[0].author.name).to.equal('root') | 192 | expect(jsonObj.items[0].author.name).to.equal('Main root channel') |
193 | } | 193 | } |
194 | 194 | ||
195 | { | 195 | { |
@@ -197,7 +197,7 @@ describe('Test syndication feeds', () => { | |||
197 | const jsonObj = JSON.parse(json) | 197 | const jsonObj = JSON.parse(json) |
198 | expect(jsonObj.items.length).to.be.equal(1) | 198 | expect(jsonObj.items.length).to.be.equal(1) |
199 | expect(jsonObj.items[0].title).to.equal('user video') | 199 | expect(jsonObj.items[0].title).to.equal('user video') |
200 | expect(jsonObj.items[0].author.name).to.equal('john') | 200 | expect(jsonObj.items[0].author.name).to.equal('Main john channel') |
201 | } | 201 | } |
202 | 202 | ||
203 | for (const server of servers) { | 203 | for (const server of servers) { |
@@ -223,7 +223,7 @@ describe('Test syndication feeds', () => { | |||
223 | const jsonObj = JSON.parse(json) | 223 | const jsonObj = JSON.parse(json) |
224 | expect(jsonObj.items.length).to.be.equal(1) | 224 | expect(jsonObj.items.length).to.be.equal(1) |
225 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') | 225 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') |
226 | expect(jsonObj.items[0].author.name).to.equal('root') | 226 | expect(jsonObj.items[0].author.name).to.equal('Main root channel') |
227 | } | 227 | } |
228 | 228 | ||
229 | { | 229 | { |
@@ -231,7 +231,7 @@ describe('Test syndication feeds', () => { | |||
231 | const jsonObj = JSON.parse(json) | 231 | const jsonObj = JSON.parse(json) |
232 | expect(jsonObj.items.length).to.be.equal(1) | 232 | expect(jsonObj.items.length).to.be.equal(1) |
233 | expect(jsonObj.items[0].title).to.equal('user video') | 233 | expect(jsonObj.items[0].title).to.equal('user video') |
234 | expect(jsonObj.items[0].author.name).to.equal('john') | 234 | expect(jsonObj.items[0].author.name).to.equal('Main john channel') |
235 | } | 235 | } |
236 | 236 | ||
237 | for (const server of servers) { | 237 | for (const server of servers) { |
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js index c65b8d3a8..58bc27661 100644 --- a/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js +++ b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js | |||
@@ -33,7 +33,17 @@ async function register ({ | |||
33 | username: 'kefka', | 33 | username: 'kefka', |
34 | email: 'kefka@example.com', | 34 | email: 'kefka@example.com', |
35 | role: 0, | 35 | role: 0, |
36 | displayName: 'Kefka Palazzo' | 36 | displayName: 'Kefka Palazzo', |
37 | adminFlags: 1, | ||
38 | videoQuota: 42000, | ||
39 | videoQuotaDaily: 42100, | ||
40 | |||
41 | // Always use new value except for videoQuotaDaily field | ||
42 | userUpdater: ({ fieldName, currentValue, newValue }) => { | ||
43 | if (fieldName === 'videoQuotaDaily') return currentValue | ||
44 | |||
45 | return newValue | ||
46 | } | ||
37 | }) | 47 | }) |
38 | }, | 48 | }, |
39 | hookTokenValidity: (options) => { | 49 | hookTokenValidity: (options) => { |
diff --git a/server/tests/fixtures/peertube-plugin-test-four/main.js b/server/tests/fixtures/peertube-plugin-test-four/main.js index 3e848c49e..b10177b45 100644 --- a/server/tests/fixtures/peertube-plugin-test-four/main.js +++ b/server/tests/fixtures/peertube-plugin-test-four/main.js | |||
@@ -76,6 +76,12 @@ async function register ({ | |||
76 | return res.json({ serverConfig }) | 76 | return res.json({ serverConfig }) |
77 | }) | 77 | }) |
78 | 78 | ||
79 | router.get('/server-listening-config', async (req, res) => { | ||
80 | const config = await peertubeHelpers.config.getServerListeningConfig() | ||
81 | |||
82 | return res.json({ config }) | ||
83 | }) | ||
84 | |||
79 | router.get('/static-route', async (req, res) => { | 85 | router.get('/static-route', async (req, res) => { |
80 | const staticRoute = peertubeHelpers.plugin.getBaseStaticRoute() | 86 | const staticRoute = peertubeHelpers.plugin.getBaseStaticRoute() |
81 | 87 | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js index ceab7b60d..fad5abf60 100644 --- a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js +++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js | |||
@@ -33,7 +33,18 @@ async function register ({ | |||
33 | if (body.id === 'laguna' && body.password === 'laguna password') { | 33 | if (body.id === 'laguna' && body.password === 'laguna password') { |
34 | return Promise.resolve({ | 34 | return Promise.resolve({ |
35 | username: 'laguna', | 35 | username: 'laguna', |
36 | email: 'laguna@example.com' | 36 | email: 'laguna@example.com', |
37 | displayName: 'Laguna Loire', | ||
38 | adminFlags: 1, | ||
39 | videoQuota: 42000, | ||
40 | videoQuotaDaily: 42100, | ||
41 | |||
42 | // Always use new value except for videoQuotaDaily field | ||
43 | userUpdater: ({ fieldName, currentValue, newValue }) => { | ||
44 | if (fieldName === 'videoQuotaDaily') return currentValue | ||
45 | |||
46 | return newValue | ||
47 | } | ||
37 | }) | 48 | }) |
38 | } | 49 | } |
39 | 50 | ||
diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js index 19dccf26e..5b4d34f15 100644 --- a/server/tests/fixtures/peertube-plugin-test/main.js +++ b/server/tests/fixtures/peertube-plugin-test/main.js | |||
@@ -226,16 +226,29 @@ async function register ({ registerHook, registerSetting, settingsManager, stora | |||
226 | } | 226 | } |
227 | }) | 227 | }) |
228 | 228 | ||
229 | registerHook({ | 229 | { |
230 | target: 'filter:api.user.signup.allowed.result', | 230 | registerHook({ |
231 | handler: (result, params) => { | 231 | target: 'filter:api.user.signup.allowed.result', |
232 | if (params && params.body && params.body.email && params.body.email.includes('jma')) { | 232 | handler: (result, params) => { |
233 | return { allowed: false, errorMessage: 'No jma' } | 233 | if (params && params.body && params.body.email && params.body.email.includes('jma 1')) { |
234 | return { allowed: false, errorMessage: 'No jma 1' } | ||
235 | } | ||
236 | |||
237 | return result | ||
234 | } | 238 | } |
239 | }) | ||
235 | 240 | ||
236 | return result | 241 | registerHook({ |
237 | } | 242 | target: 'filter:api.user.request-signup.allowed.result', |
238 | }) | 243 | handler: (result, params) => { |
244 | if (params && params.body && params.body.email && params.body.email.includes('jma 2')) { | ||
245 | return { allowed: false, errorMessage: 'No jma 2' } | ||
246 | } | ||
247 | |||
248 | return result | ||
249 | } | ||
250 | }) | ||
251 | } | ||
239 | 252 | ||
240 | registerHook({ | 253 | registerHook({ |
241 | target: 'filter:api.download.torrent.allowed.result', | 254 | target: 'filter:api.download.torrent.allowed.result', |
@@ -250,7 +263,10 @@ async function register ({ registerHook, registerSetting, settingsManager, stora | |||
250 | 263 | ||
251 | registerHook({ | 264 | registerHook({ |
252 | target: 'filter:api.download.video.allowed.result', | 265 | target: 'filter:api.download.video.allowed.result', |
253 | handler: (result, params) => { | 266 | handler: async (result, params) => { |
267 | const loggedInUser = await peertubeHelpers.user.getAuthUser(params.res) | ||
268 | if (loggedInUser) return { allowed: true } | ||
269 | |||
254 | if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) { | 270 | if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) { |
255 | return { allowed: false, errorMessage: 'Cao Cao' } | 271 | return { allowed: false, errorMessage: 'Cao Cao' } |
256 | } | 272 | } |
diff --git a/server/tests/helpers/index.ts b/server/tests/helpers/index.ts index 1b5c6d15b..073ae6455 100644 --- a/server/tests/helpers/index.ts +++ b/server/tests/helpers/index.ts | |||
@@ -6,3 +6,4 @@ import './image' | |||
6 | import './markdown' | 6 | import './markdown' |
7 | import './request' | 7 | import './request' |
8 | import './validator' | 8 | import './validator' |
9 | import './version' | ||
diff --git a/server/tests/helpers/version.ts b/server/tests/helpers/version.ts new file mode 100644 index 000000000..2a90efba3 --- /dev/null +++ b/server/tests/helpers/version.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { compareSemVer } from '@shared/core-utils' | ||
5 | |||
6 | describe('Version', function () { | ||
7 | |||
8 | it('Should correctly compare two stable versions', async function () { | ||
9 | expect(compareSemVer('3.4.0', '3.5.0')).to.be.below(0) | ||
10 | expect(compareSemVer('3.5.0', '3.4.0')).to.be.above(0) | ||
11 | |||
12 | expect(compareSemVer('3.4.0', '4.1.0')).to.be.below(0) | ||
13 | expect(compareSemVer('4.1.0', '3.4.0')).to.be.above(0) | ||
14 | |||
15 | expect(compareSemVer('3.4.0', '3.4.1')).to.be.below(0) | ||
16 | expect(compareSemVer('3.4.1', '3.4.0')).to.be.above(0) | ||
17 | }) | ||
18 | |||
19 | it('Should correctly compare two unstable version', async function () { | ||
20 | expect(compareSemVer('3.4.0-alpha', '3.4.0-beta.1')).to.be.below(0) | ||
21 | expect(compareSemVer('3.4.0-alpha.1', '3.4.0-beta.1')).to.be.below(0) | ||
22 | expect(compareSemVer('3.4.0-beta.1', '3.4.0-beta.2')).to.be.below(0) | ||
23 | expect(compareSemVer('3.4.0-beta.1', '3.5.0-alpha.1')).to.be.below(0) | ||
24 | |||
25 | expect(compareSemVer('3.4.0-alpha.1', '3.4.0-nightly.4')).to.be.below(0) | ||
26 | expect(compareSemVer('3.4.0-nightly.3', '3.4.0-nightly.4')).to.be.below(0) | ||
27 | expect(compareSemVer('3.3.0-nightly.5', '3.4.0-nightly.4')).to.be.below(0) | ||
28 | }) | ||
29 | |||
30 | it('Should correctly compare a stable and unstable versions', async function () { | ||
31 | expect(compareSemVer('3.4.0', '3.4.1-beta.1')).to.be.below(0) | ||
32 | expect(compareSemVer('3.4.0-beta.1', '3.4.0-beta.2')).to.be.below(0) | ||
33 | expect(compareSemVer('3.4.0-beta.1', '3.4.0')).to.be.below(0) | ||
34 | expect(compareSemVer('3.4.0-nightly.4', '3.4.0')).to.be.below(0) | ||
35 | }) | ||
36 | }) | ||
diff --git a/server/tests/plugins/action-hooks.ts b/server/tests/plugins/action-hooks.ts index 36f8052c0..a266ae7f1 100644 --- a/server/tests/plugins/action-hooks.ts +++ b/server/tests/plugins/action-hooks.ts | |||
@@ -153,7 +153,7 @@ describe('Test plugin action hooks', function () { | |||
153 | let userId: number | 153 | let userId: number |
154 | 154 | ||
155 | it('Should run action:api.user.registered', async function () { | 155 | it('Should run action:api.user.registered', async function () { |
156 | await servers[0].users.register({ username: 'registered_user' }) | 156 | await servers[0].registrations.register({ username: 'registered_user' }) |
157 | 157 | ||
158 | await checkHook('action:api.user.registered') | 158 | await checkHook('action:api.user.registered') |
159 | }) | 159 | }) |
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts index 437777e90..e600f958f 100644 --- a/server/tests/plugins/external-auth.ts +++ b/server/tests/plugins/external-auth.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { wait } from '@shared/core-utils' | 4 | import { wait } from '@shared/core-utils' |
5 | import { HttpStatusCode, UserRole } from '@shared/models' | 5 | import { HttpStatusCode, UserAdminFlag, UserRole } from '@shared/models' |
6 | import { | 6 | import { |
7 | cleanupTests, | 7 | cleanupTests, |
8 | createSingleServer, | 8 | createSingleServer, |
@@ -51,6 +51,7 @@ describe('Test external auth plugins', function () { | |||
51 | 51 | ||
52 | let kefkaAccessToken: string | 52 | let kefkaAccessToken: string |
53 | let kefkaRefreshToken: string | 53 | let kefkaRefreshToken: string |
54 | let kefkaId: number | ||
54 | 55 | ||
55 | let externalAuthToken: string | 56 | let externalAuthToken: string |
56 | 57 | ||
@@ -156,6 +157,9 @@ describe('Test external auth plugins', function () { | |||
156 | expect(body.account.displayName).to.equal('cyan') | 157 | expect(body.account.displayName).to.equal('cyan') |
157 | expect(body.email).to.equal('cyan@example.com') | 158 | expect(body.email).to.equal('cyan@example.com') |
158 | expect(body.role.id).to.equal(UserRole.USER) | 159 | expect(body.role.id).to.equal(UserRole.USER) |
160 | expect(body.adminFlags).to.equal(UserAdminFlag.NONE) | ||
161 | expect(body.videoQuota).to.equal(5242880) | ||
162 | expect(body.videoQuotaDaily).to.equal(-1) | ||
159 | } | 163 | } |
160 | }) | 164 | }) |
161 | 165 | ||
@@ -178,6 +182,11 @@ describe('Test external auth plugins', function () { | |||
178 | expect(body.account.displayName).to.equal('Kefka Palazzo') | 182 | expect(body.account.displayName).to.equal('Kefka Palazzo') |
179 | expect(body.email).to.equal('kefka@example.com') | 183 | expect(body.email).to.equal('kefka@example.com') |
180 | expect(body.role.id).to.equal(UserRole.ADMINISTRATOR) | 184 | expect(body.role.id).to.equal(UserRole.ADMINISTRATOR) |
185 | expect(body.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) | ||
186 | expect(body.videoQuota).to.equal(42000) | ||
187 | expect(body.videoQuotaDaily).to.equal(42100) | ||
188 | |||
189 | kefkaId = body.id | ||
181 | } | 190 | } |
182 | }) | 191 | }) |
183 | 192 | ||
@@ -240,6 +249,37 @@ describe('Test external auth plugins', function () { | |||
240 | expect(body.role.id).to.equal(UserRole.USER) | 249 | expect(body.role.id).to.equal(UserRole.USER) |
241 | }) | 250 | }) |
242 | 251 | ||
252 | it('Should login Kefka and update the profile', async function () { | ||
253 | { | ||
254 | await server.users.update({ userId: kefkaId, videoQuota: 43000, videoQuotaDaily: 43100 }) | ||
255 | await server.users.updateMe({ token: kefkaAccessToken, displayName: 'kefka updated' }) | ||
256 | |||
257 | const body = await server.users.getMyInfo({ token: kefkaAccessToken }) | ||
258 | expect(body.username).to.equal('kefka') | ||
259 | expect(body.account.displayName).to.equal('kefka updated') | ||
260 | expect(body.videoQuota).to.equal(43000) | ||
261 | expect(body.videoQuotaDaily).to.equal(43100) | ||
262 | } | ||
263 | |||
264 | { | ||
265 | const res = await loginExternal({ | ||
266 | server, | ||
267 | npmName: 'test-external-auth-one', | ||
268 | authName: 'external-auth-2', | ||
269 | username: 'kefka' | ||
270 | }) | ||
271 | |||
272 | kefkaAccessToken = res.access_token | ||
273 | kefkaRefreshToken = res.refresh_token | ||
274 | |||
275 | const body = await server.users.getMyInfo({ token: kefkaAccessToken }) | ||
276 | expect(body.username).to.equal('kefka') | ||
277 | expect(body.account.displayName).to.equal('Kefka Palazzo') | ||
278 | expect(body.videoQuota).to.equal(42000) | ||
279 | expect(body.videoQuotaDaily).to.equal(43100) | ||
280 | } | ||
281 | }) | ||
282 | |||
243 | it('Should not update an external auth email', async function () { | 283 | it('Should not update an external auth email', async function () { |
244 | await server.users.updateMe({ | 284 | await server.users.updateMe({ |
245 | token: cyanAccessToken, | 285 | token: cyanAccessToken, |
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index 083fd43ca..37eef6cf3 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -1,7 +1,15 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { HttpStatusCode, VideoDetails, VideoImportState, VideoPlaylist, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | 4 | import { |
5 | HttpStatusCode, | ||
6 | PeerTubeProblemDocument, | ||
7 | VideoDetails, | ||
8 | VideoImportState, | ||
9 | VideoPlaylist, | ||
10 | VideoPlaylistPrivacy, | ||
11 | VideoPrivacy | ||
12 | } from '@shared/models' | ||
5 | import { | 13 | import { |
6 | cleanupTests, | 14 | cleanupTests, |
7 | createMultipleServers, | 15 | createMultipleServers, |
@@ -408,28 +416,58 @@ describe('Test plugin filter hooks', function () { | |||
408 | 416 | ||
409 | describe('Should run filter:api.user.signup.allowed.result', function () { | 417 | describe('Should run filter:api.user.signup.allowed.result', function () { |
410 | 418 | ||
419 | before(async function () { | ||
420 | await servers[0].config.updateExistingSubConfig({ newConfig: { signup: { requiresApproval: false } } }) | ||
421 | }) | ||
422 | |||
411 | it('Should run on config endpoint', async function () { | 423 | it('Should run on config endpoint', async function () { |
412 | const body = await servers[0].config.getConfig() | 424 | const body = await servers[0].config.getConfig() |
413 | expect(body.signup.allowed).to.be.true | 425 | expect(body.signup.allowed).to.be.true |
414 | }) | 426 | }) |
415 | 427 | ||
416 | it('Should allow a signup', async function () { | 428 | it('Should allow a signup', async function () { |
417 | await servers[0].users.register({ username: 'john', password: 'password' }) | 429 | await servers[0].registrations.register({ username: 'john1' }) |
418 | }) | 430 | }) |
419 | 431 | ||
420 | it('Should not allow a signup', async function () { | 432 | it('Should not allow a signup', async function () { |
421 | const res = await servers[0].users.register({ | 433 | const res = await servers[0].registrations.register({ |
422 | username: 'jma', | 434 | username: 'jma 1', |
423 | password: 'password', | ||
424 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | 435 | expectedStatus: HttpStatusCode.FORBIDDEN_403 |
425 | }) | 436 | }) |
426 | 437 | ||
427 | expect(res.body.error).to.equal('No jma') | 438 | expect(res.body.error).to.equal('No jma 1') |
439 | }) | ||
440 | }) | ||
441 | |||
442 | describe('Should run filter:api.user.request-signup.allowed.result', function () { | ||
443 | |||
444 | before(async function () { | ||
445 | await servers[0].config.updateExistingSubConfig({ newConfig: { signup: { requiresApproval: true } } }) | ||
446 | }) | ||
447 | |||
448 | it('Should run on config endpoint', async function () { | ||
449 | const body = await servers[0].config.getConfig() | ||
450 | expect(body.signup.allowed).to.be.true | ||
451 | }) | ||
452 | |||
453 | it('Should allow a signup request', async function () { | ||
454 | await servers[0].registrations.requestRegistration({ username: 'john2', registrationReason: 'tt' }) | ||
455 | }) | ||
456 | |||
457 | it('Should not allow a signup request', async function () { | ||
458 | const body = await servers[0].registrations.requestRegistration({ | ||
459 | username: 'jma 2', | ||
460 | registrationReason: 'tt', | ||
461 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
462 | }) | ||
463 | |||
464 | expect((body as unknown as PeerTubeProblemDocument).error).to.equal('No jma 2') | ||
428 | }) | 465 | }) |
429 | }) | 466 | }) |
430 | 467 | ||
431 | describe('Download hooks', function () { | 468 | describe('Download hooks', function () { |
432 | const downloadVideos: VideoDetails[] = [] | 469 | const downloadVideos: VideoDetails[] = [] |
470 | let downloadVideo2Token: string | ||
433 | 471 | ||
434 | before(async function () { | 472 | before(async function () { |
435 | this.timeout(120000) | 473 | this.timeout(120000) |
@@ -459,6 +497,8 @@ describe('Test plugin filter hooks', function () { | |||
459 | for (const uuid of uuids) { | 497 | for (const uuid of uuids) { |
460 | downloadVideos.push(await servers[0].videos.get({ id: uuid })) | 498 | downloadVideos.push(await servers[0].videos.get({ id: uuid })) |
461 | } | 499 | } |
500 | |||
501 | downloadVideo2Token = await servers[0].videoToken.getVideoFileToken({ videoId: downloadVideos[2].uuid }) | ||
462 | }) | 502 | }) |
463 | 503 | ||
464 | it('Should run filter:api.download.torrent.allowed.result', async function () { | 504 | it('Should run filter:api.download.torrent.allowed.result', async function () { |
@@ -471,32 +511,42 @@ describe('Test plugin filter hooks', function () { | |||
471 | 511 | ||
472 | it('Should run filter:api.download.video.allowed.result', async function () { | 512 | it('Should run filter:api.download.video.allowed.result', async function () { |
473 | { | 513 | { |
474 | const res = await makeRawRequest({ url: downloadVideos[1].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 514 | const refused = downloadVideos[1].files[0].fileDownloadUrl |
515 | const allowed = [ | ||
516 | downloadVideos[0].files[0].fileDownloadUrl, | ||
517 | downloadVideos[2].files[0].fileDownloadUrl | ||
518 | ] | ||
519 | |||
520 | const res = await makeRawRequest({ url: refused, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
475 | expect(res.body.error).to.equal('Cao Cao') | 521 | expect(res.body.error).to.equal('Cao Cao') |
476 | 522 | ||
477 | await makeRawRequest({ url: downloadVideos[0].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) | 523 | for (const url of allowed) { |
478 | await makeRawRequest({ url: downloadVideos[2].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) | 524 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) |
525 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) | ||
526 | } | ||
479 | } | 527 | } |
480 | 528 | ||
481 | { | 529 | { |
482 | const res = await makeRawRequest({ | 530 | const refused = downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl |
483 | url: downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, | ||
484 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
485 | }) | ||
486 | 531 | ||
487 | expect(res.body.error).to.equal('Sun Jian') | 532 | const allowed = [ |
533 | downloadVideos[2].files[0].fileDownloadUrl, | ||
534 | downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, | ||
535 | downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl | ||
536 | ] | ||
488 | 537 | ||
489 | await makeRawRequest({ url: downloadVideos[2].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) | 538 | // Only streaming playlist is refuse |
539 | const res = await makeRawRequest({ url: refused, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
540 | expect(res.body.error).to.equal('Sun Jian') | ||
490 | 541 | ||
491 | await makeRawRequest({ | 542 | // But not we there is a user in res |
492 | url: downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, | 543 | await makeRawRequest({ url: refused, token: servers[0].accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
493 | expectedStatus: HttpStatusCode.OK_200 | 544 | await makeRawRequest({ url: refused, query: { videoFileToken: downloadVideo2Token }, expectedStatus: HttpStatusCode.OK_200 }) |
494 | }) | ||
495 | 545 | ||
496 | await makeRawRequest({ | 546 | // Other files work |
497 | url: downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, | 547 | for (const url of allowed) { |
498 | expectedStatus: HttpStatusCode.OK_200 | 548 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) |
499 | }) | 549 | } |
500 | } | 550 | } |
501 | }) | 551 | }) |
502 | }) | 552 | }) |
diff --git a/server/tests/plugins/id-and-pass-auth.ts b/server/tests/plugins/id-and-pass-auth.ts index fc24a5656..10155c28b 100644 --- a/server/tests/plugins/id-and-pass-auth.ts +++ b/server/tests/plugins/id-and-pass-auth.ts | |||
@@ -13,6 +13,7 @@ describe('Test id and pass auth plugins', function () { | |||
13 | 13 | ||
14 | let lagunaAccessToken: string | 14 | let lagunaAccessToken: string |
15 | let lagunaRefreshToken: string | 15 | let lagunaRefreshToken: string |
16 | let lagunaId: number | ||
16 | 17 | ||
17 | before(async function () { | 18 | before(async function () { |
18 | this.timeout(30000) | 19 | this.timeout(30000) |
@@ -78,8 +79,10 @@ describe('Test id and pass auth plugins', function () { | |||
78 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) | 79 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) |
79 | 80 | ||
80 | expect(body.username).to.equal('laguna') | 81 | expect(body.username).to.equal('laguna') |
81 | expect(body.account.displayName).to.equal('laguna') | 82 | expect(body.account.displayName).to.equal('Laguna Loire') |
82 | expect(body.role.id).to.equal(UserRole.USER) | 83 | expect(body.role.id).to.equal(UserRole.USER) |
84 | |||
85 | lagunaId = body.id | ||
83 | } | 86 | } |
84 | }) | 87 | }) |
85 | 88 | ||
@@ -132,6 +135,33 @@ describe('Test id and pass auth plugins', function () { | |||
132 | expect(body.role.id).to.equal(UserRole.MODERATOR) | 135 | expect(body.role.id).to.equal(UserRole.MODERATOR) |
133 | }) | 136 | }) |
134 | 137 | ||
138 | it('Should login Laguna and update the profile', async function () { | ||
139 | { | ||
140 | await server.users.update({ userId: lagunaId, videoQuota: 43000, videoQuotaDaily: 43100 }) | ||
141 | await server.users.updateMe({ token: lagunaAccessToken, displayName: 'laguna updated' }) | ||
142 | |||
143 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) | ||
144 | expect(body.username).to.equal('laguna') | ||
145 | expect(body.account.displayName).to.equal('laguna updated') | ||
146 | expect(body.videoQuota).to.equal(43000) | ||
147 | expect(body.videoQuotaDaily).to.equal(43100) | ||
148 | } | ||
149 | |||
150 | { | ||
151 | const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } }) | ||
152 | lagunaAccessToken = body.access_token | ||
153 | lagunaRefreshToken = body.refresh_token | ||
154 | } | ||
155 | |||
156 | { | ||
157 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) | ||
158 | expect(body.username).to.equal('laguna') | ||
159 | expect(body.account.displayName).to.equal('Laguna Loire') | ||
160 | expect(body.videoQuota).to.equal(42000) | ||
161 | expect(body.videoQuotaDaily).to.equal(43100) | ||
162 | } | ||
163 | }) | ||
164 | |||
135 | it('Should reject token of laguna by the plugin hook', async function () { | 165 | it('Should reject token of laguna by the plugin hook', async function () { |
136 | this.timeout(10000) | 166 | this.timeout(10000) |
137 | 167 | ||
@@ -147,7 +177,7 @@ describe('Test id and pass auth plugins', function () { | |||
147 | await server.servers.waitUntilLog('valid username') | 177 | await server.servers.waitUntilLog('valid username') |
148 | 178 | ||
149 | await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 179 | await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
150 | await server.servers.waitUntilLog('valid display name') | 180 | await server.servers.waitUntilLog('valid displayName') |
151 | 181 | ||
152 | await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 182 | await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
153 | await server.servers.waitUntilLog('valid role') | 183 | await server.servers.waitUntilLog('valid role') |
diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts index 038e3f0d6..e25992723 100644 --- a/server/tests/plugins/plugin-helpers.ts +++ b/server/tests/plugins/plugin-helpers.ts | |||
@@ -64,6 +64,18 @@ describe('Test plugin helpers', function () { | |||
64 | await servers[0].servers.waitUntilLog(`server url is ${servers[0].url}`) | 64 | await servers[0].servers.waitUntilLog(`server url is ${servers[0].url}`) |
65 | }) | 65 | }) |
66 | 66 | ||
67 | it('Should have the correct listening config', async function () { | ||
68 | const res = await makeGetRequest({ | ||
69 | url: servers[0].url, | ||
70 | path: '/plugins/test-four/router/server-listening-config', | ||
71 | expectedStatus: HttpStatusCode.OK_200 | ||
72 | }) | ||
73 | |||
74 | expect(res.body.config).to.exist | ||
75 | expect(res.body.config.hostname).to.equal('::') | ||
76 | expect(res.body.config.port).to.equal(servers[0].port) | ||
77 | }) | ||
78 | |||
67 | it('Should have the correct config', async function () { | 79 | it('Should have the correct config', async function () { |
68 | const res = await makeGetRequest({ | 80 | const res = await makeGetRequest({ |
69 | url: servers[0].url, | 81 | url: servers[0].url, |
diff --git a/server/tests/shared/notifications.ts b/server/tests/shared/notifications.ts index e600bd6b2..6c0688d5a 100644 --- a/server/tests/shared/notifications.ts +++ b/server/tests/shared/notifications.ts | |||
@@ -11,6 +11,7 @@ import { | |||
11 | UserNotificationType | 11 | UserNotificationType |
12 | } from '@shared/models' | 12 | } from '@shared/models' |
13 | import { | 13 | import { |
14 | ConfigCommand, | ||
14 | createMultipleServers, | 15 | createMultipleServers, |
15 | doubleFollow, | 16 | doubleFollow, |
16 | PeerTubeServer, | 17 | PeerTubeServer, |
@@ -173,6 +174,8 @@ async function checkMyVideoImportIsFinished (options: CheckerBaseParams & { | |||
173 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | 174 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) |
174 | } | 175 | } |
175 | 176 | ||
177 | // --------------------------------------------------------------------------- | ||
178 | |||
176 | async function checkUserRegistered (options: CheckerBaseParams & { | 179 | async function checkUserRegistered (options: CheckerBaseParams & { |
177 | username: string | 180 | username: string |
178 | checkType: CheckerType | 181 | checkType: CheckerType |
@@ -201,6 +204,36 @@ async function checkUserRegistered (options: CheckerBaseParams & { | |||
201 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | 204 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) |
202 | } | 205 | } |
203 | 206 | ||
207 | async function checkRegistrationRequest (options: CheckerBaseParams & { | ||
208 | username: string | ||
209 | registrationReason: string | ||
210 | checkType: CheckerType | ||
211 | }) { | ||
212 | const { username, registrationReason } = options | ||
213 | const notificationType = UserNotificationType.NEW_USER_REGISTRATION_REQUEST | ||
214 | |||
215 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
216 | if (checkType === 'presence') { | ||
217 | expect(notification).to.not.be.undefined | ||
218 | expect(notification.type).to.equal(notificationType) | ||
219 | |||
220 | expect(notification.registration.username).to.equal(username) | ||
221 | } else { | ||
222 | expect(notification).to.satisfy(n => n.type !== notificationType || n.registration.username !== username) | ||
223 | } | ||
224 | } | ||
225 | |||
226 | function emailNotificationFinder (email: object) { | ||
227 | const text: string = email['text'] | ||
228 | |||
229 | return text.includes(' wants to register ') && text.includes(username) && text.includes(registrationReason) | ||
230 | } | ||
231 | |||
232 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
233 | } | ||
234 | |||
235 | // --------------------------------------------------------------------------- | ||
236 | |||
204 | async function checkNewActorFollow (options: CheckerBaseParams & { | 237 | async function checkNewActorFollow (options: CheckerBaseParams & { |
205 | followType: 'channel' | 'account' | 238 | followType: 'channel' | 'account' |
206 | followerName: string | 239 | followerName: string |
@@ -673,10 +706,8 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an | |||
673 | const port = await MockSmtpServer.Instance.collectEmails(emails) | 706 | const port = await MockSmtpServer.Instance.collectEmails(emails) |
674 | 707 | ||
675 | const overrideConfig = { | 708 | const overrideConfig = { |
676 | smtp: { | 709 | ...ConfigCommand.getEmailOverrideConfig(port), |
677 | hostname: '127.0.0.1', | 710 | |
678 | port | ||
679 | }, | ||
680 | signup: { | 711 | signup: { |
681 | limit: 20 | 712 | limit: 20 |
682 | } | 713 | } |
@@ -735,7 +766,8 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an | |||
735 | userAccessToken, | 766 | userAccessToken, |
736 | emails, | 767 | emails, |
737 | servers, | 768 | servers, |
738 | channelId | 769 | channelId, |
770 | baseOverrideConfig: overrideConfig | ||
739 | } | 771 | } |
740 | } | 772 | } |
741 | 773 | ||
@@ -765,7 +797,8 @@ export { | |||
765 | checkNewAccountAbuseForModerators, | 797 | checkNewAccountAbuseForModerators, |
766 | checkNewPeerTubeVersion, | 798 | checkNewPeerTubeVersion, |
767 | checkNewPluginVersion, | 799 | checkNewPluginVersion, |
768 | checkVideoStudioEditionIsFinished | 800 | checkVideoStudioEditionIsFinished, |
801 | checkRegistrationRequest | ||
769 | } | 802 | } |
770 | 803 | ||
771 | // --------------------------------------------------------------------------- | 804 | // --------------------------------------------------------------------------- |
diff --git a/server/tests/shared/videos.ts b/server/tests/shared/videos.ts index c8339584b..f8ec65752 100644 --- a/server/tests/shared/videos.ts +++ b/server/tests/shared/videos.ts | |||
@@ -59,7 +59,7 @@ async function completeVideoCheck ( | |||
59 | 59 | ||
60 | expect(video.name).to.equal(attributes.name) | 60 | expect(video.name).to.equal(attributes.name) |
61 | expect(video.category.id).to.equal(attributes.category) | 61 | expect(video.category.id).to.equal(attributes.category) |
62 | expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc') | 62 | expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Unknown') |
63 | expect(video.licence.id).to.equal(attributes.licence) | 63 | expect(video.licence.id).to.equal(attributes.licence) |
64 | expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown') | 64 | expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown') |
65 | expect(video.language.id).to.equal(attributes.language) | 65 | expect(video.language.id).to.equal(attributes.language) |
diff --git a/server/types/express.d.ts b/server/types/express.d.ts index 3738ffc47..c1c379b98 100644 --- a/server/types/express.d.ts +++ b/server/types/express.d.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | |||
2 | import { OutgoingHttpHeaders } from 'http' | 1 | import { OutgoingHttpHeaders } from 'http' |
3 | import { RegisterServerAuthExternalOptions } from '@server/types' | 2 | import { RegisterServerAuthExternalOptions } from '@server/types' |
4 | import { | 3 | import { |
@@ -9,7 +8,9 @@ import { | |||
9 | MActorUrl, | 8 | MActorUrl, |
10 | MChannelBannerAccountDefault, | 9 | MChannelBannerAccountDefault, |
11 | MChannelSyncChannel, | 10 | MChannelSyncChannel, |
11 | MRegistration, | ||
12 | MStreamingPlaylist, | 12 | MStreamingPlaylist, |
13 | MUserAccountUrl, | ||
13 | MVideoChangeOwnershipFull, | 14 | MVideoChangeOwnershipFull, |
14 | MVideoFile, | 15 | MVideoFile, |
15 | MVideoFormattableDetails, | 16 | MVideoFormattableDetails, |
@@ -171,6 +172,7 @@ declare module 'express' { | |||
171 | actorFull?: MActorFull | 172 | actorFull?: MActorFull |
172 | 173 | ||
173 | user?: MUserDefault | 174 | user?: MUserDefault |
175 | userRegistration?: MRegistration | ||
174 | 176 | ||
175 | server?: MServer | 177 | server?: MServer |
176 | 178 | ||
@@ -187,6 +189,10 @@ declare module 'express' { | |||
187 | actor: MActorAccountChannelId | 189 | actor: MActorAccountChannelId |
188 | } | 190 | } |
189 | 191 | ||
192 | videoFileToken?: { | ||
193 | user: MUserAccountUrl | ||
194 | } | ||
195 | |||
190 | authenticated?: boolean | 196 | authenticated?: boolean |
191 | 197 | ||
192 | registeredPlugin?: RegisteredPlugin | 198 | registeredPlugin?: RegisteredPlugin |
diff --git a/server/types/lib.d.ts b/server/types/lib.d.ts new file mode 100644 index 000000000..c901e2032 --- /dev/null +++ b/server/types/lib.d.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | type ObjectKeys<T> = | ||
2 | T extends object | ||
3 | ? `${Exclude<keyof T, symbol>}`[] | ||
4 | : T extends number | ||
5 | ? [] | ||
6 | : T extends any | string | ||
7 | ? string[] | ||
8 | : never | ||
9 | |||
10 | interface ObjectConstructor { | ||
11 | keys<T> (o: T): ObjectKeys<T> | ||
12 | } | ||
diff --git a/server/types/models/user/index.ts b/server/types/models/user/index.ts index 6657b2128..5738f4107 100644 --- a/server/types/models/user/index.ts +++ b/server/types/models/user/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './user' | 1 | export * from './user' |
2 | export * from './user-notification' | 2 | export * from './user-notification' |
3 | export * from './user-notification-setting' | 3 | export * from './user-notification-setting' |
4 | export * from './user-registration' | ||
4 | export * from './user-video-history' | 5 | export * from './user-video-history' |
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts index d4715a0b6..a732c8aa9 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts | |||
@@ -3,6 +3,7 @@ import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse | |||
3 | import { ApplicationModel } from '@server/models/application/application' | 3 | import { ApplicationModel } from '@server/models/application/application' |
4 | import { PluginModel } from '@server/models/server/plugin' | 4 | import { PluginModel } from '@server/models/server/plugin' |
5 | import { UserNotificationModel } from '@server/models/user/user-notification' | 5 | import { UserNotificationModel } from '@server/models/user/user-notification' |
6 | import { UserRegistrationModel } from '@server/models/user/user-registration' | ||
6 | import { PickWith, PickWithOpt } from '@shared/typescript-utils' | 7 | import { PickWith, PickWithOpt } from '@shared/typescript-utils' |
7 | import { AbuseModel } from '../../../models/abuse/abuse' | 8 | import { AbuseModel } from '../../../models/abuse/abuse' |
8 | import { AccountModel } from '../../../models/account/account' | 9 | import { AccountModel } from '../../../models/account/account' |
@@ -94,13 +95,16 @@ export module UserNotificationIncludes { | |||
94 | 95 | ||
95 | export type ApplicationInclude = | 96 | export type ApplicationInclude = |
96 | Pick<ApplicationModel, 'latestPeerTubeVersion'> | 97 | Pick<ApplicationModel, 'latestPeerTubeVersion'> |
98 | |||
99 | export type UserRegistrationInclude = | ||
100 | Pick<UserRegistrationModel, 'id' | 'username'> | ||
97 | } | 101 | } |
98 | 102 | ||
99 | // ############################################################################ | 103 | // ############################################################################ |
100 | 104 | ||
101 | export type MUserNotification = | 105 | export type MUserNotification = |
102 | Omit<UserNotificationModel, 'User' | 'Video' | 'VideoComment' | 'Abuse' | 'VideoBlacklist' | | 106 | Omit<UserNotificationModel, 'User' | 'Video' | 'VideoComment' | 'Abuse' | 'VideoBlacklist' | |
103 | 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'> | 107 | 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application' | 'UserRegistration'> |
104 | 108 | ||
105 | // ############################################################################ | 109 | // ############################################################################ |
106 | 110 | ||
@@ -114,4 +118,5 @@ export type UserNotificationModelForApi = | |||
114 | Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & | 118 | Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & |
115 | Use<'Plugin', UserNotificationIncludes.PluginInclude> & | 119 | Use<'Plugin', UserNotificationIncludes.PluginInclude> & |
116 | Use<'Application', UserNotificationIncludes.ApplicationInclude> & | 120 | Use<'Application', UserNotificationIncludes.ApplicationInclude> & |
117 | Use<'Account', UserNotificationIncludes.AccountIncludeActor> | 121 | Use<'Account', UserNotificationIncludes.AccountIncludeActor> & |
122 | Use<'UserRegistration', UserNotificationIncludes.UserRegistrationInclude> | ||
diff --git a/server/types/models/user/user-registration.ts b/server/types/models/user/user-registration.ts new file mode 100644 index 000000000..216423cc9 --- /dev/null +++ b/server/types/models/user/user-registration.ts | |||
@@ -0,0 +1,15 @@ | |||
1 | import { UserRegistrationModel } from '@server/models/user/user-registration' | ||
2 | import { PickWith } from '@shared/typescript-utils' | ||
3 | import { MUserId } from './user' | ||
4 | |||
5 | type Use<K extends keyof UserRegistrationModel, M> = PickWith<UserRegistrationModel, K, M> | ||
6 | |||
7 | // ############################################################################ | ||
8 | |||
9 | export type MRegistration = Omit<UserRegistrationModel, 'User'> | ||
10 | |||
11 | // ############################################################################ | ||
12 | |||
13 | export type MRegistrationFormattable = | ||
14 | MRegistration & | ||
15 | Use<'User', MUserId> | ||
diff --git a/server/types/plugins/register-server-auth.model.ts b/server/types/plugins/register-server-auth.model.ts index 79c18c406..e10968c20 100644 --- a/server/types/plugins/register-server-auth.model.ts +++ b/server/types/plugins/register-server-auth.model.ts | |||
@@ -1,14 +1,33 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { UserRole } from '@shared/models' | 2 | import { UserAdminFlag, UserRole } from '@shared/models' |
3 | import { MOAuthToken, MUser } from '../models' | 3 | import { MOAuthToken, MUser } from '../models' |
4 | 4 | ||
5 | export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions | 5 | export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions |
6 | 6 | ||
7 | export type AuthenticatedResultUpdaterFieldName = 'displayName' | 'role' | 'adminFlags' | 'videoQuota' | 'videoQuotaDaily' | ||
8 | |||
7 | export interface RegisterServerAuthenticatedResult { | 9 | export interface RegisterServerAuthenticatedResult { |
10 | // Update the user profile if it already exists | ||
11 | // Default behaviour is no update | ||
12 | // Introduced in PeerTube >= 5.1 | ||
13 | userUpdater?: <T> (options: { | ||
14 | fieldName: AuthenticatedResultUpdaterFieldName | ||
15 | currentValue: T | ||
16 | newValue: T | ||
17 | }) => T | ||
18 | |||
8 | username: string | 19 | username: string |
9 | email: string | 20 | email: string |
10 | role?: UserRole | 21 | role?: UserRole |
11 | displayName?: string | 22 | displayName?: string |
23 | |||
24 | // PeerTube >= 5.1 | ||
25 | adminFlags?: UserAdminFlag | ||
26 | |||
27 | // PeerTube >= 5.1 | ||
28 | videoQuota?: number | ||
29 | // PeerTube >= 5.1 | ||
30 | videoQuotaDaily?: number | ||
12 | } | 31 | } |
13 | 32 | ||
14 | export interface RegisterServerExternalAuthenticatedResult extends RegisterServerAuthenticatedResult { | 33 | export interface RegisterServerExternalAuthenticatedResult extends RegisterServerAuthenticatedResult { |
diff --git a/server/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts index 1e2bd830e..df419fff4 100644 --- a/server/types/plugins/register-server-option.model.ts +++ b/server/types/plugins/register-server-option.model.ts | |||
@@ -71,6 +71,9 @@ export type PeerTubeHelpers = { | |||
71 | config: { | 71 | config: { |
72 | getWebserverUrl: () => string | 72 | getWebserverUrl: () => string |
73 | 73 | ||
74 | // PeerTube >= 5.1 | ||
75 | getServerListeningConfig: () => { hostname: string, port: number } | ||
76 | |||
74 | getServerConfig: () => Promise<ServerConfig> | 77 | getServerConfig: () => Promise<ServerConfig> |
75 | } | 78 | } |
76 | 79 | ||
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..a1740dcca --- /dev/null +++ b/shared/models/users/registration/user-registration-update-state.model.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export 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 @@ | |||
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..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' | |||
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..f57f54b34 --- /dev/null +++ b/shared/server-commands/users/registrations-command.ts | |||
@@ -0,0 +1,151 @@ | |||
1 | import { pick } from '@shared/core-utils' | ||
2 | import { HttpStatusCode, ResultList, UserRegistration, UserRegistrationRequest, UserRegistrationUpdateState } 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 & { 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 = {}) { |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index bfa7235a2..f90b7f575 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -1,7 +1,7 @@ | |||
1 | openapi: 3.0.0 | 1 | openapi: 3.0.0 |
2 | info: | 2 | info: |
3 | title: PeerTube | 3 | title: PeerTube |
4 | version: 4.0.0 | 4 | version: 5.1.0 |
5 | contact: | 5 | contact: |
6 | name: PeerTube Community | 6 | name: PeerTube Community |
7 | url: https://joinpeertube.org | 7 | url: https://joinpeertube.org |
@@ -1401,22 +1401,44 @@ paths: | |||
1401 | '200': | 1401 | '200': |
1402 | description: successful operation | 1402 | description: successful operation |
1403 | 1403 | ||
1404 | /api/v1/users/register: | 1404 | /api/v1/users/ask-send-verify-email: |
1405 | post: | 1405 | post: |
1406 | summary: Register a user | 1406 | summary: Resend user verification link |
1407 | operationId: registerUser | 1407 | operationId: resendEmailToVerifyUser |
1408 | tags: | 1408 | tags: |
1409 | - Users | 1409 | - Users |
1410 | - Register | 1410 | - Register |
1411 | requestBody: | ||
1412 | content: | ||
1413 | application/json: | ||
1414 | schema: | ||
1415 | type: object | ||
1416 | properties: | ||
1417 | email: | ||
1418 | type: string | ||
1419 | description: User email | ||
1411 | responses: | 1420 | responses: |
1412 | '204': | 1421 | '204': |
1413 | description: successful operation | 1422 | description: successful operation |
1423 | |||
1424 | /api/v1/users/registrations/ask-send-verify-email: | ||
1425 | post: | ||
1426 | summary: Resend verification link to registration email | ||
1427 | operationId: resendEmailToVerifyRegistration | ||
1428 | tags: | ||
1429 | - Register | ||
1414 | requestBody: | 1430 | requestBody: |
1415 | content: | 1431 | content: |
1416 | application/json: | 1432 | application/json: |
1417 | schema: | 1433 | schema: |
1418 | $ref: '#/components/schemas/RegisterUser' | 1434 | type: object |
1419 | required: true | 1435 | properties: |
1436 | email: | ||
1437 | type: string | ||
1438 | description: Registration email | ||
1439 | responses: | ||
1440 | '204': | ||
1441 | description: successful operation | ||
1420 | 1442 | ||
1421 | /api/v1/users/{id}/verify-email: | 1443 | /api/v1/users/{id}/verify-email: |
1422 | post: | 1444 | post: |
@@ -1425,6 +1447,7 @@ paths: | |||
1425 | description: | | 1447 | description: | |
1426 | Following a user registration, the new user will receive an email asking to click a link | 1448 | Following a user registration, the new user will receive an email asking to click a link |
1427 | containing a secret. | 1449 | containing a secret. |
1450 | This endpoint can also be used to verify a new email set in the user account. | ||
1428 | tags: | 1451 | tags: |
1429 | - Users | 1452 | - Users |
1430 | - Register | 1453 | - Register |
@@ -1451,6 +1474,36 @@ paths: | |||
1451 | '404': | 1474 | '404': |
1452 | description: user not found | 1475 | description: user not found |
1453 | 1476 | ||
1477 | /api/v1/users/registrations/{registrationId}/verify-email: | ||
1478 | post: | ||
1479 | summary: Verify a registration email | ||
1480 | operationId: verifyRegistrationEmail | ||
1481 | description: | | ||
1482 | Following a user registration request, the user will receive an email asking to click a link | ||
1483 | containing a secret. | ||
1484 | tags: | ||
1485 | - Register | ||
1486 | parameters: | ||
1487 | - $ref: '#/components/parameters/registrationId' | ||
1488 | requestBody: | ||
1489 | content: | ||
1490 | application/json: | ||
1491 | schema: | ||
1492 | type: object | ||
1493 | properties: | ||
1494 | verificationString: | ||
1495 | type: string | ||
1496 | format: url | ||
1497 | required: | ||
1498 | - verificationString | ||
1499 | responses: | ||
1500 | '204': | ||
1501 | description: successful operation | ||
1502 | '403': | ||
1503 | description: invalid verification string | ||
1504 | '404': | ||
1505 | description: registration not found | ||
1506 | |||
1454 | /api/v1/users/{id}/two-factor/request: | 1507 | /api/v1/users/{id}/two-factor/request: |
1455 | post: | 1508 | post: |
1456 | summary: Request two factor auth | 1509 | summary: Request two factor auth |
@@ -1541,18 +1594,6 @@ paths: | |||
1541 | '404': | 1594 | '404': |
1542 | description: user not found | 1595 | description: user not found |
1543 | 1596 | ||
1544 | |||
1545 | /api/v1/users/ask-send-verify-email: | ||
1546 | post: | ||
1547 | summary: Resend user verification link | ||
1548 | operationId: resendEmailToVerifyUser | ||
1549 | tags: | ||
1550 | - Users | ||
1551 | - Register | ||
1552 | responses: | ||
1553 | '204': | ||
1554 | description: successful operation | ||
1555 | |||
1556 | /api/v1/users/me: | 1597 | /api/v1/users/me: |
1557 | get: | 1598 | get: |
1558 | summary: Get my user information | 1599 | summary: Get my user information |
@@ -2037,6 +2078,146 @@ paths: | |||
2037 | '204': | 2078 | '204': |
2038 | description: successful operation | 2079 | description: successful operation |
2039 | 2080 | ||
2081 | /api/v1/users/register: | ||
2082 | post: | ||
2083 | summary: Register a user | ||
2084 | operationId: registerUser | ||
2085 | description: Signup has to be enabled and signup approval is not required | ||
2086 | tags: | ||
2087 | - Register | ||
2088 | responses: | ||
2089 | '204': | ||
2090 | description: successful operation | ||
2091 | '400': | ||
2092 | description: request error | ||
2093 | '403': | ||
2094 | description: user registration is not enabled, user limit is reached, registration is not allowed for the ip, requires approval or blocked by a plugin | ||
2095 | '409': | ||
2096 | description: 'a user with this username, channel name or email already exists' | ||
2097 | requestBody: | ||
2098 | content: | ||
2099 | application/json: | ||
2100 | schema: | ||
2101 | $ref: '#/components/schemas/RegisterUser' | ||
2102 | required: true | ||
2103 | |||
2104 | /api/v1/users/registrations/request: | ||
2105 | post: | ||
2106 | summary: Request registration | ||
2107 | description: Signup has to be enabled and require approval on the instance | ||
2108 | operationId: requestRegistration | ||
2109 | tags: | ||
2110 | - Register | ||
2111 | responses: | ||
2112 | '200': | ||
2113 | description: successful operation | ||
2114 | content: | ||
2115 | application/json: | ||
2116 | schema: | ||
2117 | $ref: '#/components/schemas/UserRegistration' | ||
2118 | '400': | ||
2119 | description: request error or signup approval is not enabled on the instance | ||
2120 | '403': | ||
2121 | description: user registration is not enabled, user limit is reached, registration is not allowed for the ip or blocked by a plugin | ||
2122 | '409': | ||
2123 | description: 'a user or registration with this username, channel name or email already exists' | ||
2124 | requestBody: | ||
2125 | content: | ||
2126 | application/json: | ||
2127 | schema: | ||
2128 | $ref: '#/components/schemas/UserRegistrationRequest' | ||
2129 | |||
2130 | /api/v1/users/registrations/{registrationId}/accept: | ||
2131 | post: | ||
2132 | security: | ||
2133 | - OAuth2: | ||
2134 | - admin | ||
2135 | - moderator | ||
2136 | summary: Accept registration | ||
2137 | operationId: acceptRegistration | ||
2138 | tags: | ||
2139 | - Register | ||
2140 | parameters: | ||
2141 | - $ref: '#/components/parameters/registrationId' | ||
2142 | requestBody: | ||
2143 | content: | ||
2144 | application/json: | ||
2145 | schema: | ||
2146 | $ref: '#/components/schemas/UserRegistrationAcceptOrReject' | ||
2147 | responses: | ||
2148 | '204': | ||
2149 | description: successful operation | ||
2150 | |||
2151 | /api/v1/users/registrations/{registrationId}/reject: | ||
2152 | post: | ||
2153 | security: | ||
2154 | - OAuth2: | ||
2155 | - admin | ||
2156 | - moderator | ||
2157 | summary: Reject registration | ||
2158 | operationId: rejectRegistration | ||
2159 | tags: | ||
2160 | - Register | ||
2161 | parameters: | ||
2162 | - $ref: '#/components/parameters/registrationId' | ||
2163 | requestBody: | ||
2164 | content: | ||
2165 | application/json: | ||
2166 | schema: | ||
2167 | $ref: '#/components/schemas/UserRegistrationAcceptOrReject' | ||
2168 | responses: | ||
2169 | '204': | ||
2170 | description: successful operation | ||
2171 | |||
2172 | /api/v1/users/registrations/{registrationId}: | ||
2173 | delete: | ||
2174 | security: | ||
2175 | - OAuth2: | ||
2176 | - admin | ||
2177 | - moderator | ||
2178 | summary: Delete registration | ||
2179 | description: 'Delete the registration entry. It will not remove the user associated with this registration (if any)' | ||
2180 | operationId: deleteRegistration | ||
2181 | tags: | ||
2182 | - Register | ||
2183 | parameters: | ||
2184 | - $ref: '#/components/parameters/registrationId' | ||
2185 | responses: | ||
2186 | '204': | ||
2187 | description: successful operation | ||
2188 | |||
2189 | /api/v1/users/registrations: | ||
2190 | get: | ||
2191 | security: | ||
2192 | - OAuth2: | ||
2193 | - admin | ||
2194 | - moderator | ||
2195 | summary: List registrations | ||
2196 | operationId: listRegistrations | ||
2197 | tags: | ||
2198 | - Register | ||
2199 | parameters: | ||
2200 | - $ref: '#/components/parameters/start' | ||
2201 | - $ref: '#/components/parameters/count' | ||
2202 | - name: search | ||
2203 | in: query | ||
2204 | required: false | ||
2205 | schema: | ||
2206 | type: string | ||
2207 | - name: sort | ||
2208 | in: query | ||
2209 | required: false | ||
2210 | schema: | ||
2211 | type: string | ||
2212 | enum: | ||
2213 | - -createdAt | ||
2214 | - createdAt | ||
2215 | - state | ||
2216 | - -state | ||
2217 | responses: | ||
2218 | '204': | ||
2219 | description: successful operation | ||
2220 | |||
2040 | /api/v1/videos/ownership: | 2221 | /api/v1/videos/ownership: |
2041 | get: | 2222 | get: |
2042 | summary: List video ownership changes | 2223 | summary: List video ownership changes |
@@ -5389,6 +5570,7 @@ components: | |||
5389 | type: string | 5570 | type: string |
5390 | enum: | 5571 | enum: |
5391 | - createdAt | 5572 | - createdAt |
5573 | |||
5392 | name: | 5574 | name: |
5393 | name: name | 5575 | name: name |
5394 | in: path | 5576 | in: path |
@@ -5404,6 +5586,13 @@ components: | |||
5404 | description: Entity id | 5586 | description: Entity id |
5405 | schema: | 5587 | schema: |
5406 | $ref: '#/components/schemas/id' | 5588 | $ref: '#/components/schemas/id' |
5589 | registrationId: | ||
5590 | name: registrationId | ||
5591 | in: path | ||
5592 | required: true | ||
5593 | description: Registration ID | ||
5594 | schema: | ||
5595 | $ref: '#/components/schemas/id' | ||
5407 | idOrUUID: | 5596 | idOrUUID: |
5408 | name: id | 5597 | name: id |
5409 | in: path | 5598 | in: path |
@@ -7724,6 +7913,7 @@ components: | |||
7724 | required: | 7913 | required: |
7725 | - video | 7914 | - video |
7726 | - rating | 7915 | - rating |
7916 | |||
7727 | RegisterUser: | 7917 | RegisterUser: |
7728 | properties: | 7918 | properties: |
7729 | username: | 7919 | username: |
@@ -7754,6 +7944,77 @@ components: | |||
7754 | - password | 7944 | - password |
7755 | 7945 | ||
7756 | 7946 | ||
7947 | UserRegistrationRequest: | ||
7948 | allOf: | ||
7949 | - $ref: '#/components/schemas/RegisterUser' | ||
7950 | - type: object | ||
7951 | properties: | ||
7952 | registrationReason: | ||
7953 | type: string | ||
7954 | description: reason for the user to register on the instance | ||
7955 | required: | ||
7956 | - registrationReason | ||
7957 | |||
7958 | UserRegistrationAcceptOrReject: | ||
7959 | type: object | ||
7960 | properties: | ||
7961 | moderationResponse: | ||
7962 | type: string | ||
7963 | description: Moderation response to send to the user | ||
7964 | preventEmailDelivery: | ||
7965 | type: boolean | ||
7966 | description: Set it to true if you don't want PeerTube to send an email to the user | ||
7967 | required: | ||
7968 | - moderationResponse | ||
7969 | |||
7970 | UserRegistration: | ||
7971 | properties: | ||
7972 | id: | ||
7973 | $ref: '#/components/schemas/id' | ||
7974 | state: | ||
7975 | type: object | ||
7976 | properties: | ||
7977 | id: | ||
7978 | type: integer | ||
7979 | enum: | ||
7980 | - 1 | ||
7981 | - 2 | ||
7982 | - 3 | ||
7983 | description: 'The registration state (Pending = `1`, Rejected = `2`, Accepted = `3`)' | ||
7984 | label: | ||
7985 | type: string | ||
7986 | registrationReason: | ||
7987 | type: string | ||
7988 | moderationResponse: | ||
7989 | type: string | ||
7990 | nullable: true | ||
7991 | username: | ||
7992 | type: string | ||
7993 | email: | ||
7994 | type: string | ||
7995 | format: email | ||
7996 | emailVerified: | ||
7997 | type: boolean | ||
7998 | accountDisplayName: | ||
7999 | type: string | ||
8000 | channelHandle: | ||
8001 | type: string | ||
8002 | channelDisplayName: | ||
8003 | type: string | ||
8004 | createdAt: | ||
8005 | type: string | ||
8006 | format: date-time | ||
8007 | updatedAt: | ||
8008 | type: string | ||
8009 | format: date-time | ||
8010 | user: | ||
8011 | type: object | ||
8012 | nullable: true | ||
8013 | description: If the registration has been accepted, this is a partial user object created by the registration | ||
8014 | properties: | ||
8015 | id: | ||
8016 | $ref: '#/components/schemas/id' | ||
8017 | |||
7757 | OAuthClient: | 8018 | OAuthClient: |
7758 | properties: | 8019 | properties: |
7759 | client_id: | 8020 | client_id: |
diff --git a/support/doc/dependencies.md b/support/doc/dependencies.md index bf53b8080..5cf1d5879 100644 --- a/support/doc/dependencies.md +++ b/support/doc/dependencies.md | |||
@@ -2,8 +2,6 @@ | |||
2 | 2 | ||
3 | :warning: **Warning**: dependencies guide is maintained by the community. Some parts may be outdated! :warning: | 3 | :warning: **Warning**: dependencies guide is maintained by the community. Some parts may be outdated! :warning: |
4 | 4 | ||
5 | Follow the below guides, and check their versions match [required external dependencies versions](https://github.com/Chocobozzz/PeerTube/blob/master/engines.yaml). | ||
6 | |||
7 | Main dependencies version supported by PeerTube: | 5 | Main dependencies version supported by PeerTube: |
8 | 6 | ||
9 | * `node` >=14.x | 7 | * `node` >=14.x |
diff --git a/support/doc/docker.md b/support/doc/docker.md index 267863a4d..b6990f3e3 100644 --- a/support/doc/docker.md +++ b/support/doc/docker.md | |||
@@ -120,7 +120,7 @@ See the production guide ["What now" section](https://docs.joinpeertube.org/inst | |||
120 | 120 | ||
121 | ## Upgrade | 121 | ## Upgrade |
122 | 122 | ||
123 | **Important:** Before upgrading, check you have all the `storage` fields in your [production.yaml file](https://github.com/Chocobozzz/PeerTube/blob/master/support/docker/production/config/production.yaml). | 123 | **Check the changelog (in particular the *IMPORTANT NOTES* section):** https://github.com/Chocobozzz/PeerTube/blob/develop/CHANGELOG.md |
124 | 124 | ||
125 | Pull the latest images: | 125 | Pull the latest images: |
126 | 126 | ||
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md index a1131ced5..9ddab3ece 100644 --- a/support/doc/plugins/guide.md +++ b/support/doc/plugins/guide.md | |||
@@ -433,7 +433,27 @@ function register (...) { | |||
433 | username: 'user' | 433 | username: 'user' |
434 | email: 'user@example.com' | 434 | email: 'user@example.com' |
435 | role: 2 | 435 | role: 2 |
436 | displayName: 'User display name' | 436 | displayName: 'User display name', |
437 | |||
438 | // Custom admin flags (bypass video auto moderation etc.) | ||
439 | // https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/users/user-flag.model.ts | ||
440 | // PeerTube >= 5.1 | ||
441 | adminFlags: 0, | ||
442 | // Quota in bytes | ||
443 | // PeerTube >= 5.1 | ||
444 | videoQuota: 1024 * 1024 * 1024, // 1GB | ||
445 | // PeerTube >= 5.1 | ||
446 | videoQuotaDaily: -1, // Unlimited | ||
447 | |||
448 | // Update the user profile if it already exists | ||
449 | // Default behaviour is no update | ||
450 | // Introduced in PeerTube >= 5.1 | ||
451 | userUpdater: ({ fieldName, currentValue, newValue }) => { | ||
452 | // Always use new value except for videoQuotaDaily field | ||
453 | if (fieldName === 'videoQuotaDaily') return currentValue | ||
454 | |||
455 | return newValue | ||
456 | } | ||
437 | }) | 457 | }) |
438 | }) | 458 | }) |
439 | 459 | ||
diff --git a/support/doc/production.md b/support/doc/production.md index dd57e9120..9a84f19a3 100644 --- a/support/doc/production.md +++ b/support/doc/production.md | |||
@@ -177,16 +177,17 @@ $ sudo vim /etc/letsencrypt/renewal/your-domain.com.conf | |||
177 | 177 | ||
178 | If you plan to have many concurrent viewers on your PeerTube instance, consider increasing `worker_connections` value: https://nginx.org/en/docs/ngx_core_module.html#worker_connections. | 178 | If you plan to have many concurrent viewers on your PeerTube instance, consider increasing `worker_connections` value: https://nginx.org/en/docs/ngx_core_module.html#worker_connections. |
179 | 179 | ||
180 | **FreeBSD** | 180 | <details> |
181 | <summary><strong>If using FreeBSD</strong></summary> | ||
182 | |||
181 | On FreeBSD you can use [Dehydrated](https://dehydrated.io/) `security/dehydrated` for [Let's Encrypt](https://letsencrypt.org/) | 183 | On FreeBSD you can use [Dehydrated](https://dehydrated.io/) `security/dehydrated` for [Let's Encrypt](https://letsencrypt.org/) |
182 | 184 | ||
183 | ```bash | 185 | ```bash |
184 | $ sudo pkg install dehydrated | 186 | $ sudo pkg install dehydrated |
185 | ``` | 187 | ``` |
188 | </details> | ||
186 | 189 | ||
187 | ### :alembic: TCP/IP Tuning | 190 | ### :alembic: Linux TCP/IP Tuning |
188 | |||
189 | **On Linux** | ||
190 | 191 | ||
191 | ```bash | 192 | ```bash |
192 | $ sudo cp /var/www/peertube/peertube-latest/support/sysctl.d/30-peertube-tcp.conf /etc/sysctl.d/ | 193 | $ sudo cp /var/www/peertube/peertube-latest/support/sysctl.d/30-peertube-tcp.conf /etc/sysctl.d/ |
@@ -231,7 +232,9 @@ $ sudo systemctl start peertube | |||
231 | $ sudo journalctl -feu peertube | 232 | $ sudo journalctl -feu peertube |
232 | ``` | 233 | ``` |
233 | 234 | ||
234 | **FreeBSD** | 235 | <details> |
236 | <summary><strong>If using FreeBSD</strong></summary> | ||
237 | |||
235 | On FreeBSD, copy the startup script and update rc.conf: | 238 | On FreeBSD, copy the startup script and update rc.conf: |
236 | 239 | ||
237 | ```bash | 240 | ```bash |
@@ -244,8 +247,10 @@ Run: | |||
244 | ```bash | 247 | ```bash |
245 | $ sudo service peertube start | 248 | $ sudo service peertube start |
246 | ``` | 249 | ``` |
250 | </details> | ||
247 | 251 | ||
248 | ### :bricks: OpenRC | 252 | <details> |
253 | <summary><strong>If using OpenRC</strong></summary> | ||
249 | 254 | ||
250 | If your OS uses OpenRC, copy the service script: | 255 | If your OS uses OpenRC, copy the service script: |
251 | 256 | ||
@@ -265,6 +270,7 @@ Run and print last logs: | |||
265 | $ sudo /etc/init.d/peertube start | 270 | $ sudo /etc/init.d/peertube start |
266 | $ tail -f /var/log/peertube/peertube.log | 271 | $ tail -f /var/log/peertube/peertube.log |
267 | ``` | 272 | ``` |
273 | </details> | ||
268 | 274 | ||
269 | ### :technologist: Administrator | 275 | ### :technologist: Administrator |
270 | 276 | ||
@@ -291,16 +297,15 @@ Now your instance is up you can: | |||
291 | 297 | ||
292 | **Check the changelog (in particular the *IMPORTANT NOTES* section):** https://github.com/Chocobozzz/PeerTube/blob/develop/CHANGELOG.md | 298 | **Check the changelog (in particular the *IMPORTANT NOTES* section):** https://github.com/Chocobozzz/PeerTube/blob/develop/CHANGELOG.md |
293 | 299 | ||
294 | #### Auto | 300 | Run the upgrade script (the password it asks is PeerTube's database user password): |
295 | |||
296 | The password it asks is PeerTube's database user password. | ||
297 | 301 | ||
298 | ```bash | 302 | ```bash |
299 | $ cd /var/www/peertube/peertube-latest/scripts && sudo -H -u peertube ./upgrade.sh | 303 | $ cd /var/www/peertube/peertube-latest/scripts && sudo -H -u peertube ./upgrade.sh |
300 | $ sudo systemctl restart peertube # Or use your OS command to restart PeerTube if you don't use systemd | 304 | $ sudo systemctl restart peertube # Or use your OS command to restart PeerTube if you don't use systemd |
301 | ``` | 305 | ``` |
302 | 306 | ||
303 | #### Manually | 307 | <details> |
308 | <summary><strong>Prefer manual upgrade?</strong></summary> | ||
304 | 309 | ||
305 | Make a SQL backup | 310 | Make a SQL backup |
306 | 311 | ||
@@ -346,17 +351,18 @@ $ cd /var/www/peertube && \ | |||
346 | sudo unlink ./peertube-latest && \ | 351 | sudo unlink ./peertube-latest && \ |
347 | sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest | 352 | sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest |
348 | ``` | 353 | ``` |
354 | </details> | ||
349 | 355 | ||
350 | ### Configuration | 356 | ### Update PeerTube configuration |
351 | 357 | ||
352 | You can check for configuration changes, and report them in your `config/production.yaml` file: | 358 | Check for configuration changes, and report them in your `config/production.yaml` file: |
353 | 359 | ||
354 | ```bash | 360 | ```bash |
355 | $ cd /var/www/peertube/versions | 361 | $ cd /var/www/peertube/versions |
356 | $ diff -u "$(ls --sort=t | head -2 | tail -1)/config/production.yaml.example" "$(ls --sort=t | head -1)/config/production.yaml.example" | 362 | $ diff -u "$(ls --sort=t | head -2 | tail -1)/config/production.yaml.example" "$(ls --sort=t | head -1)/config/production.yaml.example" |
357 | ``` | 363 | ``` |
358 | 364 | ||
359 | ### nginx | 365 | ### Update nginx configuration |
360 | 366 | ||
361 | Check changes in nginx configuration: | 367 | Check changes in nginx configuration: |
362 | 368 | ||
@@ -365,7 +371,7 @@ $ cd /var/www/peertube/versions | |||
365 | $ diff -u "$(ls --sort=t | head -2 | tail -1)/support/nginx/peertube" "$(ls --sort=t | head -1)/support/nginx/peertube" | 371 | $ diff -u "$(ls --sort=t | head -2 | tail -1)/support/nginx/peertube" "$(ls --sort=t | head -1)/support/nginx/peertube" |
366 | ``` | 372 | ``` |
367 | 373 | ||
368 | ### systemd | 374 | ### Update systemd service |
369 | 375 | ||
370 | Check changes in systemd configuration: | 376 | Check changes in systemd configuration: |
371 | 377 | ||
diff --git a/tsconfig.json b/tsconfig.json index 993acf81d..8bcd944e3 100644 --- a/tsconfig.json +++ b/tsconfig.json | |||
@@ -8,7 +8,6 @@ | |||
8 | "@shared/*": [ "shared/*" ] | 8 | "@shared/*": [ "shared/*" ] |
9 | }, | 9 | }, |
10 | "typeRoots": [ | 10 | "typeRoots": [ |
11 | "server/typings", | ||
12 | "node_modules/@types" | 11 | "node_modules/@types" |
13 | ] | 12 | ] |
14 | }, | 13 | }, |
@@ -17,5 +16,5 @@ | |||
17 | { "path": "./server" }, | 16 | { "path": "./server" }, |
18 | { "path": "./scripts" } | 17 | { "path": "./scripts" } |
19 | ], | 18 | ], |
20 | "files": [ "server.ts", "server/types/express.d.ts" ] | 19 | "files": [ "server.ts", "server/types/express.d.ts", "server/types/lib.d.ts" ] |
21 | } | 20 | } |
@@ -9099,7 +9099,7 @@ typedarray@^0.0.6: | |||
9099 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" | 9099 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" |
9100 | integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== | 9100 | integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== |
9101 | 9101 | ||
9102 | typescript@^4.0.5: | 9102 | typescript@~4.8: |
9103 | version "4.8.4" | 9103 | version "4.8.4" |
9104 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" | 9104 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" |
9105 | integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== | 9105 | integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== |