diff options
Diffstat (limited to 'server/controllers/api/users')
-rw-r--r-- | server/controllers/api/users/index.ts | 285 | ||||
-rw-r--r-- | server/controllers/api/users/me.ts | 215 |
2 files changed, 500 insertions, 0 deletions
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts new file mode 100644 index 000000000..105244ddd --- /dev/null +++ b/server/controllers/api/users/index.ts | |||
@@ -0,0 +1,285 @@ | |||
1 | import * as express from 'express' | ||
2 | import * as RateLimit from 'express-rate-limit' | ||
3 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' | ||
4 | import { logger } from '../../../helpers/logger' | ||
5 | import { getFormattedObjects } from '../../../helpers/utils' | ||
6 | import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers' | ||
7 | import { Emailer } from '../../../lib/emailer' | ||
8 | import { Redis } from '../../../lib/redis' | ||
9 | import { createUserAccountAndChannel } from '../../../lib/user' | ||
10 | import { | ||
11 | asyncMiddleware, | ||
12 | asyncRetryTransactionMiddleware, | ||
13 | authenticate, | ||
14 | ensureUserHasRight, | ||
15 | ensureUserRegistrationAllowed, | ||
16 | ensureUserRegistrationAllowedForIP, | ||
17 | paginationValidator, | ||
18 | setDefaultPagination, | ||
19 | setDefaultSort, | ||
20 | token, | ||
21 | usersAddValidator, | ||
22 | usersGetValidator, | ||
23 | usersRegisterValidator, | ||
24 | usersRemoveValidator, | ||
25 | usersSortValidator, | ||
26 | usersUpdateValidator | ||
27 | } from '../../../middlewares' | ||
28 | import { usersAskResetPasswordValidator, usersBlockingValidator, usersResetPasswordValidator } from '../../../middlewares/validators' | ||
29 | import { UserModel } from '../../../models/account/user' | ||
30 | import { OAuthTokenModel } from '../../../models/oauth/oauth-token' | ||
31 | import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' | ||
32 | import { videosRouter } from '../videos' | ||
33 | import { meRouter } from './me' | ||
34 | |||
35 | const auditLogger = auditLoggerFactory('users') | ||
36 | |||
37 | const loginRateLimiter = new RateLimit({ | ||
38 | windowMs: RATES_LIMIT.LOGIN.WINDOW_MS, | ||
39 | max: RATES_LIMIT.LOGIN.MAX, | ||
40 | delayMs: 0 | ||
41 | }) | ||
42 | |||
43 | const usersRouter = express.Router() | ||
44 | videosRouter.use('/', meRouter) | ||
45 | |||
46 | usersRouter.get('/', | ||
47 | authenticate, | ||
48 | ensureUserHasRight(UserRight.MANAGE_USERS), | ||
49 | paginationValidator, | ||
50 | usersSortValidator, | ||
51 | setDefaultSort, | ||
52 | setDefaultPagination, | ||
53 | asyncMiddleware(listUsers) | ||
54 | ) | ||
55 | |||
56 | usersRouter.post('/:id/block', | ||
57 | authenticate, | ||
58 | ensureUserHasRight(UserRight.MANAGE_USERS), | ||
59 | asyncMiddleware(usersBlockingValidator), | ||
60 | asyncMiddleware(blockUser) | ||
61 | ) | ||
62 | usersRouter.post('/:id/unblock', | ||
63 | authenticate, | ||
64 | ensureUserHasRight(UserRight.MANAGE_USERS), | ||
65 | asyncMiddleware(usersBlockingValidator), | ||
66 | asyncMiddleware(unblockUser) | ||
67 | ) | ||
68 | |||
69 | usersRouter.get('/:id', | ||
70 | authenticate, | ||
71 | ensureUserHasRight(UserRight.MANAGE_USERS), | ||
72 | asyncMiddleware(usersGetValidator), | ||
73 | getUser | ||
74 | ) | ||
75 | |||
76 | usersRouter.post('/', | ||
77 | authenticate, | ||
78 | ensureUserHasRight(UserRight.MANAGE_USERS), | ||
79 | asyncMiddleware(usersAddValidator), | ||
80 | asyncRetryTransactionMiddleware(createUser) | ||
81 | ) | ||
82 | |||
83 | usersRouter.post('/register', | ||
84 | asyncMiddleware(ensureUserRegistrationAllowed), | ||
85 | ensureUserRegistrationAllowedForIP, | ||
86 | asyncMiddleware(usersRegisterValidator), | ||
87 | asyncRetryTransactionMiddleware(registerUser) | ||
88 | ) | ||
89 | |||
90 | usersRouter.put('/:id', | ||
91 | authenticate, | ||
92 | ensureUserHasRight(UserRight.MANAGE_USERS), | ||
93 | asyncMiddleware(usersUpdateValidator), | ||
94 | asyncMiddleware(updateUser) | ||
95 | ) | ||
96 | |||
97 | usersRouter.delete('/:id', | ||
98 | authenticate, | ||
99 | ensureUserHasRight(UserRight.MANAGE_USERS), | ||
100 | asyncMiddleware(usersRemoveValidator), | ||
101 | asyncMiddleware(removeUser) | ||
102 | ) | ||
103 | |||
104 | usersRouter.post('/ask-reset-password', | ||
105 | asyncMiddleware(usersAskResetPasswordValidator), | ||
106 | asyncMiddleware(askResetUserPassword) | ||
107 | ) | ||
108 | |||
109 | usersRouter.post('/:id/reset-password', | ||
110 | asyncMiddleware(usersResetPasswordValidator), | ||
111 | asyncMiddleware(resetUserPassword) | ||
112 | ) | ||
113 | |||
114 | usersRouter.post('/token', | ||
115 | loginRateLimiter, | ||
116 | token, | ||
117 | success | ||
118 | ) | ||
119 | // TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route | ||
120 | |||
121 | // --------------------------------------------------------------------------- | ||
122 | |||
123 | export { | ||
124 | usersRouter | ||
125 | } | ||
126 | |||
127 | // --------------------------------------------------------------------------- | ||
128 | |||
129 | async function createUser (req: express.Request, res: express.Response) { | ||
130 | const body: UserCreate = req.body | ||
131 | const userToCreate = new UserModel({ | ||
132 | username: body.username, | ||
133 | password: body.password, | ||
134 | email: body.email, | ||
135 | nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
136 | autoPlayVideo: true, | ||
137 | role: body.role, | ||
138 | videoQuota: body.videoQuota | ||
139 | }) | ||
140 | |||
141 | const { user, account } = await createUserAccountAndChannel(userToCreate) | ||
142 | |||
143 | auditLogger.create(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new UserAuditView(user.toFormattedJSON())) | ||
144 | logger.info('User %s with its channel and account created.', body.username) | ||
145 | |||
146 | return res.json({ | ||
147 | user: { | ||
148 | id: user.id, | ||
149 | account: { | ||
150 | id: account.id, | ||
151 | uuid: account.Actor.uuid | ||
152 | } | ||
153 | } | ||
154 | }).end() | ||
155 | } | ||
156 | |||
157 | async function registerUser (req: express.Request, res: express.Response) { | ||
158 | const body: UserCreate = req.body | ||
159 | |||
160 | const userToCreate = new UserModel({ | ||
161 | username: body.username, | ||
162 | password: body.password, | ||
163 | email: body.email, | ||
164 | nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
165 | autoPlayVideo: true, | ||
166 | role: UserRole.USER, | ||
167 | videoQuota: CONFIG.USER.VIDEO_QUOTA | ||
168 | }) | ||
169 | |||
170 | const { user } = await createUserAccountAndChannel(userToCreate) | ||
171 | |||
172 | auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) | ||
173 | logger.info('User %s with its channel and account registered.', body.username) | ||
174 | |||
175 | return res.type('json').status(204).end() | ||
176 | } | ||
177 | |||
178 | async function unblockUser (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
179 | const user: UserModel = res.locals.user | ||
180 | |||
181 | await changeUserBlock(res, user, false) | ||
182 | |||
183 | return res.status(204).end() | ||
184 | } | ||
185 | |||
186 | async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
187 | const user: UserModel = res.locals.user | ||
188 | const reason = req.body.reason | ||
189 | |||
190 | await changeUserBlock(res, user, true, reason) | ||
191 | |||
192 | return res.status(204).end() | ||
193 | } | ||
194 | |||
195 | function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
196 | return res.json((res.locals.user as UserModel).toFormattedJSON()) | ||
197 | } | ||
198 | |||
199 | async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
200 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort) | ||
201 | |||
202 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
203 | } | ||
204 | |||
205 | async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
206 | const user: UserModel = res.locals.user | ||
207 | |||
208 | await user.destroy() | ||
209 | |||
210 | auditLogger.delete(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new UserAuditView(user.toFormattedJSON())) | ||
211 | |||
212 | return res.sendStatus(204) | ||
213 | } | ||
214 | |||
215 | async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
216 | const body: UserUpdate = req.body | ||
217 | const userToUpdate = res.locals.user as UserModel | ||
218 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) | ||
219 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role | ||
220 | |||
221 | if (body.email !== undefined) userToUpdate.email = body.email | ||
222 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota | ||
223 | if (body.role !== undefined) userToUpdate.role = body.role | ||
224 | |||
225 | const user = await userToUpdate.save() | ||
226 | |||
227 | // Destroy user token to refresh rights | ||
228 | if (roleChanged) { | ||
229 | await OAuthTokenModel.deleteUserToken(userToUpdate.id) | ||
230 | } | ||
231 | |||
232 | auditLogger.update( | ||
233 | res.locals.oauth.token.User.Account.Actor.getIdentifier(), | ||
234 | new UserAuditView(user.toFormattedJSON()), | ||
235 | oldUserAuditView | ||
236 | ) | ||
237 | |||
238 | // Don't need to send this update to followers, these attributes are not propagated | ||
239 | |||
240 | return res.sendStatus(204) | ||
241 | } | ||
242 | |||
243 | async function askResetUserPassword (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
244 | const user = res.locals.user as UserModel | ||
245 | |||
246 | const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) | ||
247 | const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString | ||
248 | await Emailer.Instance.addForgetPasswordEmailJob(user.email, url) | ||
249 | |||
250 | return res.status(204).end() | ||
251 | } | ||
252 | |||
253 | async function resetUserPassword (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
254 | const user = res.locals.user as UserModel | ||
255 | user.password = req.body.password | ||
256 | |||
257 | await user.save() | ||
258 | |||
259 | return res.status(204).end() | ||
260 | } | ||
261 | |||
262 | function success (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
263 | res.end() | ||
264 | } | ||
265 | |||
266 | async function changeUserBlock (res: express.Response, user: UserModel, block: boolean, reason?: string) { | ||
267 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) | ||
268 | |||
269 | user.blocked = block | ||
270 | user.blockedReason = reason || null | ||
271 | |||
272 | await sequelizeTypescript.transaction(async t => { | ||
273 | await OAuthTokenModel.deleteUserToken(user.id, t) | ||
274 | |||
275 | await user.save({ transaction: t }) | ||
276 | }) | ||
277 | |||
278 | await Emailer.Instance.addUserBlockJob(user, block, reason) | ||
279 | |||
280 | auditLogger.update( | ||
281 | res.locals.oauth.token.User.Account.Actor.getIdentifier(), | ||
282 | new UserAuditView(user.toFormattedJSON()), | ||
283 | oldUserAuditView | ||
284 | ) | ||
285 | } | ||
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts new file mode 100644 index 000000000..1e096a35d --- /dev/null +++ b/server/controllers/api/users/me.ts | |||
@@ -0,0 +1,215 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | ||
3 | import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' | ||
4 | import { getFormattedObjects } from '../../../helpers/utils' | ||
5 | import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../../initializers' | ||
6 | import { sendUpdateActor } from '../../../lib/activitypub/send' | ||
7 | import { | ||
8 | asyncMiddleware, | ||
9 | authenticate, | ||
10 | paginationValidator, | ||
11 | setDefaultPagination, | ||
12 | setDefaultSort, | ||
13 | usersUpdateMeValidator, | ||
14 | usersVideoRatingValidator | ||
15 | } from '../../../middlewares' | ||
16 | import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' | ||
17 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
18 | import { UserModel } from '../../../models/account/user' | ||
19 | import { VideoModel } from '../../../models/video/video' | ||
20 | import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' | ||
21 | import { createReqFiles } from '../../../helpers/express-utils' | ||
22 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' | ||
23 | import { updateAvatarValidator } from '../../../middlewares/validators/avatar' | ||
24 | import { updateActorAvatarFile } from '../../../lib/avatar' | ||
25 | import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' | ||
26 | import { VideoImportModel } from '../../../models/video/video-import' | ||
27 | |||
28 | const auditLogger = auditLoggerFactory('users-me') | ||
29 | |||
30 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) | ||
31 | |||
32 | const meRouter = express.Router() | ||
33 | |||
34 | meRouter.get('/me', | ||
35 | authenticate, | ||
36 | asyncMiddleware(getUserInformation) | ||
37 | ) | ||
38 | meRouter.delete('/me', | ||
39 | authenticate, | ||
40 | asyncMiddleware(deleteMeValidator), | ||
41 | asyncMiddleware(deleteMe) | ||
42 | ) | ||
43 | |||
44 | meRouter.get('/me/video-quota-used', | ||
45 | authenticate, | ||
46 | asyncMiddleware(getUserVideoQuotaUsed) | ||
47 | ) | ||
48 | |||
49 | meRouter.get('/me/videos/imports', | ||
50 | authenticate, | ||
51 | paginationValidator, | ||
52 | videoImportsSortValidator, | ||
53 | setDefaultSort, | ||
54 | setDefaultPagination, | ||
55 | asyncMiddleware(getUserVideoImports) | ||
56 | ) | ||
57 | |||
58 | meRouter.get('/me/videos', | ||
59 | authenticate, | ||
60 | paginationValidator, | ||
61 | videosSortValidator, | ||
62 | setDefaultSort, | ||
63 | setDefaultPagination, | ||
64 | asyncMiddleware(getUserVideos) | ||
65 | ) | ||
66 | |||
67 | meRouter.get('/me/videos/:videoId/rating', | ||
68 | authenticate, | ||
69 | asyncMiddleware(usersVideoRatingValidator), | ||
70 | asyncMiddleware(getUserVideoRating) | ||
71 | ) | ||
72 | |||
73 | meRouter.put('/me', | ||
74 | authenticate, | ||
75 | usersUpdateMeValidator, | ||
76 | asyncMiddleware(updateMe) | ||
77 | ) | ||
78 | |||
79 | meRouter.post('/me/avatar/pick', | ||
80 | authenticate, | ||
81 | reqAvatarFile, | ||
82 | updateAvatarValidator, | ||
83 | asyncMiddleware(updateMyAvatar) | ||
84 | ) | ||
85 | |||
86 | // --------------------------------------------------------------------------- | ||
87 | |||
88 | export { | ||
89 | meRouter | ||
90 | } | ||
91 | |||
92 | // --------------------------------------------------------------------------- | ||
93 | |||
94 | async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
95 | const user = res.locals.oauth.token.User as UserModel | ||
96 | const resultList = await VideoModel.listUserVideosForApi( | ||
97 | user.Account.id, | ||
98 | req.query.start as number, | ||
99 | req.query.count as number, | ||
100 | req.query.sort as VideoSortField | ||
101 | ) | ||
102 | |||
103 | const additionalAttributes = { | ||
104 | waitTranscoding: true, | ||
105 | state: true, | ||
106 | scheduledUpdate: true, | ||
107 | blacklistInfo: true | ||
108 | } | ||
109 | return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes })) | ||
110 | } | ||
111 | |||
112 | async function getUserVideoImports (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
113 | const user = res.locals.oauth.token.User as UserModel | ||
114 | const resultList = await VideoImportModel.listUserVideoImportsForApi( | ||
115 | user.id, | ||
116 | req.query.start as number, | ||
117 | req.query.count as number, | ||
118 | req.query.sort | ||
119 | ) | ||
120 | |||
121 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
122 | } | ||
123 | |||
124 | async function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
125 | // We did not load channels in res.locals.user | ||
126 | const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) | ||
127 | |||
128 | return res.json(user.toFormattedJSON()) | ||
129 | } | ||
130 | |||
131 | async function getUserVideoQuotaUsed (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
132 | // We did not load channels in res.locals.user | ||
133 | const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) | ||
134 | const videoQuotaUsed = await UserModel.getOriginalVideoFileTotalFromUser(user) | ||
135 | |||
136 | const data: UserVideoQuota = { | ||
137 | videoQuotaUsed | ||
138 | } | ||
139 | return res.json(data) | ||
140 | } | ||
141 | |||
142 | async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
143 | const videoId = +req.params.videoId | ||
144 | const accountId = +res.locals.oauth.token.User.Account.id | ||
145 | |||
146 | const ratingObj = await AccountVideoRateModel.load(accountId, videoId, null) | ||
147 | const rating = ratingObj ? ratingObj.type : 'none' | ||
148 | |||
149 | const json: FormattedUserVideoRate = { | ||
150 | videoId, | ||
151 | rating | ||
152 | } | ||
153 | res.json(json) | ||
154 | } | ||
155 | |||
156 | async function deleteMe (req: express.Request, res: express.Response) { | ||
157 | const user: UserModel = res.locals.oauth.token.User | ||
158 | |||
159 | await user.destroy() | ||
160 | |||
161 | auditLogger.delete(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new UserAuditView(user.toFormattedJSON())) | ||
162 | |||
163 | return res.sendStatus(204) | ||
164 | } | ||
165 | |||
166 | async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
167 | const body: UserUpdateMe = req.body | ||
168 | |||
169 | const user: UserModel = res.locals.oauth.token.user | ||
170 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) | ||
171 | |||
172 | if (body.password !== undefined) user.password = body.password | ||
173 | if (body.email !== undefined) user.email = body.email | ||
174 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy | ||
175 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo | ||
176 | |||
177 | await sequelizeTypescript.transaction(async t => { | ||
178 | await user.save({ transaction: t }) | ||
179 | |||
180 | if (body.displayName !== undefined) user.Account.name = body.displayName | ||
181 | if (body.description !== undefined) user.Account.description = body.description | ||
182 | await user.Account.save({ transaction: t }) | ||
183 | |||
184 | await sendUpdateActor(user.Account, t) | ||
185 | |||
186 | auditLogger.update( | ||
187 | res.locals.oauth.token.User.Account.Actor.getIdentifier(), | ||
188 | new UserAuditView(user.toFormattedJSON()), | ||
189 | oldUserAuditView | ||
190 | ) | ||
191 | }) | ||
192 | |||
193 | return res.sendStatus(204) | ||
194 | } | ||
195 | |||
196 | async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
197 | const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] | ||
198 | const user: UserModel = res.locals.oauth.token.user | ||
199 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) | ||
200 | const account = user.Account | ||
201 | |||
202 | const avatar = await updateActorAvatarFile(avatarPhysicalFile, account.Actor, account) | ||
203 | |||
204 | auditLogger.update( | ||
205 | res.locals.oauth.token.User.Account.Actor.getIdentifier(), | ||
206 | new UserAuditView(user.toFormattedJSON()), | ||
207 | oldUserAuditView | ||
208 | ) | ||
209 | |||
210 | return res | ||
211 | .json({ | ||
212 | avatar: avatar.toFormattedJSON() | ||
213 | }) | ||
214 | .end() | ||
215 | } | ||