aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-06-22 09:44:08 +0200
committerChocobozzz <me@florianbigard.com>2022-06-22 10:25:31 +0200
commitff9d43f62a4f4737c5bfe955883b48c5440f323a (patch)
tree60593f4b57ec5cd712986a3db370f39b0b7a4cef
parent2e401e8575decb1d491d0db48ca1ab1eba5b2a66 (diff)
downloadPeerTube-ff9d43f62a4f4737c5bfe955883b48c5440f323a.tar.gz
PeerTube-ff9d43f62a4f4737c5bfe955883b48c5440f323a.tar.zst
PeerTube-ff9d43f62a4f4737c5bfe955883b48c5440f323a.zip
Refactor video rights checker
-rw-r--r--server/middlewares/validators/feeds.ts6
-rw-r--r--server/middlewares/validators/shared/videos.ts84
-rw-r--r--server/middlewares/validators/videos/video-captions.ts4
-rw-r--r--server/middlewares/validators/videos/video-comments.ts10
-rw-r--r--server/middlewares/validators/videos/video-rates.ts4
-rw-r--r--server/middlewares/validators/videos/videos.ts28
-rw-r--r--server/models/user/user.ts21
-rw-r--r--server/tests/api/check-params/videos.ts5
8 files changed, 89 insertions, 73 deletions
diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts
index f8ebaf6ed..04b4e00c9 100644
--- a/server/middlewares/validators/feeds.ts
+++ b/server/middlewares/validators/feeds.ts
@@ -6,6 +6,7 @@ import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helper
6import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
7import { 7import {
8 areValidationErrors, 8 areValidationErrors,
9 checkCanSeeVideo,
9 doesAccountIdExist, 10 doesAccountIdExist,
10 doesAccountNameWithHostExist, 11 doesAccountNameWithHostExist,
11 doesUserFeedTokenCorrespond, 12 doesUserFeedTokenCorrespond,
@@ -112,7 +113,10 @@ const videoCommentsFeedsValidator = [
112 return res.fail({ message: 'videoId cannot be mixed with a channel filter' }) 113 return res.fail({ message: 'videoId cannot be mixed with a channel filter' })
113 } 114 }
114 115
115 if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return 116 if (req.query.videoId) {
117 if (!await doesVideoExist(req.query.videoId, res)) return
118 if (!await checkCanSeeVideo({ req, res, paramId: req.query.videoId, video: res.locals.videoAll })) return
119 }
116 120
117 return next() 121 return next()
118 } 122 }
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts
index 8807435f6..39aab6df7 100644
--- a/server/middlewares/validators/shared/videos.ts
+++ b/server/middlewares/validators/shared/videos.ts
@@ -1,4 +1,5 @@
1import { Request, Response } from 'express' 1import { Request, Response } from 'express'
2import { isUUIDValid } from '@server/helpers/custom-validators/misc'
2import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' 3import { loadVideo, VideoLoadType } from '@server/lib/model-loaders'
3import { isAbleToUploadVideo } from '@server/lib/user' 4import { isAbleToUploadVideo } from '@server/lib/user'
4import { authenticatePromiseIfNeeded } from '@server/middlewares/auth' 5import { authenticatePromiseIfNeeded } from '@server/middlewares/auth'
@@ -18,18 +19,19 @@ import {
18 MVideoThumbnail, 19 MVideoThumbnail,
19 MVideoWithRights 20 MVideoWithRights
20} from '@server/types/models' 21} from '@server/types/models'
21import { HttpStatusCode, ServerErrorCode, UserRight } from '@shared/models' 22import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models'
22 23
23async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { 24async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
24 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined 25 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
25 26
26 const video = await loadVideo(id, fetchType, userId) 27 const video = await loadVideo(id, fetchType, userId)
27 28
28 if (video === null) { 29 if (!video) {
29 res.fail({ 30 res.fail({
30 status: HttpStatusCode.NOT_FOUND_404, 31 status: HttpStatusCode.NOT_FOUND_404,
31 message: 'Video not found' 32 message: 'Video not found'
32 }) 33 })
34
33 return false 35 return false
34 } 36 }
35 37
@@ -58,6 +60,8 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
58 return true 60 return true
59} 61}
60 62
63// ---------------------------------------------------------------------------
64
61async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) { 65async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
62 if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) { 66 if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
63 res.fail({ 67 res.fail({
@@ -70,6 +74,8 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st
70 return true 74 return true
71} 75}
72 76
77// ---------------------------------------------------------------------------
78
73async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { 79async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
74 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) 80 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
75 81
@@ -95,32 +101,77 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
95 return true 101 return true
96} 102}
97 103
98async function checkCanSeeVideoIfPrivate (req: Request, res: Response, video: MVideo, authenticateInQuery = false) { 104// ---------------------------------------------------------------------------
99 if (!video.requiresAuth()) return true
100 105
101 const videoWithRights = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id) 106async function checkCanSeeVideo (options: {
107 req: Request
108 res: Response
109 paramId: string
110 video: MVideo
111 authenticateInQuery?: boolean // default false
112}) {
113 const { req, res, video, paramId, authenticateInQuery = false } = options
114
115 if (video.requiresAuth()) {
116 return checkCanSeeAuthVideo(req, res, video, authenticateInQuery)
117 }
102 118
103 return checkCanSeePrivateVideo(req, res, videoWithRights, authenticateInQuery) 119 if (video.privacy === VideoPrivacy.UNLISTED) {
104} 120 if (isUUIDValid(paramId)) return true
105 121
106async function checkCanSeePrivateVideo (req: Request, res: Response, video: MVideoWithRights, authenticateInQuery = false) { 122 return checkCanSeeAuthVideo(req, res, video, authenticateInQuery)
107 await authenticatePromiseIfNeeded(req, res, authenticateInQuery) 123 }
108 124
109 const user = res.locals.oauth ? res.locals.oauth.token.User : null 125 if (video.privacy === VideoPrivacy.PUBLIC) return true
126
127 throw new Error('Fatal error when checking video right ' + video.url)
128}
110 129
111 // Only the owner or a user that have blocklist rights can see the video 130async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights, authenticateInQuery = false) {
112 if (!user || !user.canGetVideo(video)) { 131 const fail = () => {
113 res.fail({ 132 res.fail({
114 status: HttpStatusCode.FORBIDDEN_403, 133 status: HttpStatusCode.FORBIDDEN_403,
115 message: 'Cannot fetch information of private/internal/blocklisted video' 134 message: 'Cannot fetch information of private/internal/blocked video'
116 }) 135 })
117 136
118 return false 137 return false
119 } 138 }
120 139
121 return true 140 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
141
142 const user = res.locals.oauth?.token.User
143 if (!user) return fail()
144
145 const videoWithRights = (video as MVideoWithRights).VideoChannel?.Account?.userId
146 ? video as MVideoWithRights
147 : await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)
148
149 const privacy = videoWithRights.privacy
150
151 if (privacy === VideoPrivacy.INTERNAL) {
152 // We know we have a user
153 return true
154 }
155
156 const isOwnedByUser = videoWithRights.VideoChannel.Account.userId === user.id
157 if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) {
158 if (isOwnedByUser && user.hasRight(UserRight.SEE_ALL_VIDEOS)) return true
159
160 return fail()
161 }
162
163 if (videoWithRights.isBlacklisted()) {
164 if (isOwnedByUser || user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return true
165
166 return fail()
167 }
168
169 // Should not happen
170 return fail()
122} 171}
123 172
173// ---------------------------------------------------------------------------
174
124function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { 175function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) {
125 // Retrieve the user who did the request 176 // Retrieve the user who did the request
126 if (onlyOwned && video.isOwned() === false) { 177 if (onlyOwned && video.isOwned() === false) {
@@ -146,6 +197,8 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right:
146 return true 197 return true
147} 198}
148 199
200// ---------------------------------------------------------------------------
201
149async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) { 202async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) {
150 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { 203 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
151 res.fail({ 204 res.fail({
@@ -167,7 +220,6 @@ export {
167 doesVideoFileOfVideoExist, 220 doesVideoFileOfVideoExist,
168 221
169 checkUserCanManageVideo, 222 checkUserCanManageVideo,
170 checkCanSeeVideoIfPrivate, 223 checkCanSeeVideo,
171 checkCanSeePrivateVideo,
172 checkUserQuota 224 checkUserQuota
173} 225}
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
index 441c6b4be..dfb8fefc5 100644
--- a/server/middlewares/validators/videos/video-captions.ts
+++ b/server/middlewares/validators/videos/video-captions.ts
@@ -7,7 +7,7 @@ import { logger } from '../../../helpers/logger'
7import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants' 7import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants'
8import { 8import {
9 areValidationErrors, 9 areValidationErrors,
10 checkCanSeeVideoIfPrivate, 10 checkCanSeeVideo,
11 checkUserCanManageVideo, 11 checkUserCanManageVideo,
12 doesVideoCaptionExist, 12 doesVideoCaptionExist,
13 doesVideoExist, 13 doesVideoExist,
@@ -74,7 +74,7 @@ const listVideoCaptionsValidator = [
74 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 74 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
75 75
76 const video = res.locals.onlyVideo 76 const video = res.locals.onlyVideo
77 if (!await checkCanSeeVideoIfPrivate(req, res, video)) return 77 if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.videoId })) return
78 78
79 return next() 79 return next()
80 } 80 }
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 698afdbd1..b22a4e3b7 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -10,7 +10,7 @@ import { Hooks } from '../../../lib/plugins/hooks'
10import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' 10import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
11import { 11import {
12 areValidationErrors, 12 areValidationErrors,
13 checkCanSeeVideoIfPrivate, 13 checkCanSeeVideo,
14 doesVideoCommentExist, 14 doesVideoCommentExist,
15 doesVideoCommentThreadExist, 15 doesVideoCommentThreadExist,
16 doesVideoExist, 16 doesVideoExist,
@@ -54,7 +54,7 @@ const listVideoCommentThreadsValidator = [
54 if (areValidationErrors(req, res)) return 54 if (areValidationErrors(req, res)) return
55 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 55 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
56 56
57 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return 57 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
58 58
59 return next() 59 return next()
60 } 60 }
@@ -73,7 +73,7 @@ const listVideoThreadCommentsValidator = [
73 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 73 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
74 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return 74 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
75 75
76 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return 76 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
77 77
78 return next() 78 return next()
79 } 79 }
@@ -91,7 +91,7 @@ const addVideoCommentThreadValidator = [
91 if (areValidationErrors(req, res)) return 91 if (areValidationErrors(req, res)) return
92 if (!await doesVideoExist(req.params.videoId, res)) return 92 if (!await doesVideoExist(req.params.videoId, res)) return
93 93
94 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return 94 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
95 95
96 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return 96 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
97 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return 97 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return
@@ -113,7 +113,7 @@ const addVideoCommentReplyValidator = [
113 if (areValidationErrors(req, res)) return 113 if (areValidationErrors(req, res)) return
114 if (!await doesVideoExist(req.params.videoId, res)) return 114 if (!await doesVideoExist(req.params.videoId, res)) return
115 115
116 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return 116 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
117 117
118 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return 118 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
119 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return 119 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts
index 1a9736034..8b8eeedb6 100644
--- a/server/middlewares/validators/videos/video-rates.ts
+++ b/server/middlewares/validators/videos/video-rates.ts
@@ -8,7 +8,7 @@ import { isRatingValid } from '../../../helpers/custom-validators/video-rates'
8import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' 8import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
9import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
10import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 10import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
11import { areValidationErrors, checkCanSeeVideoIfPrivate, doesVideoExist, isValidVideoIdParam } from '../shared' 11import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam } from '../shared'
12 12
13const videoUpdateRateValidator = [ 13const videoUpdateRateValidator = [
14 isValidVideoIdParam('id'), 14 isValidVideoIdParam('id'),
@@ -21,7 +21,7 @@ const videoUpdateRateValidator = [
21 if (areValidationErrors(req, res)) return 21 if (areValidationErrors(req, res)) return
22 if (!await doesVideoExist(req.params.id, res)) return 22 if (!await doesVideoExist(req.params.id, res)) return
23 23
24 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return 24 if (!await checkCanSeeVideo({ req, res, paramId: req.params.id, video: res.locals.videoAll })) return
25 25
26 return next() 26 return next()
27 } 27 }
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index c75c3640b..c6d31f8f0 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -7,14 +7,13 @@ import { getServerActor } from '@server/models/application/application'
7import { ExpressPromiseHandler } from '@server/types/express-handler' 7import { ExpressPromiseHandler } from '@server/types/express-handler'
8import { MUserAccountId, MVideoFullLight } from '@server/types/models' 8import { MUserAccountId, MVideoFullLight } from '@server/types/models'
9import { getAllPrivacies } from '@shared/core-utils' 9import { getAllPrivacies } from '@shared/core-utils'
10import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoPrivacy } from '@shared/models' 10import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude } from '@shared/models'
11import { 11import {
12 exists, 12 exists,
13 isBooleanValid, 13 isBooleanValid,
14 isDateValid, 14 isDateValid,
15 isFileValid, 15 isFileValid,
16 isIdValid, 16 isIdValid,
17 isUUIDValid,
18 toArray, 17 toArray,
19 toBooleanOrNull, 18 toBooleanOrNull,
20 toIntOrNull, 19 toIntOrNull,
@@ -50,7 +49,7 @@ import { Hooks } from '../../../lib/plugins/hooks'
50import { VideoModel } from '../../../models/video/video' 49import { VideoModel } from '../../../models/video/video'
51import { 50import {
52 areValidationErrors, 51 areValidationErrors,
53 checkCanSeePrivateVideo, 52 checkCanSeeVideo,
54 checkUserCanManageVideo, 53 checkUserCanManageVideo,
55 checkUserQuota, 54 checkUserQuota,
56 doesVideoChannelOfAccountExist, 55 doesVideoChannelOfAccountExist,
@@ -297,28 +296,9 @@ const videosCustomGetValidator = (
297 296
298 const video = getVideoWithAttributes(res) as MVideoFullLight 297 const video = getVideoWithAttributes(res) as MVideoFullLight
299 298
300 // Video private or blacklisted 299 if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id, authenticateInQuery })) return
301 if (video.requiresAuth()) {
302 if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) {
303 return next()
304 }
305 300
306 return 301 return next()
307 }
308
309 // Video is public, anyone can access it
310 if (video.privacy === VideoPrivacy.PUBLIC) return next()
311
312 // Video is unlisted, check we used the uuid to fetch it
313 if (video.privacy === VideoPrivacy.UNLISTED) {
314 if (isUUIDValid(req.params.id)) return next()
315
316 // Don't leak this unlisted video
317 return res.fail({
318 status: HttpStatusCode.NOT_FOUND_404,
319 message: 'Video not found'
320 })
321 }
322 } 302 }
323 ] 303 ]
324} 304}
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
index a25551ecd..dc260e512 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -29,12 +29,11 @@ import {
29 MUserDefault, 29 MUserDefault,
30 MUserFormattable, 30 MUserFormattable,
31 MUserNotifSettingChannelDefault, 31 MUserNotifSettingChannelDefault,
32 MUserWithNotificationSetting, 32 MUserWithNotificationSetting
33 MVideoWithRights
34} from '@server/types/models' 33} from '@server/types/models'
35import { AttributesOnly } from '@shared/typescript-utils' 34import { AttributesOnly } from '@shared/typescript-utils'
36import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' 35import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users'
37import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' 36import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models'
38import { User, UserRole } from '../../../shared/models/users' 37import { User, UserRole } from '../../../shared/models/users'
39import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' 38import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
40import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' 39import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
@@ -851,22 +850,6 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
851 .then(u => u.map(u => u.username)) 850 .then(u => u.map(u => u.username))
852 } 851 }
853 852
854 canGetVideo (video: MVideoWithRights) {
855 const videoUserId = video.VideoChannel.Account.userId
856
857 if (video.isBlacklisted()) {
858 return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
859 }
860
861 if (video.privacy === VideoPrivacy.PRIVATE) {
862 return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
863 }
864
865 if (video.privacy === VideoPrivacy.INTERNAL) return true
866
867 return false
868 }
869
870 hasRight (right: UserRight) { 853 hasRight (right: UserRight) {
871 return hasUserRight(this.role, right) 854 return hasUserRight(this.role, right)
872 } 855 }
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index 41064d2ff..5ff51d1ff 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -39,10 +39,7 @@ describe('Test videos API validator', function () {
39 39
40 await setAccessTokensToServers([ server ]) 40 await setAccessTokensToServers([ server ])
41 41
42 const username = 'user1' 42 userAccessToken = await server.users.generateUserAndToken('user1')
43 const password = 'my super password'
44 await server.users.create({ username: username, password: password })
45 userAccessToken = await server.login.getAccessToken({ username, password })
46 43
47 { 44 {
48 const body = await server.users.getMyInfo() 45 const body = await server.users.getMyInfo()