diff options
Diffstat (limited to 'shared')
-rw-r--r-- | shared/models/users/index.ts | 1 | ||||
-rw-r--r-- | shared/models/users/two-factor-enable-result.model.ts | 7 | ||||
-rw-r--r-- | shared/models/users/user.model.ts | 2 | ||||
-rw-r--r-- | shared/server-commands/server/server.ts | 12 | ||||
-rw-r--r-- | shared/server-commands/users/index.ts | 1 | ||||
-rw-r--r-- | shared/server-commands/users/login-command.ts | 73 | ||||
-rw-r--r-- | shared/server-commands/users/two-factor-command.ts | 92 | ||||
-rw-r--r-- | shared/server-commands/users/users-command.ts | 3 |
8 files changed, 166 insertions, 25 deletions
diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts index b25978587..32f7a441c 100644 --- a/shared/models/users/index.ts +++ b/shared/models/users/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './two-factor-enable-result.model' | ||
1 | export * from './user-create-result.model' | 2 | export * from './user-create-result.model' |
2 | export * from './user-create.model' | 3 | export * from './user-create.model' |
3 | export * from './user-flag.model' | 4 | export * from './user-flag.model' |
diff --git a/shared/models/users/two-factor-enable-result.model.ts b/shared/models/users/two-factor-enable-result.model.ts new file mode 100644 index 000000000..1fc801f0a --- /dev/null +++ b/shared/models/users/two-factor-enable-result.model.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | export interface TwoFactorEnableResult { | ||
2 | otpRequest: { | ||
3 | requestToken: string | ||
4 | secret: string | ||
5 | uri: string | ||
6 | } | ||
7 | } | ||
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 63c5c8a92..7b6494ff8 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -62,6 +62,8 @@ export interface User { | |||
62 | pluginAuth: string | null | 62 | pluginAuth: string | null |
63 | 63 | ||
64 | lastLoginDate: Date | null | 64 | lastLoginDate: Date | null |
65 | |||
66 | twoFactorEnabled: boolean | ||
65 | } | 67 | } |
66 | 68 | ||
67 | export interface MyUserSpecialPlaylist { | 69 | export interface MyUserSpecialPlaylist { |
diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts index a8f8c1d84..7096faf21 100644 --- a/shared/server-commands/server/server.ts +++ b/shared/server-commands/server/server.ts | |||
@@ -13,7 +13,15 @@ import { AbusesCommand } from '../moderation' | |||
13 | import { OverviewsCommand } from '../overviews' | 13 | import { OverviewsCommand } from '../overviews' |
14 | import { SearchCommand } from '../search' | 14 | import { SearchCommand } from '../search' |
15 | import { SocketIOCommand } from '../socket' | 15 | import { SocketIOCommand } from '../socket' |
16 | import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users' | 16 | import { |
17 | AccountsCommand, | ||
18 | BlocklistCommand, | ||
19 | LoginCommand, | ||
20 | NotificationsCommand, | ||
21 | SubscriptionsCommand, | ||
22 | TwoFactorCommand, | ||
23 | UsersCommand | ||
24 | } from '../users' | ||
17 | import { | 25 | import { |
18 | BlacklistCommand, | 26 | BlacklistCommand, |
19 | CaptionsCommand, | 27 | CaptionsCommand, |
@@ -136,6 +144,7 @@ export class PeerTubeServer { | |||
136 | videos?: VideosCommand | 144 | videos?: VideosCommand |
137 | videoStats?: VideoStatsCommand | 145 | videoStats?: VideoStatsCommand |
138 | views?: ViewsCommand | 146 | views?: ViewsCommand |
147 | twoFactor?: TwoFactorCommand | ||
139 | 148 | ||
140 | constructor (options: { serverNumber: number } | { url: string }) { | 149 | constructor (options: { serverNumber: number } | { url: string }) { |
141 | if ((options as any).url) { | 150 | if ((options as any).url) { |
@@ -417,5 +426,6 @@ export class PeerTubeServer { | |||
417 | this.videoStudio = new VideoStudioCommand(this) | 426 | this.videoStudio = new VideoStudioCommand(this) |
418 | this.videoStats = new VideoStatsCommand(this) | 427 | this.videoStats = new VideoStatsCommand(this) |
419 | this.views = new ViewsCommand(this) | 428 | this.views = new ViewsCommand(this) |
429 | this.twoFactor = new TwoFactorCommand(this) | ||
420 | } | 430 | } |
421 | } | 431 | } |
diff --git a/shared/server-commands/users/index.ts b/shared/server-commands/users/index.ts index f6f93b4d2..1afc02dc1 100644 --- a/shared/server-commands/users/index.ts +++ b/shared/server-commands/users/index.ts | |||
@@ -5,4 +5,5 @@ 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 './subscriptions-command' | 7 | export * from './subscriptions-command' |
8 | export * from './two-factor-command' | ||
8 | export * from './users-command' | 9 | export * from './users-command' |
diff --git a/shared/server-commands/users/login-command.ts b/shared/server-commands/users/login-command.ts index 54070e426..f2fc6d1c5 100644 --- a/shared/server-commands/users/login-command.ts +++ b/shared/server-commands/users/login-command.ts | |||
@@ -2,34 +2,27 @@ import { HttpStatusCode, PeerTubeProblemDocument } from '@shared/models' | |||
2 | import { unwrapBody } from '../requests' | 2 | import { unwrapBody } from '../requests' |
3 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | 3 | import { AbstractCommand, OverrideCommandOptions } from '../shared' |
4 | 4 | ||
5 | type LoginOptions = OverrideCommandOptions & { | ||
6 | client?: { id?: string, secret?: string } | ||
7 | user?: { username: string, password?: string } | ||
8 | otpToken?: string | ||
9 | } | ||
10 | |||
5 | export class LoginCommand extends AbstractCommand { | 11 | export class LoginCommand extends AbstractCommand { |
6 | 12 | ||
7 | login (options: OverrideCommandOptions & { | 13 | async login (options: LoginOptions = {}) { |
8 | client?: { id?: string, secret?: string } | 14 | const res = await this._login(options) |
9 | user?: { username: string, password?: string } | ||
10 | } = {}) { | ||
11 | const { client = this.server.store.client, user = this.server.store.user } = options | ||
12 | const path = '/api/v1/users/token' | ||
13 | 15 | ||
14 | const body = { | 16 | return this.unwrapLoginBody(res.body) |
15 | client_id: client.id, | 17 | } |
16 | client_secret: client.secret, | ||
17 | username: user.username, | ||
18 | password: user.password ?? 'password', | ||
19 | response_type: 'code', | ||
20 | grant_type: 'password', | ||
21 | scope: 'upload' | ||
22 | } | ||
23 | 18 | ||
24 | return unwrapBody<{ access_token: string, refresh_token: string } & PeerTubeProblemDocument>(this.postBodyRequest({ | 19 | async loginAndGetResponse (options: LoginOptions = {}) { |
25 | ...options, | 20 | const res = await this._login(options) |
26 | 21 | ||
27 | path, | 22 | return { |
28 | requestType: 'form', | 23 | res, |
29 | fields: body, | 24 | body: this.unwrapLoginBody(res.body) |
30 | implicitToken: false, | 25 | } |
31 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
32 | })) | ||
33 | } | 26 | } |
34 | 27 | ||
35 | getAccessToken (arg1?: { username: string, password?: string }): Promise<string> | 28 | getAccessToken (arg1?: { username: string, password?: string }): Promise<string> |
@@ -129,4 +122,38 @@ export class LoginCommand extends AbstractCommand { | |||
129 | defaultExpectedStatus: HttpStatusCode.OK_200 | 122 | defaultExpectedStatus: HttpStatusCode.OK_200 |
130 | }) | 123 | }) |
131 | } | 124 | } |
125 | |||
126 | private _login (options: LoginOptions) { | ||
127 | const { client = this.server.store.client, user = this.server.store.user, otpToken } = options | ||
128 | const path = '/api/v1/users/token' | ||
129 | |||
130 | const body = { | ||
131 | client_id: client.id, | ||
132 | client_secret: client.secret, | ||
133 | username: user.username, | ||
134 | password: user.password ?? 'password', | ||
135 | response_type: 'code', | ||
136 | grant_type: 'password', | ||
137 | scope: 'upload' | ||
138 | } | ||
139 | |||
140 | const headers = otpToken | ||
141 | ? { 'x-peertube-otp': otpToken } | ||
142 | : {} | ||
143 | |||
144 | return this.postBodyRequest({ | ||
145 | ...options, | ||
146 | |||
147 | path, | ||
148 | headers, | ||
149 | requestType: 'form', | ||
150 | fields: body, | ||
151 | implicitToken: false, | ||
152 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
153 | }) | ||
154 | } | ||
155 | |||
156 | private unwrapLoginBody (body: any) { | ||
157 | return body as { access_token: string, refresh_token: string } & PeerTubeProblemDocument | ||
158 | } | ||
132 | } | 159 | } |
diff --git a/shared/server-commands/users/two-factor-command.ts b/shared/server-commands/users/two-factor-command.ts new file mode 100644 index 000000000..5542acfda --- /dev/null +++ b/shared/server-commands/users/two-factor-command.ts | |||
@@ -0,0 +1,92 @@ | |||
1 | import { TOTP } from 'otpauth' | ||
2 | import { HttpStatusCode, TwoFactorEnableResult } from '@shared/models' | ||
3 | import { unwrapBody } from '../requests' | ||
4 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
5 | |||
6 | export class TwoFactorCommand extends AbstractCommand { | ||
7 | |||
8 | static buildOTP (options: { | ||
9 | secret: string | ||
10 | }) { | ||
11 | const { secret } = options | ||
12 | |||
13 | return new TOTP({ | ||
14 | issuer: 'PeerTube', | ||
15 | algorithm: 'SHA1', | ||
16 | digits: 6, | ||
17 | period: 30, | ||
18 | secret | ||
19 | }) | ||
20 | } | ||
21 | |||
22 | request (options: OverrideCommandOptions & { | ||
23 | userId: number | ||
24 | currentPassword?: string | ||
25 | }) { | ||
26 | const { currentPassword, userId } = options | ||
27 | |||
28 | const path = '/api/v1/users/' + userId + '/two-factor/request' | ||
29 | |||
30 | return unwrapBody<TwoFactorEnableResult>(this.postBodyRequest({ | ||
31 | ...options, | ||
32 | |||
33 | path, | ||
34 | fields: { currentPassword }, | ||
35 | implicitToken: true, | ||
36 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
37 | })) | ||
38 | } | ||
39 | |||
40 | confirmRequest (options: OverrideCommandOptions & { | ||
41 | userId: number | ||
42 | requestToken: string | ||
43 | otpToken: string | ||
44 | }) { | ||
45 | const { userId, requestToken, otpToken } = options | ||
46 | |||
47 | const path = '/api/v1/users/' + userId + '/two-factor/confirm-request' | ||
48 | |||
49 | return this.postBodyRequest({ | ||
50 | ...options, | ||
51 | |||
52 | path, | ||
53 | fields: { requestToken, otpToken }, | ||
54 | implicitToken: true, | ||
55 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
56 | }) | ||
57 | } | ||
58 | |||
59 | disable (options: OverrideCommandOptions & { | ||
60 | userId: number | ||
61 | currentPassword?: string | ||
62 | }) { | ||
63 | const { userId, currentPassword } = options | ||
64 | const path = '/api/v1/users/' + userId + '/two-factor/disable' | ||
65 | |||
66 | return this.postBodyRequest({ | ||
67 | ...options, | ||
68 | |||
69 | path, | ||
70 | fields: { currentPassword }, | ||
71 | implicitToken: true, | ||
72 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
73 | }) | ||
74 | } | ||
75 | |||
76 | async requestAndConfirm (options: OverrideCommandOptions & { | ||
77 | userId: number | ||
78 | currentPassword?: string | ||
79 | }) { | ||
80 | const { userId, currentPassword } = options | ||
81 | |||
82 | const { otpRequest } = await this.request({ userId, currentPassword }) | ||
83 | |||
84 | await this.confirmRequest({ | ||
85 | userId, | ||
86 | requestToken: otpRequest.requestToken, | ||
87 | otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate() | ||
88 | }) | ||
89 | |||
90 | return otpRequest | ||
91 | } | ||
92 | } | ||
diff --git a/shared/server-commands/users/users-command.ts b/shared/server-commands/users/users-command.ts index e7d021059..811b9685b 100644 --- a/shared/server-commands/users/users-command.ts +++ b/shared/server-commands/users/users-command.ts | |||
@@ -202,7 +202,8 @@ export class UsersCommand extends AbstractCommand { | |||
202 | token, | 202 | token, |
203 | userId: user.id, | 203 | userId: user.id, |
204 | userChannelId: me.videoChannels[0].id, | 204 | userChannelId: me.videoChannels[0].id, |
205 | userChannelName: me.videoChannels[0].name | 205 | userChannelName: me.videoChannels[0].name, |
206 | password | ||
206 | } | 207 | } |
207 | } | 208 | } |
208 | 209 | ||