Compare commits

...

209 Commits

Author SHA1 Message Date
41a6083a4b Client:Store: refactoring files store 2024-07-19 11:17:02 +03:00
fc3593eab1 Client:Store: create structure of files stores, init files socket stores 2024-07-15 15:36:45 +03:00
925cc648f5 Remove old files 2024-07-12 11:41:10 +04:00
8fd8aed172 Remove useless runtime.json 2024-07-12 11:39:30 +04:00
9dd2820abc Shared: AOuth: fix axios response data 2024-07-12 11:37:27 +04:00
f1e6f79704 Merge branch 'release/v2.6.0' into develop
# Conflicts:
#	packages/login/src/components/LoginForm/index.tsx
2024-07-12 11:11:05 +04:00
320d882d86 Merge branch 'release/v2.6.0' into develop 2024-07-11 16:18:28 +04:00
99e265ebee Merge branch 'release/v2.6.0' into develop
# Conflicts:
#	packages/client/src/components/PrivateRouteWrapper/index.tsx
#	packages/client/src/pages/PortalSettings/categories/developer-tools/index.js
#	packages/login/src/app/layout.tsx
#	packages/login/src/components/LoginForm/index.tsx
#	packages/login/src/middleware.ts
#	packages/login/src/utils/index.ts
#	packages/shared/components/table/TableHeader.tsx
#	packages/shared/routes/Route.private.tsx
2024-07-11 12:27:31 +04:00
6c727811a0
Merge pull request #541 from ONLYOFFICE/feature/oauth2-client
Feature/oauth2
2024-07-09 16:22:28 +03:00
b354c305cf Add check for enable identity services 2024-07-09 15:39:14 +03:00
ae294f7e64 Add tasks for build identity services 2024-07-09 15:38:57 +03:00
dd435561a5 setup default config for oauth origin parameter 2024-07-04 18:51:04 +03:00
a966d2e948 Shared:Components:PortalLogo: fix for SSR 2024-06-28 18:44:52 +03:00
5f712f87f3 Add oauth origin for client config 2024-06-28 18:44:39 +03:00
2ffa095303 Client:OAuth: enable public client by default and remove toggle button 2024-06-28 17:37:55 +03:00
e2205d9387 Shared:Utils:OAuth: fix scope list render 2024-06-28 13:38:08 +03:00
8f4ddba367 Merge branch 'develop' into feature/oauth2-client 2024-06-28 13:28:10 +03:00
f612ba9edf Shared:Utils:OAuth: fix scope list render 2024-06-28 13:19:31 +03:00
572e16a7f2 Login:OAuth: add translations and default logo for tenant list 2024-06-28 13:12:11 +03:00
dea872e497 Login:OAuth: add handle errors 2024-06-28 13:01:13 +03:00
11b5de43c8 Login: fix consent for public client 2024-06-27 18:21:13 +03:00
def151fe66 Client:OAuth: refactoring 2024-06-27 13:31:49 +03:00
341110b486 Client:OAuth: fix table for rtl 2024-06-26 18:48:00 +03:00
10b2e65d7f Shared:Components:Table: fix types for table cell 2024-06-26 17:47:45 +03:00
b2f47a0125 Login:LoginForm: restore auth for 1 portal for public client 2024-06-26 15:47:21 +03:00
125590044e Login:Consent: add login method 2024-06-26 15:42:20 +03:00
ab24a40a0e Client:OAuth: refactoring 2024-06-26 15:19:13 +03:00
d7914ad024 Login: add tenant-list route 2024-06-26 15:19:00 +03:00
ffe98ad175 Shared:Utils: delete console log 2024-06-24 15:32:46 +03:00
0fb2b762c0 Merge branch 'develop' into feature/oauth2-client 2024-06-24 14:01:36 +03:00
ec907c512d Login: add check for public oauth client 2024-06-19 18:14:43 +03:00
f16ba9f57e Public:Scripts:Config: add oauth2 publicClient flag 2024-06-19 15:19:18 +03:00
252eb14be5 Merge branch 'develop' into feature/oauth2-client 2024-06-19 15:16:18 +03:00
6145817cb8 Merge branch 'develop' into feature/oauth2-client 2024-06-18 13:12:53 +03:00
b7fbb88185 Merge branch 'develop' into feature/oauth2-client 2024-06-14 12:32:13 +03:00
9419551e8c Merge branch 'develop' into feature/oauth2-client 2024-06-06 12:39:08 +03:00
05c2f12d44 Merge branch 'develop' into feature/oauth2-client 2024-06-03 10:12:51 +03:00
58c6248d1f Client:Profile:AuthorizedApps: refactoring 2024-05-30 15:39:07 +03:00
680437524f Client:Profile: fix authorized-apps table 2024-05-30 13:57:26 +03:00
57d60d1798 Merge branch 'develop' into feature/oauth2-client 2024-05-30 13:12:55 +03:00
a3af4a49cc Login: delete old files 2024-05-30 13:08:50 +03:00
9c986d3bc7 Login:OAuth: fix login without auth cookie 2024-05-30 13:03:11 +03:00
fb3a0c51af Update yarn.lock 2024-05-30 10:07:20 +03:00
5e70e6fee5 Login: fix oauth pages 2024-05-30 10:07:09 +03:00
996c79d617 Revert "Merge branch 'develop'"
This reverts commit da29dab3bf.
2024-05-28 14:05:49 +03:00
da29dab3bf Merge branch 'develop' 2024-05-28 14:04:24 +03:00
d78e24f4bf Merge branch 'develop' into feature/oauth2-client 2024-05-28 14:00:07 +03:00
9594256e2f Client:PortalSettings:OAuth: fix table and row view 2024-05-28 13:53:42 +03:00
626f38f283 Client:PortalSettings:DeveloperTools: fix oauth translation key 2024-05-28 12:06:08 +03:00
02b4294e63 Merge branch 'develop' into feature/oauth2-client 2024-05-28 10:32:48 +03:00
e7c97f0806 Client:Pages:OAuth2: fix create and edit form after merge develop 2024-05-27 10:21:18 +03:00
33bc642d0d Fix errors after merge develop 2024-05-23 19:20:26 +03:00
3bb3c7d5ea Merge branch 'develop' into feature/oauth2-client 2024-05-23 18:23:05 +03:00
ac47e07815 Fix after merge 2024-02-20 12:58:40 +03:00
aa772acb03 Fix after merge 2024-02-20 12:47:10 +03:00
9573816860 Merge branch 'develop' into feature/oauth2-client 2024-02-20 12:43:09 +03:00
b7e9886d70 Fix after merge 2024-02-09 15:19:03 +03:00
9e981c09da Fix after merge 2024-02-09 15:15:33 +03:00
71e1ed6a77 Update runtime.json 2024-02-09 13:12:33 +03:00
8b828f9e2a Merge branch 'develop' into feature/oauth2-client 2024-02-09 13:05:17 +03:00
ea5dd21226 Merge branch 'develop' into feature/oauth2-client 2024-02-08 13:02:07 +03:00
6d0dd00052 Fix after merge 'develop' 2024-02-08 12:58:28 +03:00
c346033713 Merge branch 'develop' into feature/oauth2-client 2024-02-07 10:20:15 +03:00
740996ca7e Merge branch 'develop' into feature/oauth2-client 2024-01-22 10:11:13 +03:00
f129e8f1dc Client: fix after merge develop 2024-01-18 16:03:01 +03:00
16ada7a735 Client: fix after merge develop 2024-01-18 13:56:23 +03:00
a855f7b345 Merge branch 'develop' into feature/oauth2-client 2024-01-18 10:55:22 +03:00
e9602c6c43 Client:OAuth: remove tag icon 2023-12-22 12:32:31 +03:00
d5fd4a7868 Client:OAuth: fix update client 2023-12-21 14:06:30 +03:00
55d7b5e500 Client:OAuth: fix update 2023-12-21 14:06:15 +03:00
fbd5335167 Client:OAuth2: enable save button and throw error after click 2023-12-15 10:29:40 +03:00
06da361ba3 Merge branch 'develop' into feature/oauth2-client 2023-12-14 16:10:18 +03:00
82e0c8e3e9 Client:PortalSettings:OAuth2: add delete client dialog 2023-12-08 16:18:43 +03:00
edc3636ba0 Client:PortalSettings:OAuth2: add reset client secret dialog 2023-12-08 15:37:51 +03:00
84bc76263f Client:PortalSettings:OAuth2: add disable application dialog 2023-12-08 13:16:59 +03:00
8ed65d92d8 Client:PortalSettings:OAuth2: delete console log 2023-12-08 11:57:05 +03:00
80ebe7c16a Client:PortalSettings:OAuth2: enable allowed origins for edit, fix error translation 2023-12-08 11:55:02 +03:00
86ed0acd04 Client:PortalSettings:OAuth2: fix copy client id and client secret 2023-12-08 11:01:22 +03:00
20c3d69d42 Common:Utils:OAuth: fix scope list for openid 2023-12-05 18:33:15 +03:00
8b9c3af330 Client:PortalSettings:OAuth2: show error on input blur 2023-12-05 17:41:30 +03:00
20073c7158 Client:PortalSettings:OAuth2: fix translation 2023-12-05 17:00:46 +03:00
71c42ce188 Client:PortalSettings:OAuth2: hide write checkbox for openid 2023-12-05 13:30:12 +03:00
55d7dbcd64 Client:OAuth2: add allow pkce option 2023-12-04 12:48:49 +03:00
2fdc2d84ff OAuth2: remove useless cookie 2023-11-29 16:10:40 +03:00
ea9073c4e0 Client:OAuth2: add empty screen for authorized apps tab at profile 2023-11-29 15:43:58 +03:00
611ba72c9a Client:OAuth2: update translation keys 2023-11-29 15:09:32 +03:00
8b27e2f47a Client:OAuth2: add toastr for catch section 2023-11-29 13:41:18 +03:00
ec66aae56a Client:OAuth2: fix list loader 2023-11-29 13:31:14 +03:00
fa4a1c5213 Client:OAuth2: fix form validation 2023-11-29 11:55:45 +03:00
f6f8f67757 Client:OAuth2: fix error field layout 2023-11-29 11:44:57 +03:00
31982b1cfd Login:Oauth2: fix consent screen 2023-11-29 11:29:09 +03:00
4ca49ed173 OAuth2: add disable redirect cookie 2023-11-28 18:51:02 +03:00
0d3073375f Delete useless files 2023-11-28 13:05:03 +03:00
7031ade6dd Merge branch 'develop' into feature/oauth2-client 2023-11-28 13:01:05 +03:00
7cbcdb8180 Client:OAuth2: add state; fix preview dialog for client_secret_post authentication method 2023-11-28 12:57:11 +03:00
de3d152a9c Client:OAuth2: add loader for 'Reset' button 2023-11-28 12:13:41 +03:00
722ef797cb Client:OAuth2: add selector for authentication method 2023-11-28 12:00:24 +03:00
951de70db7 Client:OAuth2: fix scopes block 2023-11-28 11:38:40 +03:00
fea17b461c Client:Oauth2: fix translation keys 2023-11-28 11:37:40 +03:00
0c1b4420a7 Client:OAuth2: fix styles for edit form 2023-11-27 17:57:36 +03:00
50bcf1d577 Login:OAuth2: fix deny 2023-11-27 17:45:04 +03:00
1f6679f29a Client:OAuth2: restore allowed origins, add open id scope 2023-11-27 13:38:35 +03:00
4a09f4c562 Client:OAuth2:PreviewDialog: add authorize link as separate block 2023-11-23 11:27:40 +03:00
a525c677ef Login:OAuth2: fix error redirect 2023-11-23 10:43:10 +03:00
2cf00f3e1d Client:OAuth2: fix edit form validation 2023-11-22 16:27:25 +03:00
961b47feb7 Client:OAuth2: delete useless code 2023-11-22 14:29:17 +03:00
312140a0dc Client:OAuth2: generate PKCE pair at client 2023-11-22 14:28:29 +03:00
008a2b07ad Common:OAuth: fix scope list color 2023-11-22 10:42:23 +03:00
7c1387ab53 Client:OAuth2: add image compression 2023-11-21 18:03:40 +03:00
9fab8360ce Client:OAuth2: fix url validations 2023-11-21 14:49:58 +03:00
f3ce27d404 Client:OAuth2: add errors for client form create 2023-11-21 09:57:18 +03:00
f9bf6e08d0 Client:OAuth2: hide allowed origins 2023-11-20 11:04:17 +03:00
6e3b5d0338 Login: remove get sso if type oauth 2023-11-17 14:20:44 +03:00
61ae9c1a72 Login: hide loger 2023-11-15 18:40:27 +03:00
377f543baf Login: hide SSO 2023-11-15 18:39:02 +03:00
8391dad883 Login: add loggs 2023-11-15 18:34:50 +03:00
c00351f0f6 Login: add console log 2023-11-15 18:26:21 +03:00
711e799b05 Login: add console log 2023-11-15 18:25:49 +03:00
a80c0318ab Client: fix after merge 2023-11-15 17:28:31 +03:00
ae5b4d4ed8 Merge branch 'develop' into feature/oauth2-client 2023-11-15 17:13:50 +03:00
9dd8624bbb Client:OAuth2: change auth_method 2023-11-15 15:06:06 +03:00
5b5011ce8b Client:OAuth2: add revoke dialog 2023-11-15 11:24:58 +03:00
0a4cb47576 Client:OAuth2: add theme support 2023-11-15 10:21:40 +03:00
139c1f2cb3 Client:OAuth2:EditForm: add loader 2023-11-14 18:43:26 +03:00
ab1738172c Client:OAuth2:PreviewDialog: add 'Ok' button 2023-11-14 14:05:21 +03:00
3d766424e0 Client:OAuth2: add list loader 2023-11-14 13:34:22 +03:00
20b79e4833 Client:Profile: fix authorized apps, add revoke 2023-11-14 12:06:09 +03:00
571031397c Login:OAuth2: delete console log 2023-11-14 11:48:51 +03:00
280400f0a2 Login:OAuth2: fix consent change user 2023-11-14 11:38:44 +03:00
6c78635489 Client:OAuth2: fix create user translations 2023-11-13 16:51:36 +03:00
dd964bdbf9 Client:OAuth2: fix create user and displaying list 2023-11-13 16:41:39 +03:00
70ab743bf0 Login:OAuth2: fix consent state 2023-11-13 16:41:08 +03:00
045452631e Common:API:OAuth: fix onOAuthSubmit 2023-11-13 11:19:21 +03:00
ebe91b23b2 Client:Locales:OAuth: sort 2023-11-10 17:39:34 +03:00
a02fac57f0 Client:OAuth2: fix 2023-11-10 17:28:12 +03:00
01bb18a6a3 Client:OAuth2: fix translation key 2023-11-10 16:18:06 +03:00
6ce021dd3a Client:OAuth2: add translations keys 2023-11-10 16:05:55 +03:00
97e3c2769d Fix after merge 2023-11-10 13:15:47 +03:00
c1c9b7de02 Merge branch 'develop' into feature/oauth2-client 2023-11-10 12:01:49 +03:00
f0c94941e8 Client:Profile: add consent list 2023-11-09 18:35:46 +03:00
e691e88771 Login: fix consent screen, add redirect, remove useless requests 2023-11-09 14:31:37 +03:00
8c8df8af5a Client:OAuth2: fix delete operation 2023-11-08 17:25:30 +03:00
9067d28459 Client:OAuth2: add preview dialog 2023-11-08 16:55:06 +03:00
85c59b1075 Components:TextArea: remove background for copy icon 2023-11-08 16:54:37 +03:00
b179efeeae Client:OAuth2:InfoDialog: fix 2023-11-07 09:53:56 +03:00
76fdf6e9b6 Client:OAuth2: add info dialog 2023-11-03 17:51:40 +03:00
7902e9824d Client:PortalSettings: fix edit client form 2023-11-03 14:10:15 +03:00
147779cb13 Client: fix editing client form 2023-11-02 17:43:32 +03:00
fe29d4f897 Client:PortalSettings: fix table and row view 2023-11-02 15:52:47 +03:00
9f576f112c Client:PortalSettings: fix save client form 2023-11-02 13:06:54 +03:00
b593aa13f3 Client:PortalSettings:OAuth: fix fetch clients 2023-11-01 12:34:24 +03:00
8318dbb8f5 Client:OAuth2: fix logic for new client form 2023-10-31 18:34:07 +03:00
86f17497a8 PortalSettings:OAuth: add create client form 2023-10-27 18:24:18 +03:00
348383e2fb Login: fix consent screen 2023-10-27 11:55:30 +03:00
a4f555b853 Common:Utils: fix merge 2023-10-27 11:36:59 +03:00
51cc80ecfa Components:Theme: add separatorColor 2023-10-27 11:35:50 +03:00
b5c55da859 Merge branch 'develop' into feature/oauth2-client 2023-10-27 11:34:56 +03:00
ed4b027bcc Login: add support oauth2 2023-10-27 11:23:41 +03:00
f631a208f7 Common:Utils:OAuth: add scope list component; update interfaces and utils 2023-10-27 11:19:03 +03:00
380bec5048 Login:Locales: add transltation keys for consent page 2023-10-27 09:24:40 +03:00
16fca0a7c8 Locales: add transltation keys for oauth scopes 2023-10-27 09:23:54 +03:00
5d30cdca13 Common:Utils: update setCookie: disable encoding with flag 2023-10-26 10:24:56 +03:00
8dbfda6f2b Common:Axios: set new header cookie 'x-docspace-address' for oauth2 service, update return value on onSuccess request 2023-10-26 10:23:50 +03:00
f5c2bd64db Common:API: change url for oauth2 2023-10-20 10:01:28 +03:00
bba1e34a09 Merge branch 'develop' into feature/oauth2-client 2023-10-04 15:08:25 +03:00
86bded9492 Fix after merge 2023-10-04 15:03:39 +03:00
8dd604dc53 Merge branch 'develop' into feature/oauth2-client 2023-10-04 13:31:17 +03:00
ad047bc372 Merge branch 'develop' into feature/oauth2-client 2023-10-02 10:05:58 +03:00
e02510be1d Build: Docker: Fixed docker build scripts and .env 2023-09-29 16:50:36 +03:00
bb5b8cccdd Common: OAuth: Migration: Fixed sql script 2023-09-29 16:48:46 +03:00
ba23f9bb7b fixed migration 2023-09-29 14:39:22 +03:00
84afb50b66 merge from develop 2023-09-29 14:28:10 +03:00
aa6545bfe1 Build: add build docker oauth2 services for unix 2023-09-28 17:59:35 +03:00
4b888c6640 Build: add build docker oauth2 services 2023-09-28 17:42:06 +03:00
a34bedef1e Web:Client:OAUth: add preview 2023-09-28 14:36:53 +03:00
f6dca051ac Rename oauth service to oauth_api_service, add new oauth service 2023-09-28 13:26:00 +03:00
ca73c5b0c4 Web:Client:OAuth: add row view 2023-09-28 10:21:43 +03:00
807215a0cb Web:Client:OAuth: fix mobx warning, add infinity list for table view 2023-09-27 18:13:07 +03:00
2381fcf68f chore: remove react client 2023-09-27 18:14:30 +05:00
8ee516850b fix: use server side templates 2023-09-27 18:14:21 +05:00
0f9d61b9f0 fix: server side resources 2023-09-27 18:13:54 +05:00
bbce172bba Move ASC.OAuth from web to common 2023-09-27 14:05:59 +03:00
5f8718c78d Add yarn.lock for ASC.OAuth client 2023-09-27 14:02:16 +03:00
76e439bad3 Merge branch 'feature/oauth2' into feature/oauth2-client 2023-09-27 14:01:10 +03:00
d8a535432d Web:Client:PortalSettings:OAuth: add table view 2023-09-27 14:00:11 +03:00
6956adc231 Web:Client:PortalSettings: fix create and edit oauth client 2023-09-26 11:47:27 +03:00
310f2d5dc5 Web:Client:PortalSettings: add OAuth edit and create page 2023-09-26 10:31:22 +03:00
61c920555b Nginx: fix /api/scopes for OAuth 2023-09-25 17:04:55 +03:00
8b0bca3a44 Web:Client:Routes: fix OAuth create and edit routes 2023-09-25 17:04:30 +03:00
74e94c0a10 Web:Client:Store: rewrite OAuth store to typescipt 2023-09-25 17:03:57 +03:00
dc1274fb88 Web:Client:PortalSettings: add OAuth empty page 2023-09-25 17:03:25 +03:00
3b9c56c5f4 Web:Common:OAuth: add new utils, new interfaces 2023-09-25 17:02:28 +03:00
a56a46236c Web:Client:PortalSettings: add OAuth page 2023-09-25 10:09:24 +03:00
17f6195d8c Merge branch 'develop' into feature/oauth2-client 2023-09-25 08:54:25 +03:00
31bf677340 Merge branch 'develop' into feature/oauth2-client 2023-09-22 17:31:22 +03:00
8e751923d9 Web:OAuth2: add CRUD 2023-09-22 17:28:37 +03:00
ebdffb55ea Update yarn.lock 2023-09-22 15:38:32 +03:00
ac496b82a2 Add oauth service proxy 2023-09-22 15:38:11 +03:00
6d6116c5ce merge from develop 2023-09-22 13:39:24 +03:00
11c322f03b chore: client url 2023-09-13 17:41:05 +05:00
2a36d19707 feat: authorization server 2023-09-13 16:10:28 +05:00
4a7f1710de merge from develop 2023-08-15 19:50:23 +03:00
a4d69eebe8 Web: Client: OAuth: Fixed DTO and client view styles 2023-07-18 12:36:44 +03:00
e173c5932d Web: Client: OAuth: Added view page 2023-07-18 12:36:44 +03:00
fbaace1941 Web: Client: PortalSettings: Fixed OAuth settings page 2023-07-18 12:36:44 +03:00
a0b9e56097 Web: Client: PortalSettings: Added base OAuth page 2023-07-18 12:36:44 +03:00
06c63bdb6f ASC.MigrationPersonalToDocspace: fixed 2023-07-06 19:40:10 +03:00
d5b009554e backend: update JWT dll. Added authorize via JWT token 2023-06-29 20:47:47 +03:00
eefb4b5e2e refactoring:Authorization replace mvc filter to route 2023-06-16 19:46:14 +03:00
153 changed files with 11947 additions and 2242 deletions

45
.vscode/tasks.json vendored
View File

@ -62,6 +62,21 @@
"close": false
}
},
{
"label": "Backend | build SAAS + dnsmasq + identity",
"command": "cd ${workspaceFolder}/../ ; ${command:python.interpreterPath} buildtools/build.backend.docker.py -s -d -i",
"type": "shell",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
"close": false
}
},
{
"label": "Backend | rebuild SAAS + dnsmasq",
"command": "cd ${workspaceFolder}/../ ; ${command:python.interpreterPath} buildtools/build.backend.docker.py -s -d -f",
@ -92,6 +107,36 @@
"close": false
}
},
{
"label": "Backend | build EE + identity",
"command": "cd ${workspaceFolder}/../ ; ${command:python.interpreterPath} buildtools/build.backend.docker.py -i",
"type": "shell",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
"close": false
}
},
{
"label": "Backend | build EE + dnsmasq + identity",
"command": "cd ${workspaceFolder}/../ ; ${command:python.interpreterPath} buildtools/build.backend.docker.py -d -i",
"type": "shell",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
"close": false
}
},
{
"label": "Backend | clear",
"command": "cd ${workspaceFolder}/../ ; ${command:python.interpreterPath} buildtools/clear.backend.docker.py",

View File

@ -67,6 +67,11 @@
"task": "Backend | build SAAS + dnsmasq",
"tooltip": "🛠️ Start the \"backend docker build SAAS + dnsmasq\" task",
},
{
"label": "Docker : Build-SAAS + dnsmasq + identity",
"task": "Backend | build SAAS + dnsmasq + identity",
"tooltip": "🛠️ Start the \"backend docker build SAAS + dnsmasq + identity\" task",
},
{
"label": "Docker : ReBuild-SAAS + dnsmasq",
"task": "Backend | rebuild SAAS + dnsmasq",
@ -77,6 +82,16 @@
"task": "Backend | build EE + dnsmasq",
"tooltip": "🛠️ Start the \"backend docker build EE + dnsmasq\" task",
},
{
"label": "Docker : Build-EE + identity",
"task": "Backend | build EE + identity",
"tooltip": "🛠️ Start the \"backend docker build EE + identity\" task",
},
{
"label": "Docker : Build-EE + dnsmasq + identity",
"task": "Backend | build EE + dnsmasq + identity",
"tooltip": "🛠️ Start the \"backend docker build EE + dnsmasq + identity\" task",
},
{
"label": "Docker : Clear",
"task": "Backend | clear",

View File

@ -48,6 +48,7 @@
"@uiw/codemirror-theme-github": "^4.21.25",
"@uiw/react-codemirror": "^4.21.24",
"copy-to-clipboard": "^3.3.3",
"crypto-js": "^4.2.0",
"element-resize-detector": "^1.2.4",
"file-saver": "^2.0.5",
"firebase": "^10.8.0",
@ -73,6 +74,7 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@svgr/webpack": "^5.5.0",
"@types/crypto-js": "^4.2.1",
"@types/eslint": "^8.44.7",
"@types/he": "^1.2.3",
"@typescript-eslint/eslint-plugin": "^6.12.0",

View File

@ -0,0 +1,69 @@
{
"Access": "Access",
"AccessGranted": "Access granted",
"AppIcon": "App icon",
"AllowedOrigins": "Allowed origins",
"AllowedOriginsHelpButton": "URLs added here are used to improve the OAuth redirect security.",
"AllowPKCE": "Allow public client (PKCE)",
"AllowPKCEHelpButton": "PKCE is not a form of client authentication, and PKCE is not a replacement for a client secret or another client authentication type. PKCE is recommended even if a client is using a client secret or another form of client authentication like private_key_jwt.<br/> <strong>Note</strong>: Since PKCE is not a replacement for client authentication, it does not allow treating a public client as confidential one.",
"AppName": "App name",
"Apps": "Applications",
"AuthButton": "Auth button",
"AuthorizedApps": "Authorized apps",
"AuthorizeLink": "Authorize link",
"AuthenticationMethod": "Authentication method",
"Client": "Client",
"ClientCopy": "Client id successfully copied to clipboard",
"Creator": "Creator",
"ClientHelpButton": "Credentials for using OAth 2.0 as your Authentication type.<br/> <strong>Note</strong>: Any enterprise admin who knows the app's client ID will be able to retrieve information about the app including app name, authentication type, app scopes and redirect URI.",
"CodeVerifier": "Code verifier",
"DeleteHeader": "Delete application",
"DeleteDescription": "If you delete this application, all active consents and authorization will be revoked. If the user tries to open the consent screen for this app, an error will be thrown in the document space and the user will be redirected to the specified redirect URL.",
"DisableApplication": "Disable application",
"DisableApplicationDescription": "If you disable this application, all active consents and authorization will be disabled. If necessary, you can later enable the disabled application.",
"EditApp": "Edit application",
"EnterDescription": "Enter description",
"ErrorName": "Minimal name length:",
"ErrorWrongURL": "URL not valid, example",
"EnterURL": "Enter URL",
"IconDescription": "JPG, PNG or SVG, 32x32",
"ID": "ID",
"LastModified": "Last modified",
"NewApp": "New application",
"NoAuthorizedApps": "No authorized apps",
"NoOAuthAppHeader": "No OAuth applications",
"OAuth": "OAuth 2.0",
"OAuthAppDescription": "OAuth applications are used to access the ONLYOFFICE DocSpace API for authorization and further actions such as accessing files, etc.",
"OAuthHeaderBlock": "OAuth urls",
"ProfileDescription": "Here you can check the apps info to which you have granted the auth access, and revoke consent if needed.",
"PrivacyPolicy": "Privacy policy",
"PrivacyPolicyURL": "Privacy policy URL",
"PrivacyPolicyURLHelpButton": "Provide a URL link to your Privacy Policy that must comply with applicable laws and regulations and that make clear how you collect, use, share, retain and otherwise process personal information.",
"Read": "Read",
"RedirectsURLS": "Redirects URLS",
"RedirectsURLSHelpButton": "After a user successfully authorizes an application, the authorization server will redirect the user back to the application with sensitive information.",
"RegisterNewApp": "Register a new application",
"Reset": "Reset",
"ResetHeader": "Reset client secret",
"ResetDescription": "If you reset client secret, all active consents and authorization will be revoked. For apply next consent need use new client secret. Note that all users will again be required to complete the consent screen.",
"Revoke": "Revoke",
"RevokeConsent": "Revoke consent",
"RevokeConsentDescription": "Once you revoke the consent to use the ONLYOFFICE DocSpace auth data in the service {{name}}, ONLYOFFICE DocSpace will automatically stop logging into {{name}}. Your account in {{name}} will not be deleted.",
"RevokeConsentDescriptionGroup": "Once you revoke the consent to use the ONLYOFFICE DocSpace auth data in the services, ONLYOFFICE DocSpace will automatically stop logging. Your accounts will not be deleted.",
"RevokeConsentLogin": "If you want to renew an automatic login into {{name}} using ONLYOFFICE DocSpace, you will be asked to grant access to your DocSpace account data.",
"RevokeConsentLoginGroup": "If you want to renew an automatic login using ONLYOFFICE DocSpace, you will be asked to grant access to your DocSpace account data.",
"Secret": "Secret",
"SecretCopy": "Client secret successfully copied to clipboard",
"SelectNewImage": "Select new image",
"Scopes": "Scopes",
"ScopesHeader": "Access scopes",
"ScopesHelp": "Scopes are used to limit your app's access to all user-related data, and they'll let you specify exactly what kind of access you need.",
"SignIn": "Sign in with DocSpace",
"SupportAndLegalInfo": "Support & Legal info",
"TermsOfService": "Terms of Service",
"TermsOfServiceURL": "Terms of Service URL",
"TermsOfServiceURLHelpButton": "Terms and conditions that users must comply with when using this application.",
"ThisRequiredField": "This is a required field",
"WebsiteUrl": "Website URL",
"Write": "Write"
}

View File

@ -31,7 +31,7 @@ import { DeviceType } from "@docspace/shared/enums";
import { isTablet, isMobile, Context } from "@docspace/shared/utils";
import { isMobile as isMobileDevice } from "react-device-detect";
type DeviceUnionType = (typeof DeviceType)[keyof typeof DeviceType];
export type DeviceUnionType = (typeof DeviceType)[keyof typeof DeviceType];
type useViewEffectProps = {
view: string;

View File

@ -46,6 +46,8 @@ const PrivateRouteWrapper = ({
restricted,
withCollaborator,
withManager,
identityServerEnabled,
limitedAccessSpace,
baseDomain,
}: Partial<PrivateRouteProps>) => {
return (
@ -65,6 +67,8 @@ const PrivateRouteWrapper = ({
withCollaborator={withCollaborator}
isPortalDeactivate={isPortalDeactivate!}
enablePortalRename={enablePortalRename!}
identityServerEnabled={identityServerEnabled}
limitedAccessSpace={limitedAccessSpace ?? null}
baseDomain={baseDomain}
>
{children}
@ -82,7 +86,10 @@ export default inject<TStore>(
isLogout,
isCommunity,
isEnterprise,
capabilities,
} = authStore;
const identityServerEnabled = capabilities?.identityServerEnabled;
const { isNotPaidPeriod } = currentTariffStatusStore;
const { user } = userStore;
@ -91,6 +98,7 @@ export default inject<TStore>(
tenantStatus,
isPortalDeactivate,
enablePortalRename,
limitedAccessSpace,
baseDomain,
} = settingsStore;
@ -107,6 +115,8 @@ export default inject<TStore>(
isLogout,
isEnterprise,
enablePortalRename,
identityServerEnabled,
limitedAccessSpace,
baseDomain,
};
},

View File

@ -27,8 +27,9 @@
import React, { useEffect } from "react";
import { Loader } from "@docspace/shared/components/loader";
import Section from "@docspace/shared/components/section";
import { getCookie, deleteCookie } from "@docspace/shared/utils/cookie";
import { loginWithConfirmKey } from "@docspace/shared/api/user";
import { useSearchParams } from "react-router-dom";
import { useSearchParams, useLocation } from "react-router-dom";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import { toastr } from "@docspace/shared/components/toast";
import { frameCallEvent } from "@docspace/shared/utils/common";
@ -37,6 +38,7 @@ const Auth = (props) => {
//console.log("Auth render");
const { linkData } = props;
let [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
useEffect(() => {
loginWithConfirmKey({
ConfirmData: {
@ -50,6 +52,22 @@ const Auth = (props) => {
const url = searchParams.get("referenceUrl");
const redirectUrl = getCookie("x-redirect-authorization-uri");
deleteCookie("x-redirect-authorization-uri");
if (redirectUrl) {
window.location.replace(redirectUrl);
return;
}
if (url && url.includes("oauth2")) {
const newUrl = location.search.split("referenceUrl=")[1];
window.location.replace(newUrl);
return;
}
if (url) {
try {
new URL(url);

View File

@ -49,7 +49,7 @@ import {
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import TariffBar from "SRC_DIR/components/TariffBar";
const HeaderContainer = styled.div`
export const HeaderContainer = styled.div`
position: relative;
display: flex;
align-items: center;
@ -157,7 +157,7 @@ const HeaderContainer = styled.div`
}
`;
const StyledContainer = styled.div`
export const StyledContainer = styled.div`
.group-button-menu-container {
${(props) =>
props.viewAs === "table"

View File

@ -31,13 +31,14 @@ import { SectionHeaderContent, SectionPagingContent } from "./Section";
import { inject, observer } from "mobx-react";
import Section from "@docspace/shared/components/section";
import withLoading from "SRC_DIR/HOCs/withLoading";
import ArticleWrapper from "SRC_DIR/components/ArticleWrapper";
import SectionWrapper from "SRC_DIR/components/Section";
import { useParams } from "react-router-dom";
import HistoryHeader from "../categories/developer-tools/Webhooks/WebhookHistory/sub-components/HistoryHeader";
import DetailsNavigationHeader from "../categories/developer-tools/Webhooks/WebhookEventDetails/sub-components/DetailsNavigationHeader";
import ArticleWrapper from "SRC_DIR/components/ArticleWrapper";
import OAuthSectionHeader from "../categories/developer-tools/OAuth/OAuthSectionHeader";
const ArticleSettings = React.memo(({ showArticleLoader, needPageReload }) => {
const onLogoClickAction = () => {
@ -88,6 +89,8 @@ const Layout = ({
const webhookHistoryPath = `/portal-settings/developer-tools/webhooks/${id}`;
const webhookDetailsPath = `/portal-settings/developer-tools/webhooks/${id}/${eventId}`;
const oauthCreatePath = `/portal-settings/developer-tools/oauth/create`;
const oauthEditPath = `/portal-settings/developer-tools/oauth/${id}`;
const currentPath = window.location.pathname;
return (
@ -107,6 +110,9 @@ const Layout = ({
<HistoryHeader />
) : currentPath === webhookDetailsPath ? (
<DetailsNavigationHeader />
) : currentPath === oauthCreatePath ||
currentPath === oauthEditPath ? (
<OAuthSectionHeader />
) : (
<SectionHeaderContent />
)}

View File

@ -171,7 +171,7 @@ export const Frame = styled(Box)`
position: relative;
border-radius: 6px;
border: 1px solid ${(props) => props.theme.sdkPresets.borderColor};
border: 1px solid ${(props) => props.theme.sdkPresets?.borderColor};
width: calc(${(props) => (props.width ? props.width : "100%")} + 2px);
height: calc(${(props) => (props.height ? props.height : "100%")} + 2px);

View File

@ -0,0 +1,23 @@
import { IClientProps } from "@docspace/shared/utils/oauth/types";
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
export interface OAuthProps {
viewAs: ViewAsType;
setViewAs: (viewAs: string) => void;
clientList: IClientProps[];
isEmptyClientList: boolean;
fetchClients: () => Promise<void>;
fetchScopes: () => Promise<void>;
currentDeviceType: DeviceUnionType;
infoDialogVisible?: boolean;
previewDialogVisible?: boolean;
disableDialogVisible?: boolean;
deleteDialogVisible?: boolean;
isInit: boolean;
setIsInit: (value: boolean) => void;
}

View File

@ -0,0 +1,18 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { setDocumentTitle } from "SRC_DIR/helpers/utils";
import ClientForm from "../sub-components/ClientForm";
const OAuthCreatePage = () => {
const { t } = useTranslation(["OAuth"]);
React.useEffect(() => {
setDocumentTitle(t("OAuth"));
}, [t]);
return <ClientForm />;
};
export default OAuthCreatePage;

View File

@ -0,0 +1,21 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { setDocumentTitle } from "SRC_DIR/helpers/utils";
import ClientForm from "../sub-components/ClientForm";
const OAuthEditPage = () => {
const { id } = useParams();
const { t } = useTranslation(["OAuth"]);
React.useEffect(() => {
setDocumentTitle(t("OAuth"));
}, [t]);
return <ClientForm id={id} />;
};
export default OAuthEditPage;

View File

@ -0,0 +1,49 @@
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import Headline from "@docspace/shared/components/headline/Headline";
import { IconButton } from "@docspace/shared/components/icon-button";
import ArrowPathReactSvgUrl from "PUBLIC_DIR/images/arrow.path.react.svg?url";
import LoaderSectionHeader from "SRC_DIR/pages/PortalSettings/Layout/Section/loaderSectionHeader";
import {
StyledContainer,
HeaderContainer,
} from "../../../../Layout/Section/Header";
const OAuthSectionHeader = ({ isEdit }: { isEdit: boolean }) => {
const { t, ready } = useTranslation(["OAuth"]);
const navigate = useNavigate();
const onBack = () => {
navigate("/portal-settings/developer-tools/oauth");
};
if (!ready) return <LoaderSectionHeader />;
return (
<StyledContainer>
<HeaderContainer>
<Headline type="content" truncate>
<div className="settings-section_header">
<div className="header">
<IconButton
iconName={ArrowPathReactSvgUrl}
size={17}
isFill
onClick={onBack}
className="arrow-button"
/>
{isEdit ? t("EditApp") : t("NewApp")}
</div>
</div>
</Headline>
</HeaderContainer>
</StyledContainer>
);
};
export default OAuthSectionHeader;

View File

@ -0,0 +1,10 @@
import styled from "styled-components";
export const OAuthContainer = styled.div`
width: 100%;
.ec-subheading {
margin-top: 8px;
text-align: center;
}
`;

View File

@ -0,0 +1,148 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import useViewEffect from "SRC_DIR/Hooks/useViewEffect";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import { setDocumentTitle } from "SRC_DIR/helpers/utils";
import { OAuthContainer } from "./StyledOAuth";
import { OAuthProps } from "./OAuth.types";
import InfoDialog from "./sub-components/InfoDialog";
import PreviewDialog from "./sub-components/PreviewDialog";
import OAuthLoader from "./sub-components/List/Loader";
import DisableDialog from "./sub-components/DisableDialog";
import DeleteDialog from "./sub-components/DeleteDialog";
import OAuthEmptyScreen from "./sub-components/EmptyScreen";
import List from "./sub-components/List";
const MIN_LOADER_TIME = 500;
const OAuth = ({
clientList,
viewAs,
isEmptyClientList,
setViewAs,
fetchClients,
fetchScopes,
currentDeviceType,
infoDialogVisible,
previewDialogVisible,
isInit,
setIsInit,
disableDialogVisible,
deleteDialogVisible,
}: OAuthProps) => {
const { t } = useTranslation(["OAuth"]);
const [isLoading, setIsLoading] = React.useState<boolean>(true);
const startLoadingRef = React.useRef<null | Date>(null);
const getData = React.useCallback(async () => {
if (isInit) return;
const actions = [];
actions.push(fetchScopes(), fetchClients());
await Promise.all(actions);
if (startLoadingRef.current) {
const currentDate = new Date();
const ms = Math.abs(
startLoadingRef.current.getTime() - currentDate.getTime(),
);
if (ms < MIN_LOADER_TIME)
return setTimeout(() => {
setIsLoading(false);
setIsInit(true);
}, MIN_LOADER_TIME - ms);
}
setIsLoading(false);
setIsInit(true);
}, [fetchClients, fetchScopes, isInit, setIsInit]);
useViewEffect({
view: viewAs,
setView: setViewAs,
currentDeviceType,
});
React.useEffect(() => {
if (isInit) return setIsLoading(false);
startLoadingRef.current = new Date();
getData();
}, [getData, setIsInit, isInit]);
React.useEffect(() => {
setDocumentTitle(t("OAuth"));
}, [t]);
return (
<OAuthContainer>
{isLoading ? (
<OAuthLoader viewAs={viewAs} currentDeviceType={currentDeviceType} />
) : isEmptyClientList ? (
<OAuthEmptyScreen />
) : (
<List
clients={clientList}
viewAs={viewAs}
currentDeviceType={currentDeviceType}
/>
)}
{infoDialogVisible && <InfoDialog visible={infoDialogVisible} />}
{disableDialogVisible && <DisableDialog />}
{previewDialogVisible && <PreviewDialog visible={previewDialogVisible} />}
{deleteDialogVisible && <DeleteDialog />}
</OAuthContainer>
);
};
export default inject(
({
oauthStore,
settingsStore,
}: {
oauthStore: OAuthStoreProps;
settingsStore: SettingsStore;
}) => {
const { currentDeviceType } = settingsStore;
const {
viewAs,
setViewAs,
clientList,
isEmptyClientList,
fetchClients,
fetchScopes,
infoDialogVisible,
previewDialogVisible,
isInit,
setIsInit,
disableDialogVisible,
deleteDialogVisible,
} = oauthStore;
return {
viewAs,
setViewAs,
clientList,
isEmptyClientList,
fetchClients,
currentDeviceType,
infoDialogVisible,
previewDialogVisible,
fetchScopes,
isInit,
setIsInit,
disableDialogVisible,
deleteDialogVisible,
};
},
)(observer(OAuth));

View File

@ -0,0 +1,315 @@
import styled from "styled-components";
import { mobile } from "@docspace/shared/utils/device";
import { Base } from "@docspace/shared/themes";
const StyledContainer = styled.div`
width: 100%;
max-width: 660px;
display: flex;
flex-direction: column;
gap: 24px;
.loader {
rect {
width: 100%;
}
}
.scope-name-loader {
margin-bottom: 4px;
}
.scope-desc-loader {
margin-bottom: 2px;
}
`;
const StyledBlock = styled.div`
width: 100%;
height: auto;
display: flex;
flex-direction: column;
gap: 12px;
.icon-field {
margin: 0;
}
`;
const StyledHeaderRow = styled.div`
width: 100%;
display: flex;
flex-direction: row;
gap: 4px;
align-items: center;
div {
height: 12px;
}
`;
const StyledInputBlock = styled.div`
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
@media ${mobile} {
display: flex;
flex-direction: column;
}
`;
const StyledInputGroup = styled.div`
width: 100%;
height: auto;
display: flex;
flex-direction: column;
gap: 4px;
svg {
cursor: pointer;
}
.pkce {
margin-top: 4px;
display: flex;
align-items: center;
gap: 0px;
}
.public_client {
margin-top: 4px;
display: flex;
align-items: center;
label {
position: relative;
}
}
.label {
height: 20px;
}
.select {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin: 4px 0;
.client-logo {
max-width: 32px;
max-height: 32px;
width: 32px;
height: 32px;
border-radius: 3px;
}
p {
color: ${(props) => props.theme.oauth.clientForm.descriptionColor};
}
}
.description {
color: ${(props) => props.theme.oauth.clientForm.descriptionColor};
}
.input-block-with-button {
.field-body {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
}
`;
StyledInputGroup.defaultProps = { theme: Base };
const StyledInputRow = styled.div`
width: 100%;
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;
input {
user-select: none;
}
`;
const StyledChipsContainer = styled.div`
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 4px;
`;
const StyledScopesContainer = styled.div`
width: 100%;
display: grid;
grid-template-columns: 1fr max-content max-content;
align-items: center;
gap: 16px 0;
.header {
padding-bottom: 8px;
padding-right: 24px;
margin-right: -12px;
border-bottom: ${(props) => props.theme.oauth.clientForm.headerBorder};
}
.header-last {
margin-right: 0px;
padding-right: 0px;
}
.checkbox-read {
margin-right: 12px;
}
`;
StyledScopesContainer.defaultProps = { theme: Base };
const StyledScopesName = styled.div`
display: flex;
flex-direction: column;
.scope-name {
margin-bottom: 2px;
}
.scope-desc {
color: ${(props) => props.theme.oauth.clientForm.scopeDesc};
}
`;
StyledScopesName.defaultProps = { theme: Base };
const StyledScopesCheckbox = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: flex-start;
justify-content: flex-end;
.checkbox {
margin-right: 0px;
}
`;
const StyledButtonContainer = styled.div`
width: fit-content;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
@media ${mobile} {
width: 100%;
}
`;
const StyledInputAddBlock = styled.div`
width: calc(100% - 40px);
height: 44px;
padding: 0 6px;
box-sizing: border-box;
cursor: pointer;
z-index: 200;
display: none;
align-items: center;
justify-content: space-between;
gap: 10px;
background: ${(props) => props.theme.backgroundColor};
position: absolute;
top: 40px;
left: 0px;
border-radius: 3px;
border: ${(props) => props.theme.oauth.clientForm.headerBorder};
box-shadow: ${(props) => props.theme.navigation.boxShadow};
.add-block {
display: flex;
align-items: center;
gap: 4px;
p {
color: #4781d1;
}
svg path {
fill: #4781d1;
}
}
`;
const StyledCheckboxGroup = styled.div`
width: 100%;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin-top: 4px;
`;
export {
StyledContainer,
StyledBlock,
StyledHeaderRow,
StyledInputBlock,
StyledInputGroup,
StyledInputRow,
StyledChipsContainer,
StyledScopesContainer,
StyledScopesName,
StyledScopesCheckbox,
StyledButtonContainer,
StyledInputAddBlock,
StyledCheckboxGroup,
};

View File

@ -0,0 +1,63 @@
import {
IClientProps,
IClientReqDTO,
TScope,
} from "@docspace/shared/utils/oauth/types";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
export interface InputProps {
value: string;
name: string;
placeholder: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
isReadOnly?: boolean;
isSecret?: boolean;
withCopy?: boolean;
withButton?: boolean;
buttonLabel?: string;
onClickButton?: () => void;
multiplyInput?: boolean;
}
export interface CheckboxProps {
isChecked: boolean;
onChange: () => void;
label: string;
description: string;
}
export interface BlockProps {
children: React.ReactNode;
}
export interface ClientFormProps {
id?: string;
client?: IClientProps;
scopeList?: TScope[];
fetchScopes?: () => Promise<void>;
saveClient?: (client: IClientReqDTO) => Promise<void>;
updateClient?: (clientId: string, client: IClientReqDTO) => Promise<void>;
resetDialogVisible?: boolean;
setResetDialogVisible?: (value: boolean) => void;
currentDeviceType?: DeviceUnionType;
setClientSecretProps?: (value: string) => void;
clientSecretProps?: string;
}
export interface ClientStore {
settingsStore: SettingsStore;
oauthStore: OAuthStoreProps;
}

View File

@ -0,0 +1,9 @@
export function isValidUrl(url: string) {
try {
const newUrl = new URL(url);
if (newUrl) return true;
return false;
} catch (err) {
return false;
}
}

View File

@ -0,0 +1,316 @@
import React from "react";
import { RectangleSkeleton } from "@docspace/shared/skeletons/rectangle";
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
import {
StyledBlock,
StyledButtonContainer,
StyledCheckboxGroup,
StyledContainer,
StyledHeaderRow,
StyledInputBlock,
StyledInputGroup,
StyledInputRow,
StyledScopesCheckbox,
StyledScopesContainer,
StyledScopesName,
} from "./ClientForm.styled";
const HelpButtonSkeleton = () => {
return <RectangleSkeleton width="12px" height="12px" />;
};
const CheckboxSkeleton = ({ className }: { className?: string }) => {
return <RectangleSkeleton className={className} width="16px" height="16px" />;
};
const ClientFormLoader = ({
currentDeviceType,
isEdit,
}: {
currentDeviceType?: DeviceUnionType;
isEdit: boolean;
}) => {
const buttonHeight = currentDeviceType !== "desktop" ? "40px" : "32px";
return (
<StyledContainer>
<StyledBlock>
<StyledHeaderRow>
<RectangleSkeleton width="78px" height="22px" />
</StyledHeaderRow>
<StyledInputBlock>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="65px" height="20px" />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width="100%" height="32px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="80px" height="20px" />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width="100%" height="32px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<div className="label">
<RectangleSkeleton width="60px" height="20px" />
</div>
<div className="select">
<RectangleSkeleton width="32px" height="32px" />
<RectangleSkeleton width="32px" height="32px" />
<RectangleSkeleton width="109px" height="20px" />
</div>
<RectangleSkeleton width="130px" height="16px" />
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="75px" height="20px" />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width="100%" height="60px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="75px" height="20px" />
</StyledHeaderRow>
<StyledCheckboxGroup>
<CheckboxSkeleton />
<RectangleSkeleton width="151px" height="18px" />
<HelpButtonSkeleton />
</StyledCheckboxGroup>
</StyledInputGroup>
</StyledInputBlock>
</StyledBlock>
{isEdit && (
<StyledBlock>
<StyledHeaderRow>
<RectangleSkeleton width="47px" height="22px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<StyledInputBlock>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="96px" height="20px" />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width="100%" height="32px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="60px" height="20px" />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton
className="loader"
width="calc(100% - 91px)"
height="32px"
/>
<RectangleSkeleton width="91px" height="32px" />
</StyledInputRow>
</StyledInputGroup>
</StyledInputBlock>
</StyledBlock>
)}
<StyledBlock>
<StyledHeaderRow>
<RectangleSkeleton width="96px" height="22px" />
</StyledHeaderRow>
<StyledInputBlock>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="87px" height="20px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton
className="loader"
width="calc(100% - 40px)"
height="32px"
/>
<RectangleSkeleton width="32px" height="32px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="96px" height="20px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton
className="loader"
width="calc(100% - 40px)"
height="32px"
/>
<RectangleSkeleton width="32px" height="32px" />
</StyledInputRow>
</StyledInputGroup>
</StyledInputBlock>
</StyledBlock>
<StyledScopesContainer>
<StyledHeaderRow className="header">
<RectangleSkeleton width="111px" height="22px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<RectangleSkeleton className="header" width="34px" height="22px" />
<RectangleSkeleton
className="header header-last"
width="37px"
height="22px"
/>
<StyledScopesName>
<RectangleSkeleton
className="scope-name-loader"
width="98px"
height="16px"
/>
<RectangleSkeleton
className="scope-desc-loader"
width="200px"
height="17px"
/>
<RectangleSkeleton
className="scope-desc-loader"
width="230px"
height="17px"
/>
</StyledScopesName>
<StyledScopesCheckbox>
<CheckboxSkeleton className="checkbox-read" />
</StyledScopesCheckbox>
<StyledScopesCheckbox>
<CheckboxSkeleton />
</StyledScopesCheckbox>
<StyledScopesName>
<RectangleSkeleton
className="scope-name-loader"
width="98px"
height="16px"
/>
<RectangleSkeleton
className="scope-desc-loader"
width="200px"
height="17px"
/>
<RectangleSkeleton
className="scope-desc-loader"
width="230px"
height="17px"
/>
</StyledScopesName>
<StyledScopesCheckbox>
<CheckboxSkeleton className="checkbox-read" />
</StyledScopesCheckbox>
<StyledScopesCheckbox>
<CheckboxSkeleton />
</StyledScopesCheckbox>
<StyledScopesName>
<RectangleSkeleton
className="scope-name-loader"
width="98px"
height="16px"
/>
<RectangleSkeleton
className="scope-desc-loader"
width="200px"
height="17px"
/>
<RectangleSkeleton
className="scope-desc-loader"
width="230px"
height="17px"
/>
</StyledScopesName>
<StyledScopesCheckbox>
<CheckboxSkeleton className="checkbox-read" />
</StyledScopesCheckbox>
<StyledScopesCheckbox>
<CheckboxSkeleton />
</StyledScopesCheckbox>
<StyledScopesName>
<RectangleSkeleton
className="scope-name-loader"
width="98px"
height="16px"
/>
<RectangleSkeleton
className="scope-desc-loader"
width="200px"
height="17px"
/>
<RectangleSkeleton
className="scope-desc-loader"
width="230px"
height="17px"
/>
</StyledScopesName>
<StyledScopesCheckbox>
<CheckboxSkeleton className="checkbox-read" />
</StyledScopesCheckbox>
<StyledScopesCheckbox>
<CheckboxSkeleton />
</StyledScopesCheckbox>{" "}
<StyledScopesName>
<RectangleSkeleton
className="scope-name-loader"
width="98px"
height="16px"
/>
<RectangleSkeleton
className="scope-desc-loader"
width="200px"
height="17px"
/>
</StyledScopesName>
<StyledScopesCheckbox>
<CheckboxSkeleton className="checkbox-read" />
</StyledScopesCheckbox>
</StyledScopesContainer>
<StyledBlock>
<StyledHeaderRow>
<RectangleSkeleton width="162px" height="22px" />
</StyledHeaderRow>
<StyledInputBlock>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="114px" height="20px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width="100%" height="32px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="96px" height="20px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width="100%" height="32px" />
</StyledInputRow>
</StyledInputGroup>
</StyledInputBlock>
</StyledBlock>
<StyledButtonContainer>
<RectangleSkeleton
width={currentDeviceType === "desktop" ? "86px" : "100%"}
height={buttonHeight}
/>
<RectangleSkeleton
width={currentDeviceType === "desktop" ? "86px" : "100%"}
height={buttonHeight}
/>
</StyledButtonContainer>
</StyledContainer>
);
};
export default ClientFormLoader;

View File

@ -0,0 +1,257 @@
import React from "react";
import { Trans } from "react-i18next";
import { TTranslation } from "@docspace/shared/types";
import { HelpButton } from "@docspace/shared/components/help-button";
import { FieldContainer } from "@docspace/shared/components/field-container";
import { Checkbox } from "@docspace/shared/components/checkbox";
import { IClientReqDTO } from "@docspace/shared/utils/oauth/types";
// import { ToggleButton } from "@docspace/shared/components/toggle-button";
// import { Text } from "@docspace/shared/components/text";
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
import BlockHeader from "./BlockHeader";
import InputGroup from "./InputGroup";
import TextAreaGroup from "./TextAreaGroup";
import SelectGroup from "./SelectGroup";
interface BasicBlockProps {
t: TTranslation;
nameValue: string;
websiteUrlValue: string;
logoValue: string;
descriptionValue: string;
allowPkce: boolean;
// isPublic: boolean;
changeValue: (
name: keyof IClientReqDTO,
value: string | boolean,
remove?: boolean,
) => void;
isEdit: boolean;
errorFields: string[];
requiredErrorFields: string[];
onBlur: (name: string) => void;
}
function getImageDimensions(
image: HTMLImageElement,
): Promise<{ width: number; height: number }> {
return new Promise((resolve) => {
image.onload = () => {
const width = image.width;
const height = image.height;
resolve({ height, width });
};
});
}
function compressImage(
image: HTMLImageElement,
scale: number,
initialWidth: number,
initialHeight: number,
): Promise<Blob | undefined | null> {
return new Promise((resolve) => {
const canvas = document.createElement("canvas");
canvas.width = scale * initialWidth;
canvas.height = scale * initialHeight;
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
ctx.canvas.toBlob((blob) => {
resolve(blob);
}, "image/png");
}
});
}
const BasicBlock = ({
t,
nameValue,
websiteUrlValue,
logoValue,
descriptionValue,
allowPkce,
// isPublic,
changeValue,
isEdit,
errorFields,
requiredErrorFields,
onBlur,
}: BasicBlockProps) => {
const onChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const target = e.target;
changeValue(target.name as keyof IClientReqDTO, target.value);
};
const onSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file =
e.target.files && e.target.files?.length > 0 && e.target.files[0];
if (file) {
const imgEl = document.getElementsByClassName(
"client-logo",
)[0] as HTMLImageElement;
imgEl.src = URL.createObjectURL(file);
const { height, width } = await getImageDimensions(imgEl);
const MAX_WIDTH = 32; // if we resize by width, this is the max width of compressed image
const MAX_HEIGHT = 32; // if we resize by height, this is the max height of the compressed image
const widthRatioBlob = await compressImage(
imgEl,
MAX_WIDTH / width,
width,
height,
);
const heightRatioBlob = await compressImage(
imgEl,
MAX_HEIGHT / height,
width,
height,
);
if (widthRatioBlob && heightRatioBlob) {
// pick the smaller blob between both
const compressedBlob =
widthRatioBlob.size > heightRatioBlob.size
? heightRatioBlob
: widthRatioBlob;
const reader = new FileReader();
reader.readAsDataURL(compressedBlob);
reader.onload = () => {
const result = reader.result as string;
changeValue("logo", result);
};
}
}
};
const pkceHelpButtonText = (
<Trans t={t} i18nKey="AllowPKCEHelpButton" ns="OAuth" />
);
// const publicClientHelpButtonText = "Help text";
const isNameRequiredError = requiredErrorFields.includes("name");
const isWebsiteRequiredError = requiredErrorFields.includes("website_url");
const isNameError = errorFields.includes("name");
const isWebsiteError = errorFields.includes("website_url");
const isLogoRequiredError = requiredErrorFields.includes("logo");
return (
<StyledBlock>
<BlockHeader header="Basic info" />
<StyledInputBlock>
<InputGroup
label={t("AppName")}
name="name"
placeholder={t("Common:EnterName")}
value={nameValue}
error={isNameError ? `${t("ErrorName")} 3` : t("ThisRequiredField")}
onChange={onChange}
isRequired
isError={isNameRequiredError || isNameError}
onBlur={onBlur}
/>
<InputGroup
label={t("WebsiteUrl")}
name="website_url"
placeholder={t("EnterURL")}
value={websiteUrlValue}
error={
isWebsiteError
? `${t("ErrorWrongURL")}: ${window.location.origin}`
: t("ThisRequiredField")
}
onChange={onChange}
disabled={isEdit}
isRequired
isError={isWebsiteRequiredError || isWebsiteError}
onBlur={onBlur}
/>
<FieldContainer
isVertical
labelVisible={false}
errorMessage={t("ThisRequiredField")}
hasError={isLogoRequiredError}
className="icon-field"
>
<SelectGroup
label={t("AppIcon")}
value={logoValue}
selectLabel={t("SelectNewImage")}
description={t("IconDescription")}
onSelect={onSelect}
/>
</FieldContainer>
<TextAreaGroup
label={t("Common:Description")}
name="description"
placeholder={t("EnterDescription")}
value={descriptionValue}
onChange={onChange}
increaseHeight={isLogoRequiredError}
/>
<InputGroup
label={t("AuthenticationMethod")}
name="auth_method"
placeholder={t("EnterURL")}
value={websiteUrlValue}
error=""
onChange={() => {}}
>
<div className="pkce">
<Checkbox
label={t("AllowPKCE")}
isChecked={allowPkce}
onChange={() => {
changeValue("allow_pkce", !allowPkce);
}}
/>
<HelpButton tooltipContent={pkceHelpButtonText} />
</div>
</InputGroup>
{/* <InputGroup
label="Client type"
name="public_client"
placeholder={t("EnterURL")}
value=""
error=""
onChange={() => {}}
>
<div className="public_client">
<ToggleButton
isChecked={isPublic}
onChange={(e) => {
changeValue("is_public", e.target.checked);
}}
/>
<Text>Public client</Text>
<HelpButton tooltipContent={publicClientHelpButtonText} />
</div>
</InputGroup> */}
</StyledInputBlock>
</StyledBlock>
);
};
export default BasicBlock;

View File

@ -0,0 +1,36 @@
import { Text } from "@docspace/shared/components/text";
import { HelpButton } from "@docspace/shared/components/help-button";
import { StyledHeaderRow } from "../ClientForm.styled";
interface BlockHeaderProps {
header: string;
helpButtonText?: string | React.ReactNode;
className?: string;
}
const BlockHeader = ({
header,
helpButtonText,
className,
}: BlockHeaderProps) => {
return (
<StyledHeaderRow className={className}>
<Text
fontSize="16px"
fontWeight={700}
lineHeight="22px"
title={header}
tag=""
as="p"
color=""
textAlign=""
>
{header}
</Text>
{helpButtonText && <HelpButton tooltipContent={helpButtonText} />}
</StyledHeaderRow>
);
};
export default BlockHeader;

View File

@ -0,0 +1,57 @@
import { DeviceType } from "@docspace/shared/enums";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { StyledButtonContainer } from "../ClientForm.styled";
interface ButtonsBlockProps {
saveLabel: string;
cancelLabel: string;
isRequestRunning: boolean;
saveButtonDisabled: boolean;
cancelButtonDisabled: boolean;
onSaveClick: () => void;
onCancelClick: () => void;
currentDeviceType: string;
}
const ButtonsBlock = ({
saveLabel,
cancelLabel,
isRequestRunning,
saveButtonDisabled,
cancelButtonDisabled,
onSaveClick,
onCancelClick,
currentDeviceType,
}: ButtonsBlockProps) => {
const isDesktop = currentDeviceType === DeviceType.desktop;
const buttonSize = isDesktop ? ButtonSize.small : ButtonSize.normal;
return (
<StyledButtonContainer>
<Button
label={saveLabel}
isLoading={isRequestRunning}
isDisabled={saveButtonDisabled}
primary
size={buttonSize}
scale={!isDesktop}
onClick={onSaveClick}
/>
<Button
label={cancelLabel}
isDisabled={cancelButtonDisabled}
size={buttonSize}
scale={!isDesktop}
onClick={onCancelClick}
/>
</StyledButtonContainer>
);
};
export default ButtonsBlock;

View File

@ -0,0 +1,83 @@
import React from "react";
import { Trans } from "react-i18next";
import copy from "copy-to-clipboard";
import { toastr } from "@docspace/shared/components/toast";
import { TTranslation } from "@docspace/shared/types";
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
import BlockHeader from "./BlockHeader";
import InputGroup from "./InputGroup";
interface ClientBlockProps {
t: TTranslation;
idValue: string;
secretValue: string;
onResetClick: () => void;
}
const ClientBlock = ({
t,
idValue,
secretValue,
onResetClick,
}: ClientBlockProps) => {
const [value, setValue] = React.useState<{ [key: string]: string }>({
id: idValue,
secret: secretValue,
});
React.useEffect(() => {
setValue({ id: idValue, secret: secretValue });
}, [idValue, secretValue]);
const onChange = () => {};
const onCopyClick = (name: string) => {
if (name === "id") {
copy(value[name]);
toastr.success(t("ClientCopy"));
} else {
copy(value[name]);
toastr.success(t("SecretCopy"));
}
};
const helpButtonText = <Trans t={t} i18nKey="ClientHelpButton" ns="OAuth" />;
return (
<StyledBlock>
<BlockHeader header={t("Client")} helpButtonText={helpButtonText} />
<StyledInputBlock>
<InputGroup
label={t("ID")}
name=""
placeholder=""
value={value.id}
error=""
onChange={onChange}
withCopy
onCopyClick={() => onCopyClick("id")}
/>
<InputGroup
label={t("Secret")}
name=""
placeholder=""
value={value.secret}
error=""
onChange={onChange}
withCopy
isPassword
buttonLabel={t("Reset")}
onButtonClick={onResetClick}
onCopyClick={() => onCopyClick("secret")}
/>
</StyledInputBlock>
</StyledBlock>
);
};
export default ClientBlock;

View File

@ -0,0 +1,133 @@
import React from "react";
import { InputBlock } from "@docspace/shared/components/input-block";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { FieldContainer } from "@docspace/shared/components/field-container";
import { RectangleSkeleton } from "@docspace/shared/skeletons/rectangle";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
import CopyReactSvgUrl from "PUBLIC_DIR/images/copy.react.svg?url";
import { StyledInputGroup } from "../ClientForm.styled";
interface InputGroupProps {
label: string;
name: string;
value: string;
placeholder: string;
error: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
helpButtonText?: string;
buttonLabel?: string;
onButtonClick?: () => void;
withCopy?: boolean;
onCopyClick?: (e: React.MouseEvent) => void;
isPassword?: boolean;
disabled?: boolean;
isRequired?: boolean;
isError?: boolean;
children?: React.ReactNode;
onBlur?: (name: string) => void;
}
const InputGroup = ({
label,
name,
value,
placeholder,
error,
onChange,
onBlur,
helpButtonText,
buttonLabel,
onButtonClick,
withCopy,
onCopyClick,
isPassword,
disabled,
isRequired,
isError,
children,
}: InputGroupProps) => {
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
const onButtonClickAction = async () => {
setIsRequestRunning(true);
onButtonClick?.();
setTimeout(() => {
setIsRequestRunning(false);
});
};
return (
<StyledInputGroup>
<FieldContainer
className={buttonLabel ? "input-block-with-button" : ""}
isVertical
isRequired={isRequired}
labelVisible
labelText={label}
tooltipContent={helpButtonText}
errorMessage={error}
removeMargin
hasError={isError}
>
{children || (
<>
{isRequestRunning ? (
<RectangleSkeleton
className="loader"
width="100%"
height="32px"
/>
) : (
<InputBlock
name={name}
value={value}
placeholder={placeholder}
onChange={onChange}
scale
tabIndex={0}
maxLength={255}
isReadOnly={withCopy}
isDisabled={disabled}
size={InputSize.base}
iconName={withCopy ? CopyReactSvgUrl : ""}
onIconClick={withCopy ? onCopyClick : undefined}
type={isPassword ? InputType.password : InputType.text}
onBlur={() => onBlur?.(name)}
hasError={isError}
/>
)}
{buttonLabel && (
<Button
label={buttonLabel}
size={ButtonSize.small}
onClick={onButtonClickAction}
isDisabled={isRequestRunning}
/>
)}
</>
)}
</FieldContainer>
</StyledInputGroup>
);
};
export default InputGroup;

View File

@ -0,0 +1,192 @@
import React from "react";
import { InputBlock } from "@docspace/shared/components/input-block";
import { Text } from "@docspace/shared/components/text";
import { SelectorAddButton } from "@docspace/shared/components/selector-add-button";
import { SelectedItem } from "@docspace/shared/components/selected-item";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
import { TTranslation } from "@docspace/shared/types";
import { IClientReqDTO } from "@docspace/shared/utils/oauth/types";
import ArrowIcon from "PUBLIC_DIR/images/arrow.right.react.svg";
import {
StyledChipsContainer,
StyledInputAddBlock,
StyledInputGroup,
StyledInputRow,
} from "../ClientForm.styled";
import { isValidUrl } from "../ClientForm.utils";
import InputGroup from "./InputGroup";
interface MultiInputGroupProps {
t: TTranslation;
label: string;
name: string;
placeholder: string;
currentValue: string[];
hasError?: boolean;
onAdd: (name: keyof IClientReqDTO, value: string, remove?: boolean) => void;
helpButtonText?: string;
isDisabled?: boolean;
}
const MultiInputGroup = ({
t,
label,
name,
placeholder,
currentValue,
onAdd,
hasError,
helpButtonText,
isDisabled,
}: MultiInputGroupProps) => {
const [value, setValue] = React.useState("");
const [isFocus, setIsFocus] = React.useState(false);
const [isAddVisible, setIsAddVisible] = React.useState(false);
const [isError, setIsError] = React.useState(false);
const addRef = React.useRef<null | HTMLDivElement>(null);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value;
setValue(v);
if (isValidUrl(v)) {
setIsAddVisible(true);
} else {
setIsAddVisible(false);
}
};
const onFocus = () => {
setIsFocus(true);
};
const onBlur = () => {
setIsFocus(false);
if (value) {
if (isValidUrl(value)) {
setIsError(false);
} else {
setIsError(true);
}
} else {
setIsError(false);
}
};
const onAddAction = React.useCallback(() => {
if (isDisabled || isError) return;
onAdd(name as keyof IClientReqDTO, value);
setIsAddVisible(false);
setIsError(false);
setValue("");
}, [isDisabled, isError, name, onAdd, value]);
React.useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" && isAddVisible) {
onAddAction();
}
};
if (isFocus) {
window.addEventListener("keydown", onKeyDown);
} else {
window.removeEventListener("keydown", onKeyDown);
}
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [isAddVisible, isFocus, onAddAction]);
React.useEffect(() => {
if (!addRef.current) return;
if (isAddVisible) {
addRef.current.style.display = "flex";
} else {
addRef.current.style.display = "none";
}
}, [isAddVisible]);
return (
<StyledInputGroup>
<InputGroup
label={label}
helpButtonText={helpButtonText}
name={name}
value={value}
placeholder={placeholder}
onChange={onChange}
error={
isError
? `${t("ErrorWrongURL")}: ${window.location.origin}`
: t("ThisRequiredField")
}
isRequired
isError={isError || hasError}
>
<StyledInputRow>
<InputBlock
name={name}
value={value}
placeholder={placeholder}
onChange={onChange}
scale
tabIndex={0}
maxLength={255}
isDisabled={isDisabled}
onFocus={onFocus}
onBlur={onBlur}
hasError={isError || hasError}
size={InputSize.base}
type={InputType.text}
/>
<StyledInputAddBlock ref={addRef} onClick={onAddAction}>
<Text fontSize="13px" fontWeight={600} lineHeight="20px" truncate>
{value}
</Text>
<div className="add-block">
<Text fontSize="13px" fontWeight={400} lineHeight="20px" truncate>
{t("Common:AddButton")}
</Text>
<ArrowIcon />
</div>
</StyledInputAddBlock>
<SelectorAddButton
onClick={onAddAction}
isDisabled={isDisabled || isError}
/>
</StyledInputRow>
</InputGroup>
<StyledChipsContainer>
{currentValue.map((v) => (
<SelectedItem
key={`${v}`}
propKey={v}
isInline
label={v}
isDisabled={isDisabled}
hideCross={isDisabled}
onClose={() => {
if (!isDisabled) onAdd(name as keyof IClientReqDTO, v, true);
}}
/>
))}
</StyledChipsContainer>
</StyledInputGroup>
);
};
export default MultiInputGroup;

View File

@ -0,0 +1,66 @@
import React from "react";
import { TTranslation } from "@docspace/shared/types";
import { IClientReqDTO } from "@docspace/shared/utils/oauth/types";
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
import BlockHeader from "./BlockHeader";
import MultiInputGroup from "./MultiInputGroup";
interface OAuthBlockProps {
t: TTranslation;
redirectUrisValue: string[];
allowedOriginsValue: string[];
changeValue: (
name: keyof IClientReqDTO,
value: string,
remove?: boolean,
) => void;
requiredErrorFields: string[];
isEdit: boolean;
}
const OAuthBlock = ({
t,
redirectUrisValue,
allowedOriginsValue,
changeValue,
requiredErrorFields,
isEdit,
}: OAuthBlockProps) => {
return (
<StyledBlock>
<BlockHeader header={t("OAuthHeaderBlock")} />
<StyledInputBlock>
<MultiInputGroup
t={t}
label={t("RedirectsURLS")}
placeholder={t("EnterURL")}
name="redirect_uris"
onAdd={changeValue}
currentValue={redirectUrisValue}
helpButtonText={t("RedirectsURLSHelpButton")}
isDisabled={isEdit}
hasError={requiredErrorFields.includes("redirect_uris")}
/>
<MultiInputGroup
t={t}
label={t("AllowedOrigins")}
placeholder={t("EnterURL")}
name="allowed_origins"
onAdd={changeValue}
currentValue={allowedOriginsValue}
helpButtonText={t("AllowedOriginsHelpButton")}
hasError={requiredErrorFields.includes("allowed_origins")}
/>
</StyledInputBlock>
</StyledBlock>
);
};
export default OAuthBlock;

View File

@ -0,0 +1,224 @@
import React from "react";
import {
IClientReqDTO,
TFilteredScopes,
TScope,
} from "@docspace/shared/utils/oauth/types";
import {
filterScopeByGroup,
getScopeTKeyName,
} from "@docspace/shared/utils/oauth";
import { ScopeGroup, ScopeType } from "@docspace/shared/enums";
import { TTranslation } from "@docspace/shared/types";
import { Text } from "@docspace/shared/components/text";
import { Checkbox } from "@docspace/shared/components/checkbox";
import BlockHeader from "./BlockHeader";
import {
StyledScopesCheckbox,
StyledScopesContainer,
StyledScopesName,
} from "../ClientForm.styled";
interface TScopesBlockProps {
scopes: TScope[];
selectedScopes: string[];
onAddScope: (name: keyof IClientReqDTO, scope: string) => void;
t: TTranslation;
isEdit: boolean;
}
const ScopesBlock = ({
scopes,
selectedScopes,
onAddScope,
t,
isEdit,
}: TScopesBlockProps) => {
const [checkedScopes, setCheckedScopes] = React.useState<string[]>([]);
const [filteredScopes, setFilteredScopes] = React.useState<TFilteredScopes>(
filterScopeByGroup(selectedScopes, scopes),
);
React.useEffect(() => {
const filtered = filterScopeByGroup(selectedScopes, scopes);
setCheckedScopes([...selectedScopes]);
setFilteredScopes({ ...filtered });
}, [scopes, selectedScopes]);
const onAddCheckedScope = (
group: ScopeGroup,
type: ScopeType,
name: string = "",
) => {
const isChecked = checkedScopes.includes(name);
if (!isChecked) {
setFilteredScopes((val) => {
val[group].isChecked = true;
val[group].checkedType = type;
return { ...val };
});
setCheckedScopes((val) => [...val, name]);
} else {
if (type === ScopeType.read) {
setFilteredScopes((val) => {
val[group].isChecked = false;
val[group].checkedType = undefined;
return { ...val };
});
} else {
setFilteredScopes((val) => {
const isReadChecked = checkedScopes.includes(
val[group].read?.name || "",
);
val[group].isChecked = isReadChecked;
val[group].checkedType = isReadChecked ? ScopeType.read : undefined;
return { ...val };
});
}
setCheckedScopes((val) => val.filter((v) => v !== name));
}
onAddScope("scopes", name);
};
const getRenderedScopeList = () => {
const list: React.ReactNode[] = [];
Object.entries(filteredScopes).forEach(([key, value]) => {
const name = getScopeTKeyName(key as ScopeGroup);
const isReadDisabled = value.checkedType === ScopeType.write;
const isReadChecked = value.isChecked;
const row = (
<React.Fragment key={name}>
<StyledScopesName>
<Text
className="scope-name"
fontSize="14px"
fontWeight={600}
lineHeight="16px"
>
{t(`Common:${name}`)}
</Text>
{value.read?.name && (
<Text
className="scope-desc"
fontSize="12px"
fontWeight={400}
lineHeight="16px"
>
<Text
className="scope-desc"
as="span"
fontSize="12px"
fontWeight={600}
lineHeight="16px"
>
{value.read?.name}
</Text>{" "}
{t(`Common:${value.read?.tKey}`)}
</Text>
)}
{value.write?.name && (
<Text
className="scope-desc"
fontSize="12px"
fontWeight={400}
lineHeight="16px"
>
<Text
className="scope-desc"
as="span"
fontSize="12px"
fontWeight={600}
lineHeight="16px"
>
{value.write?.name}
</Text>{" "}
{t(`Common:${value.write?.tKey}`)}
</Text>
)}
</StyledScopesName>
<StyledScopesCheckbox>
<Checkbox
className="checkbox-read"
isChecked={isReadChecked}
isDisabled={isReadDisabled || isEdit}
onChange={() =>
onAddCheckedScope(
key as ScopeGroup,
ScopeType.read,
value.read?.name,
)
}
/>
</StyledScopesCheckbox>
<StyledScopesCheckbox>
{value.write?.name && (
<Checkbox
isChecked={isReadDisabled}
isDisabled={isEdit || !value.write?.name}
onChange={() =>
onAddCheckedScope(
key as ScopeGroup,
ScopeType.write,
value.write?.name,
)
}
/>
)}
</StyledScopesCheckbox>
</React.Fragment>
);
list.push(row);
});
return list;
};
const list = getRenderedScopeList();
return (
<StyledScopesContainer>
<BlockHeader
className="header"
header={t("ScopesHeader")}
helpButtonText={t("ScopesHelp")}
/>
<Text
className="header"
fontSize="14px"
fontWeight={600}
lineHeight="22px"
>
{t("Read")}
</Text>
<Text
className="header header-last"
fontSize="14px"
fontWeight={600}
lineHeight="22px"
>
{t("Write")}
</Text>
{list.map((item) => item)}
</StyledScopesContainer>
);
};
export default ScopesBlock;

View File

@ -0,0 +1,110 @@
import React from "react";
import { Text } from "@docspace/shared/components/text";
import { SelectorAddButton } from "@docspace/shared/components/selector-add-button";
import { StyledInputGroup } from "../ClientForm.styled";
interface SelectGroupProps {
label: string;
selectLabel: string;
value: string;
description: string;
onSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const SelectGroup = ({
label,
selectLabel,
value,
description,
onSelect,
}: SelectGroupProps) => {
const inputRef = React.useRef<HTMLInputElement | null>(null);
const onClick = () => {
if (inputRef.current) {
inputRef.current.click();
}
};
const onInputClick = () => {
if (inputRef.current) {
inputRef.current.value = "";
inputRef.current.files = null;
}
};
return (
<StyledInputGroup>
<div className="label">
<Text
fontSize="13px"
fontWeight={600}
lineHeight="20px"
title=""
tag=""
as="p"
color=""
textAlign=""
>
{label} *
</Text>
</div>
<div className="select">
<img
className="client-logo"
style={{ display: value ? "block" : "none" }}
alt="img"
src={value}
/>
<SelectorAddButton onClick={onClick} />
<Text
fontSize="13px"
fontWeight={600}
lineHeight="20px"
title=""
tag=""
as="p"
color=""
textAlign=""
>
{selectLabel}
</Text>
</div>
<Text
fontSize="12px"
fontWeight={600}
lineHeight="16px"
title=""
tag=""
as="p"
color=""
textAlign=""
className="description"
>
{description}
</Text>
<input
ref={inputRef}
id="customFileInput"
className="custom-file-input"
multiple
type="file"
onChange={onSelect}
onClick={onInputClick}
style={{ display: "none" }}
accept="image/png, image/jpeg, svg"
/>
</StyledInputGroup>
);
};
export default SelectGroup;

View File

@ -0,0 +1,91 @@
import React from "react";
import { TTranslation } from "@docspace/shared/types";
import { IClientReqDTO } from "@docspace/shared/utils/oauth/types";
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
import BlockHeader from "./BlockHeader";
import InputGroup from "./InputGroup";
interface SupportBlockProps {
t: TTranslation;
policyUrlValue: string;
termsUrlValue: string;
changeValue: (name: keyof IClientReqDTO, value: string) => void;
isEdit: boolean;
errorFields: string[];
onBlur?: (name: string) => void;
requiredErrorFields: string[];
}
const SupportBlock = ({
t,
policyUrlValue,
termsUrlValue,
changeValue,
isEdit,
errorFields,
onBlur,
requiredErrorFields,
}: SupportBlockProps) => {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target;
changeValue(target.name as keyof IClientReqDTO, target.value);
};
const policyRequiredError = requiredErrorFields.includes("policy_url");
const termsRequiredError = requiredErrorFields.includes("terms_url");
const policyError = errorFields.includes("policy_url");
const termsError = errorFields.includes("terms_url");
return (
<StyledBlock>
<BlockHeader header={t("SupportAndLegalInfo")} />
<StyledInputBlock>
<InputGroup
label={t("PrivacyPolicyURL")}
name="policy_url"
placeholder={t("EnterURL")}
value={policyUrlValue}
error={
policyError
? `${t("ErrorWrongURL")}: ${window.location.origin}`
: t("ThisRequiredField")
}
onChange={onChange}
helpButtonText={t("PrivacyPolicyURLHelpButton")}
disabled={isEdit}
isRequired
isError={policyError || policyRequiredError}
onBlur={onBlur}
/>
<InputGroup
label={t("TermsOfServiceURL")}
name="terms_url"
placeholder={t("EnterURL")}
value={termsUrlValue}
error={
termsError
? `${t("ErrorWrongURL")}: ${window.location.origin}`
: t("ThisRequiredField")
}
onChange={onChange}
helpButtonText={t("TermsOfServiceURLHelpButton")}
disabled={isEdit}
isRequired
isError={termsError || termsRequiredError}
onBlur={onBlur}
/>
</StyledInputBlock>
</StyledBlock>
);
};
export default SupportBlock;

View File

@ -0,0 +1,56 @@
import React from "react";
import { Text } from "@docspace/shared/components/text";
import { Textarea } from "@docspace/shared/components/textarea";
import { StyledInputGroup } from "../ClientForm.styled";
interface TextAreaProps {
label: string;
name: string;
value: string;
placeholder: string;
increaseHeight: boolean;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
}
const TextAreaGroup = ({
label,
name,
value,
placeholder,
increaseHeight,
onChange,
}: TextAreaProps) => {
return (
<StyledInputGroup>
<div className="label">
<Text
fontSize="13px"
fontWeight={600}
lineHeight="20px"
title=""
tag=""
as="p"
color=""
textAlign=""
>
{label}
</Text>
</div>
<Textarea
name={name}
value={value}
placeholder={placeholder}
onChange={onChange}
tabIndex={0}
heightTextArea={increaseHeight ? 81 : 60}
maxLength={255}
/>
</StyledInputGroup>
);
};
export default TextAreaGroup;

View File

@ -0,0 +1,484 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
IClientProps,
IClientReqDTO,
} from "@docspace/shared/utils/oauth/types";
import { AuthenticationMethod } from "@docspace/shared/enums";
import { toastr } from "@docspace/shared/components/toast";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { getClient } from "@docspace/shared/api/oauth";
import ResetDialog from "../ResetDialog";
import BasicBlock from "./components/BasicBlock";
import ClientBlock from "./components/ClientBlock";
import SupportBlock from "./components/SupportBlock";
import OAuthBlock from "./components/OAuthBlock";
import ScopesBlock from "./components/ScopesBlock";
import ButtonsBlock from "./components/ButtonsBlock";
import { StyledContainer } from "./ClientForm.styled";
import { ClientFormProps, ClientStore } from "./ClientForm.types";
import { isValidUrl } from "./ClientForm.utils";
import ClientFormLoader from "./Loader";
const ClientForm = ({
id,
client,
scopeList,
fetchScopes,
saveClient,
updateClient,
resetDialogVisible,
setResetDialogVisible,
clientSecretProps,
setClientSecretProps,
currentDeviceType,
}: ClientFormProps) => {
const navigate = useNavigate();
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [isRequestRunning, setIsRequestRunning] =
React.useState<boolean>(false);
const [initialClient, setInitialClient] = React.useState<IClientProps>(
{} as IClientProps,
);
const [form, setForm] = React.useState<IClientReqDTO>({
name: "",
logo: "",
website_url: "",
description: "",
redirect_uris: [],
allowed_origins: [],
logout_redirect_uri: "",
terms_url: "",
policy_url: "",
is_public: true,
allow_pkce: false,
scopes: [],
});
const [errorFields, setErrorFields] = React.useState<string[]>([]);
const [requiredErrorFields, setRequiredErrorFields] = React.useState<
string[]
>([]);
const { t } = useTranslation(["OAuth", "Common"]);
const [clientId, setClientId] = React.useState<string>("");
const [clientSecret, setClientSecret] = React.useState<string>("");
const isEdit = !!id;
React.useEffect(() => {
if (clientSecretProps) {
setClientSecret(clientSecretProps);
setClientSecretProps?.("");
}
}, [clientSecretProps, setClientSecretProps]);
const onCancelClick = () => {
navigate("/portal-settings/developer-tools/oauth");
};
const onSaveClick = async () => {
try {
if (!id) {
let isValid = true;
Object.entries(form).forEach(([key, value]) => {
if (key === "description" || key === "logout_redirect_uri") return;
if (
(value === "" && typeof value === "string") ||
(value.length === 0 && value instanceof Array)
) {
if (!requiredErrorFields.includes(key))
setRequiredErrorFields((s) => [...s, key]);
isValid = false;
}
isValid = isValid && !errorFields.includes(key);
if (key === "website_url" && !isValidUrl(value)) {
isValid = false;
}
});
if (!isValid) return;
setIsRequestRunning(true);
await saveClient?.(form);
} else {
await updateClient?.(clientId, form);
}
onCancelClick();
} catch (e) {
toastr.error(e as unknown as TData);
}
};
const onResetClick = React.useCallback(async () => {
setResetDialogVisible?.(true);
}, [setResetDialogVisible]);
const onChangeForm = (
name: keyof IClientReqDTO,
value: string | boolean,
remove?: boolean,
) => {
setForm((val) => {
if (!(name in val)) return val;
const newVal: IClientReqDTO = { ...val };
let item = newVal[name];
if (typeof value === "string" && item instanceof Array) {
if (item.includes(value) && remove) {
item = item.filter((v: string) => v !== value);
} else if (!item.includes(value)) {
item.push(value);
}
} else {
item = value;
}
function updateForm<K extends keyof IClientReqDTO>(
key: K,
v: IClientReqDTO[K],
) {
newVal[key] = v;
}
updateForm(name, item);
return { ...newVal };
});
};
const getClientData = React.useCallback(async () => {
if (clientId) return;
const actions = [];
if (id && !client) {
actions.push(getClient(id));
}
if (scopeList?.length === 0) actions.push(fetchScopes?.());
try {
if (actions.length > 0) setIsLoading(true);
const [fetchedClient] = await Promise.all(actions);
const item = fetchedClient ?? client;
if (id && item) {
setForm({
name: item.name,
logo: item.logo,
website_url: item.websiteUrl,
description: item.description ?? "",
redirect_uris: item.redirectUris ? [...item.redirectUris] : [],
allowed_origins: item.allowedOrigins ? [...item.allowedOrigins] : [],
logout_redirect_uri: item.logoutRedirectUri ?? "",
terms_url: item.termsUrl ?? "",
policy_url: item.policyUrl ?? "",
allow_pkce: item.authenticationMethods
? item.authenticationMethods.includes(AuthenticationMethod.none)
: false,
is_public: item.isPublic ?? false,
scopes: item.scopes ? [...item.scopes] : [],
});
setClientId(item.clientId ?? " ");
setClientSecret(item.clientSecret ?? " ");
setInitialClient(item ?? ({} as IClientProps));
}
setTimeout(() => {
setIsLoading(false);
}, 500);
} catch (e) {
setIsLoading(false);
toastr.error(e as unknown as TData);
}
}, [clientId, id, client, scopeList?.length, fetchScopes]);
React.useEffect(() => {
getClientData();
}, [getClientData]);
const onBlur = (key: string) => {
if (
key === "name" &&
form[key] &&
!errorFields.includes(key) &&
(form[key].length < 3 || form[key].length > 256)
) {
setErrorFields((value) => {
return [...value, key];
});
} else if (
(key === "website_url" || key === "terms_url" || key === "policy_url") &&
form[key] &&
!errorFields.includes(key) &&
!isValidUrl(form[key])
) {
setErrorFields((value) => {
return [...value, key];
});
}
};
const compareAndValidate = () => {
let isValid = true;
if (isEdit) {
Object.entries(form).forEach(([key, value]) => {
switch (key) {
case "name":
isValid = isValid && !!value;
if (
value &&
!errorFields.includes(key) &&
(value.length < 3 || value.length > 256)
) {
isValid = false;
setErrorFields((val) => {
return [...val, key];
});
return;
}
if (
errorFields.includes(key) &&
(!value || (value.length > 2 && value.length < 257))
) {
setErrorFields((val) => {
return val.filter((n) => n !== key);
});
return;
}
isValid = isValid && !errorFields.includes(key);
break;
default:
break;
}
});
return (
(isValid &&
form.name &&
form.logo &&
form.allowed_origins.length > 0 &&
(form.name !== initialClient.name ||
form.logo !== initialClient.logo ||
form.description !== initialClient.description ||
form.allowed_origins.length !==
initialClient.allowedOrigins.length ||
form.allow_pkce !==
initialClient.authenticationMethods.includes(
AuthenticationMethod.none,
))) ||
form.is_public !== initialClient.isPublic
);
}
Object.entries(form).forEach(([key, value]) => {
switch (key) {
case "name":
case "logo":
case "terms_url":
case "policy_url":
case "website_url":
if (
errorFields.includes(key) &&
(!value || (value.length > 2 && value.length < 256))
) {
if (
(key === "website_url" && isValidUrl(value)) ||
key !== "website_url"
)
setErrorFields((val) => {
return val.filter((n) => n !== key);
});
}
if (requiredErrorFields.includes(key) && value !== "")
setRequiredErrorFields((val) => val.filter((v) => v !== key));
break;
case "redirect_uris":
case "allowed_origins":
case "scopes":
if (requiredErrorFields.includes(key) && value.length > 0)
setRequiredErrorFields((val) => val.filter((v) => v !== key));
break;
default:
break;
}
});
return isValid;
};
const isValid = compareAndValidate();
return (
<>
<StyledContainer>
{isLoading ? (
<ClientFormLoader
isEdit={isEdit}
currentDeviceType={currentDeviceType}
/>
) : (
<>
<BasicBlock
t={t}
nameValue={form.name}
websiteUrlValue={form.website_url}
descriptionValue={form.description}
logoValue={form.logo}
allowPkce={form.allow_pkce}
// isPublic={form.is_public}
changeValue={onChangeForm}
isEdit={isEdit}
errorFields={errorFields}
requiredErrorFields={requiredErrorFields}
onBlur={onBlur}
/>
{isEdit && (
<ClientBlock
t={t}
idValue={clientId}
secretValue={clientSecret}
onResetClick={onResetClick}
/>
)}
<OAuthBlock
t={t}
redirectUrisValue={form.redirect_uris}
allowedOriginsValue={form.allowed_origins}
changeValue={onChangeForm}
isEdit={isEdit}
requiredErrorFields={requiredErrorFields}
/>
<ScopesBlock
t={t}
scopes={scopeList || []}
selectedScopes={form.scopes}
onAddScope={onChangeForm}
isEdit={isEdit}
/>
<SupportBlock
t={t}
policyUrlValue={form.policy_url}
termsUrlValue={form.terms_url}
changeValue={onChangeForm}
isEdit={isEdit}
errorFields={errorFields}
requiredErrorFields={requiredErrorFields}
onBlur={onBlur}
/>
<ButtonsBlock
saveLabel={t("Common:SaveButton")}
cancelLabel={t("Common:CancelButton")}
onSaveClick={onSaveClick}
onCancelClick={onCancelClick}
isRequestRunning={isRequestRunning}
saveButtonDisabled={isEdit ? !isValid : false}
cancelButtonDisabled={isRequestRunning}
currentDeviceType={currentDeviceType || ""}
/>
</>
)}
</StyledContainer>
{resetDialogVisible && <ResetDialog />}
</>
);
};
export default inject(
({ oauthStore, settingsStore }: ClientStore, { id }: ClientFormProps) => {
const {
clientList,
scopeList,
fetchScopes,
saveClient,
updateClient,
setResetDialogVisible,
resetDialogVisible,
setClientSecret,
clientSecret,
} = oauthStore;
const { currentDeviceType } = settingsStore;
const props: ClientFormProps = {
scopeList,
fetchScopes,
saveClient,
updateClient,
setResetDialogVisible,
currentDeviceType,
resetDialogVisible,
setClientSecretProps: setClientSecret,
clientSecretProps: clientSecret,
};
if (id) {
const client = clientList.find((c: IClientProps) => c.clientId === id);
props.client = client;
}
return { ...props };
},
)(observer(ClientForm));

View File

@ -0,0 +1,98 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { useTranslation, Trans } from "react-i18next";
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { toastr } from "@docspace/shared/components/toast";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
interface DeleteClientDialogProps {
isVisible?: boolean;
onClose?: () => void;
onDisable?: () => Promise<void>;
}
const DeleteClientDialog = (props: DeleteClientDialogProps) => {
const { t, ready } = useTranslation(["OAuth", "Common"]);
const { isVisible, onClose, onDisable } = props;
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
const onDisableClick = async () => {
try {
setIsRequestRunning(true);
await onDisable?.();
setIsRequestRunning(true);
onClose?.();
} catch (error: unknown) {
const e = error as TData;
toastr.error(e);
onClose?.();
}
};
return (
<ModalDialog
isLoading={!ready}
visible={isVisible}
onClose={onClose}
displayType={ModalDialogType.modal}
>
<ModalDialog.Header>{t("DeleteHeader")}</ModalDialog.Header>
<ModalDialog.Body>
<Trans t={t} i18nKey="DeleteDescription" ns="OAuth" />
</ModalDialog.Body>
<ModalDialog.Footer>
<Button
className="delete-button"
key="DeletePortalBtn"
label={t("Common:OkButton")}
size={ButtonSize.normal}
scale
primary
isLoading={isRequestRunning}
onClick={onDisableClick}
/>
<Button
className="cancel-button"
key="CancelDeleteBtn"
label={t("Common:CancelButton")}
size={ButtonSize.normal}
scale
isDisabled={isRequestRunning}
onClick={onClose}
/>
</ModalDialog.Footer>
</ModalDialog>
);
};
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
const {
bufferSelection,
setDeleteDialogVisible,
setActiveClient,
setSelection,
deleteClient,
deleteDialogVisible,
} = oauthStore;
const onClose = () => {
setDeleteDialogVisible(false);
};
const onDisable = async () => {
if (!bufferSelection) return;
setActiveClient(bufferSelection.clientId);
await deleteClient([bufferSelection.clientId]);
setActiveClient("");
setSelection("");
};
return { isVisible: deleteDialogVisible, onClose, onDisable };
})(observer(DeleteClientDialog));

View File

@ -0,0 +1,99 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { useTranslation, Trans } from "react-i18next";
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { toastr } from "@docspace/shared/components/toast";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
interface DisableClientDialogProps {
isVisible?: boolean;
onClose?: () => void;
onDisable?: () => Promise<void>;
}
const DisableClientDialog = (props: DisableClientDialogProps) => {
const { t, ready } = useTranslation(["OAuth", "Common"]);
const { isVisible, onClose, onDisable } = props;
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
const onDisableClick = async () => {
try {
setIsRequestRunning(true);
await onDisable?.();
setIsRequestRunning(true);
onClose?.();
} catch (error: unknown) {
const e = error as TData;
toastr.error(e);
onClose?.();
}
};
return (
<ModalDialog
isLoading={!ready}
visible={isVisible}
onClose={onClose}
displayType={ModalDialogType.modal}
>
<ModalDialog.Header>{t("DisableApplication")}</ModalDialog.Header>
<ModalDialog.Body>
<Trans t={t} i18nKey="DisableApplicationDescription" ns="OAuth" />
</ModalDialog.Body>
<ModalDialog.Footer>
<Button
className="delete-button"
key="DeletePortalBtn"
label={t("Common:OkButton")}
size={ButtonSize.normal}
scale
primary
isLoading={isRequestRunning}
onClick={onDisableClick}
/>
<Button
className="cancel-button"
key="CancelDeleteBtn"
label={t("Common:CancelButton")}
size={ButtonSize.normal}
scale
isDisabled={isRequestRunning}
onClick={onClose}
/>
</ModalDialog.Footer>
</ModalDialog>
);
};
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
const {
bufferSelection,
setDisableDialogVisible,
setActiveClient,
setSelection,
changeClientStatus,
disableDialogVisible,
} = oauthStore;
const onClose = () => {
setDisableDialogVisible(false);
};
const onDisable = async () => {
if (!bufferSelection) return;
setActiveClient(bufferSelection.clientId);
await changeClientStatus(bufferSelection.clientId, false);
setActiveClient("");
setSelection("");
};
return { isVisible: disableDialogVisible, onClose, onDisable };
})(observer(DisableClientDialog));

View File

@ -0,0 +1,23 @@
import { useTranslation } from "react-i18next";
import { EmptyScreenContainer } from "@docspace/shared/components/empty-screen-container";
import EmptyScreenOauthSvgUrl from "PUBLIC_DIR/images/empty_screen_oauth.svg?url";
import RegisterNewButton from "../RegisterNewButton";
const OAuthEmptyScreen = () => {
const { t } = useTranslation(["OAuth"]);
return (
<EmptyScreenContainer
imageSrc={EmptyScreenOauthSvgUrl}
imageAlt="Empty oauth list"
headerText={t("NoOAuthAppHeader")}
subheadingText={t("OAuthAppDescription")}
buttons={<RegisterNewButton />}
/>
);
};
export default OAuthEmptyScreen;

View File

@ -0,0 +1,443 @@
import React from "react";
import { inject, observer } from "mobx-react";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { IClientProps, TScope } from "@docspace/shared/utils/oauth/types";
import ScopeList from "@docspace/shared/utils/oauth/ScopeList";
import getCorrectDate from "@docspace/shared/utils/getCorrectDate";
import { getCookie } from "@docspace/shared/utils/cookie";
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
import { Text } from "@docspace/shared/components/text";
import {
ContextMenuButton,
ContextMenuButtonDisplayType,
} from "@docspace/shared/components/context-menu-button";
import {
Avatar,
AvatarRole,
AvatarSize,
} from "@docspace/shared/components/avatar";
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
import { Base } from "@docspace/shared/themes";
import { TTranslation } from "@docspace/shared/types";
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
const StyledContainer = styled.div<{
showDescription: boolean;
withShowText: boolean;
}>`
width: 100%;
height: 100%;
box-sizing: border-box;
padding-top: 8px;
display: flex;
flex-direction: column;
.client-block {
width: 100%;
height: 32px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.client-block__info {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
.client-block__info-logo {
width: 32px;
height: 32px;
max-width: 32px;
max-height: 32px;
border-radius: 3px;
}
}
}
.description {
max-height: ${(props) =>
props.showDescription ? "100%" : props.withShowText ? "100px" : "100%"};
overflow: hidden;
margin-bottom: ${(props) => (props.withShowText ? "4px" : 0)};
}
.desc-link {
color: ${(props) => props.theme.oauth.infoDialog.descLinkColor};
}
.block-header {
margin-top: 20px;
margin-bottom: 12px;
color: ${(props) => props.theme.oauth.infoDialog.blockHeaderColor};
}
.creator-block {
margin: 8px 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.privacy-block {
display: flex;
.separator {
display: inline-block;
margin-top: 2px;
height: 16px;
width: 1px;
margin: 0 8px;
background: ${(props) => props.theme.oauth.infoDialog.separatorColor};
}
}
`;
StyledContainer.defaultProps = { theme: Base };
interface InfoDialogProps {
visible: boolean;
scopeList?: TScope[];
setInfoDialogVisible?: (value: boolean) => void;
getContextMenuItems?: (
t: TTranslation,
item: IClientProps,
isInfo?: boolean,
isSettings?: boolean,
) => ContextMenuModel[];
client?: IClientProps;
isProfile?: boolean;
}
const InfoDialog = ({
visible,
client,
scopeList,
setInfoDialogVisible,
getContextMenuItems,
isProfile,
}: InfoDialogProps) => {
const { t } = useTranslation(["OAuth", "Common"]);
const [showDescription, setShowDescription] = React.useState(false);
const [isRender, setIsRender] = React.useState(false);
const [withShowText, setWithShowText] = React.useState(false);
React.useEffect(() => {
setIsRender(true);
}, []);
React.useEffect(() => {
const el = document.getElementById("client-info-description-text");
if (!el) return;
setWithShowText(el?.offsetHeight >= 100);
}, [isRender]);
const getContextOptions = () => {
const contextOptions =
client && getContextMenuItems
? getContextMenuItems(t, client, true, !isProfile)
: [];
return contextOptions;
};
const onClose = () => {
setInfoDialogVisible?.(false);
};
const locale = getCookie("asc_language");
const modifiedDate = getCorrectDate(locale || "", client?.modifiedOn || "");
return (
<ModalDialog
visible={visible}
displayType={ModalDialogType.aside}
onClose={onClose}
>
<ModalDialog.Header>{t("Common:Info")}</ModalDialog.Header>
<ModalDialog.Body>
<StyledContainer
showDescription={showDescription}
withShowText={withShowText}
>
<div className="client-block">
<div className="client-block__info">
<img
className="client-block__info-logo"
alt="client-logo"
src={client?.logo}
/>
<Text
fontSize="16px"
lineHeight="22px"
fontWeight="700"
noSelect
truncate
>
{client?.name}
</Text>
</div>
<ContextMenuButton
displayType={ContextMenuButtonDisplayType.dropdown}
getData={getContextOptions}
/>
</div>
{!isProfile && (
<>
<Text
className="block-header"
fontSize="14px"
lineHeight="16px"
fontWeight="600"
noSelect
truncate
>
{t("Creator")}
</Text>
<div className="creator-block">
<Avatar
source={client?.creatorAvatar || ""}
size={AvatarSize.min}
role={AvatarRole.user}
/>
<Text
fontSize="14px"
lineHeight="16px"
fontWeight="600"
noSelect
truncate
>
{client?.creatorDisplayName}
</Text>
</div>
</>
)}
{!isProfile && (
<>
<Text
className="block-header"
fontSize="14px"
lineHeight="16px"
fontWeight="600"
noSelect
truncate
>
{t("Common:Description")}
</Text>
<Text
id="client-info-description-text"
className="description"
fontSize="13px"
lineHeight="20px"
fontWeight="400"
noSelect
>
{client?.description}
</Text>
{withShowText && (
<Link
className="desc-link"
fontSize="13px"
lineHeight="15px"
fontWeight="600"
isHovered
onClick={() => setShowDescription((val) => !val)}
type={LinkType.action}
>
{showDescription ? "Hide" : "Show more"}
</Link>
)}
</>
)}
<Text
className="block-header"
fontSize="14px"
lineHeight="16px"
fontWeight="600"
noSelect
truncate
>
{t("Common:Website")}
</Text>
<Link
fontSize="13px"
lineHeight="15px"
fontWeight="600"
isHovered
href={client?.websiteUrl}
type={LinkType.action}
target={LinkTarget.blank}
>
{client?.websiteUrl}
</Link>
<Text
className="block-header"
fontSize="14px"
lineHeight="16px"
fontWeight="600"
noSelect
truncate
>
{t("Access")}
</Text>
<ScopeList
selectedScopes={client?.scopes || []}
scopes={scopeList || []}
t={t}
/>
{isProfile && (
<>
<Text
className="block-header"
fontSize="14px"
lineHeight="16px"
fontWeight="600"
noSelect
truncate
>
{t("AccessGranted")}
</Text>
<Text
fontSize="13px"
lineHeight="20px"
fontWeight="600"
noSelect
truncate
>
{modifiedDate}
</Text>
</>
)}
<Text
className="block-header"
fontSize="14px"
lineHeight="20px"
fontWeight="600"
noSelect
truncate
>
{t("SupportAndLegalInfo")}
</Text>
<Text
className="privacy-block"
fontSize="13px"
lineHeight="15px"
fontWeight="600"
noSelect
truncate
>
<Link
fontSize="13px"
lineHeight="15px"
fontWeight="600"
isHovered
href={client?.policyUrl}
type={LinkType.action}
target={LinkTarget.blank}
>
{t("PrivacyPolicy")}
</Link>
<span className="separator" />
<Link
fontSize="13px"
lineHeight="15px"
fontWeight="600"
isHovered
href={client?.termsUrl}
type={LinkType.action}
target={LinkTarget.blank}
>
{t("Terms of Service")}
</Link>
</Text>
{!isProfile && (
<>
<Text
className="block-header"
fontSize="14px"
lineHeight="16px"
fontWeight="600"
noSelect
truncate
>
{t("LastModified")}
</Text>
<Text
fontSize="13px"
lineHeight="20px"
fontWeight="600"
noSelect
truncate
>
{modifiedDate}
</Text>
</>
)}
</StyledContainer>
</ModalDialog.Body>
</ModalDialog>
);
};
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
const {
setInfoDialogVisible,
bufferSelection,
scopeList,
getContextMenuItems,
} = oauthStore;
return {
setInfoDialogVisible,
client: bufferSelection,
scopeList,
getContextMenuItems,
};
})(observer(InfoDialog));

View File

@ -0,0 +1,41 @@
import React from "react";
import { RectangleSkeleton } from "@docspace/shared/skeletons/rectangle";
import { TableSkeleton } from "@docspace/shared/skeletons/table";
import { RowsSkeleton } from "@docspace/shared/skeletons/rows";
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
import { OAuthContainer } from "../../StyledOAuth";
import { StyledContainer } from ".";
const OAuthLoader = ({
viewAs,
currentDeviceType,
}: {
viewAs: ViewAsType;
currentDeviceType: DeviceUnionType;
}) => {
const buttonHeight = currentDeviceType !== "desktop" ? "40px" : "32px";
return (
<OAuthContainer>
<StyledContainer>
<RectangleSkeleton className="description" width="100%" height="16px" />
<RectangleSkeleton
className="add-button"
width="220px"
height={buttonHeight}
/>
{viewAs === "table" ? (
<TableSkeleton style={{}} />
) : (
<RowsSkeleton style={{}} />
)}
</StyledContainer>
</OAuthContainer>
);
};
export default OAuthLoader;

View File

@ -0,0 +1,83 @@
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Row } from "@docspace/shared/components/row";
import { RowContent } from "./RowContent";
import { RowProps } from "./RowView.types";
export const OAuthRow = (props: RowProps) => {
const {
item,
sectionWidth,
changeClientStatus,
isChecked,
inProgress,
getContextMenuItems,
setSelection,
} = props;
const navigate = useNavigate();
const { t } = useTranslation(["OAuth", "Common", "Files"]);
const editClient = () => {
navigate(`${item.clientId}`);
};
const handleToggleEnabled = async () => {
if (!changeClientStatus) return;
await changeClientStatus(item.clientId, !item.enabled);
};
const handleRowClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
if (
target.closest(".checkbox") ||
target.closest(".table-container_row-checkbox") ||
e.detail === 0
) {
return;
}
if (
target.closest(".table-container_row-context-menu-wrapper") ||
target.closest(".toggleButton") ||
target.closest(".row_context-menu-wrapper")
) {
return setSelection && setSelection("");
}
editClient();
};
const contextOptions = getContextMenuItems && getContextMenuItems(t, item);
const element = (
<img style={{ borderRadius: "3px" }} src={item.logo} alt="App logo" />
);
return (
<Row
key={item.clientId}
contextOptions={contextOptions}
onRowClick={handleRowClick}
element={element}
mode="modern"
checked={isChecked}
inProgress={inProgress}
onSelect={() => setSelection && setSelection(item.clientId)}
className={`oauth2-row${isChecked ? " oauth2-row-selected" : ""}`}
>
<RowContent
sectionWidth={sectionWidth}
item={item}
isChecked={isChecked}
inProgress={inProgress}
setSelection={setSelection}
handleToggleEnabled={handleToggleEnabled}
/>
</Row>
);
};
export default OAuthRow;

View File

@ -0,0 +1,46 @@
import { Text } from "@docspace/shared/components/text";
import { ToggleButton } from "@docspace/shared/components/toggle-button";
import {
StyledRowContent,
ContentWrapper,
FlexWrapper,
ToggleButtonWrapper,
} from "./RowView.styled";
import { RowContentProps } from "./RowView.types";
export const RowContent = ({
sectionWidth,
item,
handleToggleEnabled,
}: RowContentProps) => {
return (
<StyledRowContent sectionWidth={sectionWidth}>
<ContentWrapper>
<FlexWrapper>
<Text
fontWeight={600}
fontSize="14px"
style={{ marginInlineEnd: "8px" }}
>
{item.name}
</Text>
</FlexWrapper>
<Text fontWeight={600} fontSize="12px" color="#A3A9AE">
{item.description}
</Text>
</ContentWrapper>
<ToggleButtonWrapper>
<ToggleButton
className="toggle toggleButton"
id="toggle id"
isChecked={item.enabled}
onChange={handleToggleEnabled}
/>
</ToggleButtonWrapper>
</StyledRowContent>
);
};

View File

@ -0,0 +1,111 @@
import styled from "styled-components";
import { RowContainer } from "@docspace/shared/components/row-container";
import { RowContent } from "@docspace/shared/components/row-content";
import { tablet } from "@docspace/shared/utils/device";
export const StyledRowContainer = styled(RowContainer)`
margin-top: 0px;
.row-list-item {
padding-left: 21px;
}
.row-loader {
width: calc(100% - 46px) !important;
padding-left: 21px;
}
img {
width: 32px;
max-width: 32px;
height: 32px;
max-height: 32px;
}
.oauth2-row-selected {
background: ${(props) =>
props.theme.filesSection.rowView.checkedBackground};
cursor: pointer;
border-bottom: none;
margin-left: -24px;
margin-right: -24px;
padding-left: 24px;
padding-right: 24px;
@media ${tablet} {
margin-left: -16px;
margin-right: -16px;
padding-left: 16px;
padding-right: 16px;
}
}
.oauth2-row {
margin-top: -3px;
padding-top: 3px;
:hover {
background: ${(props) =>
props.theme.filesSection.rowView.checkedBackground};
cursor: pointer;
border-bottom: none;
margin-left: -24px;
margin-right: -24px;
padding-left: 24px;
padding-right: 24px;
@media ${tablet} {
margin-left: -16px;
margin-right: -16px;
padding-left: 16px;
padding-right: 16px;
}
}
}
`;
export const StyledRowContent = styled(RowContent)`
display: flex;
padding-bottom: 10px;
.rowMainContainer {
height: 100%;
width: 100%;
}
.mainIcons {
min-width: 76px;
}
`;
export const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
justify-items: center;
`;
export const ToggleButtonWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
label {
margin-top: 1px;
position: relative;
gap: 0px;
margin-right: -8px;
}
`;
export const FlexWrapper = styled.div`
display: flex;
`;

View File

@ -0,0 +1,46 @@
import { TTranslation } from "@docspace/shared/types";
import { IClientProps } from "@docspace/shared/utils/oauth/types";
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
export interface RowViewProps {
items: IClientProps[];
sectionWidth: number;
viewAs?: ViewAsType;
setViewAs?: (value: ViewAsType) => void;
selection?: string[];
setSelection?: (clientId: string) => void;
getContextMenuItems?: (
t: TTranslation,
item: IClientProps,
) => ContextMenuModel[];
activeClients?: string[];
hasNextPage?: boolean;
itemCount?: number;
fetchNextClients?: (startIndex: number) => Promise<void>;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
export interface RowProps {
item: IClientProps;
isChecked: boolean;
inProgress: boolean;
sectionWidth: number;
getContextMenuItems?: (
t: TTranslation,
item: IClientProps,
) => ContextMenuModel[];
setSelection?: (clientId: string) => void;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
export interface RowContentProps {
sectionWidth: number;
item: IClientProps;
isChecked: boolean;
inProgress: boolean;
handleToggleEnabled: () => void;
setSelection?: (clientId: string) => void;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}

View File

@ -0,0 +1,86 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import { OAuthRow } from "./Row";
import { RowViewProps } from "./RowView.types";
import { StyledRowContainer } from "./RowView.styled";
const RowView = (props: RowViewProps) => {
const {
items,
sectionWidth,
changeClientStatus,
selection,
setSelection,
activeClients,
getContextMenuItems,
hasNextPage,
itemCount,
fetchNextClients,
} = props;
const fetchMoreFiles = React.useCallback(
async ({ startIndex }: { startIndex: number; stopIndex: number }) => {
await fetchNextClients?.(startIndex);
},
[fetchNextClients],
);
return (
<StyledRowContainer
itemHeight={59}
filesLength={items.length}
fetchMoreFiles={fetchMoreFiles}
hasMoreFiles={hasNextPage || false}
itemCount={itemCount || 0}
useReactWindow
onScroll={() => {}}
>
{items.map((item) => (
<OAuthRow
key={item.clientId}
item={item}
isChecked={selection?.includes(item.clientId) || false}
inProgress={activeClients?.includes(item.clientId) || false}
setSelection={setSelection}
changeClientStatus={changeClientStatus}
getContextMenuItems={getContextMenuItems}
sectionWidth={sectionWidth}
/>
))}
</StyledRowContainer>
);
};
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
const {
viewAs,
setViewAs,
selection,
setSelection,
changeClientStatus,
getContextMenuItems,
activeClients,
hasNextPage,
itemCount,
fetchNextClients,
} = oauthStore;
return {
viewAs,
setViewAs,
changeClientStatus,
selection,
setSelection,
activeClients,
getContextMenuItems,
hasNextPage,
itemCount,
fetchNextClients,
};
})(observer(RowView));

View File

@ -0,0 +1,67 @@
import { useTranslation } from "react-i18next";
import { TTableColumn, TableHeader } from "@docspace/shared/components/table";
import { HeaderProps } from "./TableView.types";
const Header = (props: HeaderProps) => {
const { sectionWidth, tableRef, columnStorageName, tagRef } = props;
const { t } = useTranslation(["Oauth", "Files", "Webhooks", "Common"]);
const defaultColumns: TTableColumn[] = [
{
key: "Name",
title: t("Common:Name"),
resizable: true,
enable: true,
default: true,
active: false,
minWidth: 210,
},
{
key: "Creator",
title: t("Creator"),
resizable: true,
enable: true,
minWidth: 150,
},
{
key: "Modified",
title: t("Files:ByLastModified"),
resizable: true,
enable: true,
minWidth: 150,
},
{
key: "Scopes",
title: t("Scopes"),
resizable: true,
enable: true,
withTagRef: true,
minWidth: 150,
},
{
key: "State",
title: t("Webhooks:State"),
enable: true,
resizable: false,
defaultSize: 64,
},
];
return (
<TableHeader
containerRef={{ current: tableRef }}
columns={defaultColumns}
columnStorageName={columnStorageName}
tableStorageName={columnStorageName}
sectionWidth={sectionWidth}
showSettings={false}
useReactWindow
infoPanelVisible={false}
tagRef={tagRef}
/>
);
};
export default Header;

View File

@ -0,0 +1,131 @@
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { TableCell } from "@docspace/shared/components/table";
import { Tags } from "@docspace/shared/components/tags";
import { Text } from "@docspace/shared/components/text";
import { ToggleButton } from "@docspace/shared/components/toggle-button";
import getCorrectDate from "@docspace/shared/utils/getCorrectDate";
import { getCookie } from "@docspace/shared/utils/cookie";
import NameCell from "./columns/name";
import CreatorCell from "./columns/creator";
import { StyledRowWrapper, StyledTableRow } from "./TableView.styled";
import { RowProps } from "./TableView.types";
const Row = (props: RowProps) => {
const {
item,
changeClientStatus,
isChecked,
inProgress,
getContextMenuItems,
setSelection,
tagCount,
} = props;
const navigate = useNavigate();
const { t } = useTranslation(["OAuth", "Common", "Files"]);
const editClient = () => {
navigate(`${item.clientId}`);
};
const handleToggleEnabled = async () => {
if (!changeClientStatus) return;
await changeClientStatus(item.clientId, !item.enabled);
};
const handleRowClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
if (
target.closest(".checkbox") ||
target.closest(".table-container_row-checkbox") ||
target.closest(".advanced-tag") ||
target.closest(".tag") ||
e.detail === 0
) {
return;
}
if (
target.closest(".type-combobox") ||
target.closest(".table-container_row-context-menu-wrapper") ||
target.closest(".toggleButton")
) {
return setSelection && setSelection("");
}
editClient();
};
const contextOptions = getContextMenuItems?.(t, item);
const getContextMenuModel = () =>
getContextMenuItems ? getContextMenuItems(t, item) : [];
const locale = getCookie("asc_language");
const modifiedDate = getCorrectDate(locale || "", item.modifiedOn || "");
return (
<StyledRowWrapper className="handle">
<StyledTableRow
contextOptions={contextOptions || []}
onClick={handleRowClick}
getContextModel={getContextMenuModel}
>
<TableCell className="table-container_file-name-cell">
<NameCell
name={item.name}
icon={item.logo}
isChecked={isChecked}
inProgress={inProgress}
clientId={item.clientId}
setSelection={setSelection}
/>
</TableCell>
<TableCell className="author-cell">
<CreatorCell
avatar={item.creatorAvatar || ""}
displayName={item.creatorDisplayName || ""}
/>
</TableCell>
<TableCell className="">
<Text
as="span"
fontWeight={400}
className="mr-8 textOverflow description-text"
>
{modifiedDate}
</Text>
</TableCell>
<TableCell className="">
<Text
as="span"
fontWeight={400}
className="mr-8 textOverflow description-text"
>
<Tags
tags={item.scopes}
removeTagIcon
columnCount={tagCount}
onSelectTag={() => {}}
/>
</Text>
</TableCell>
<TableCell className="">
<ToggleButton
className="toggle toggleButton"
isChecked={item.enabled}
onChange={handleToggleEnabled}
/>
</TableCell>
</StyledTableRow>
</StyledRowWrapper>
);
};
export default Row;

View File

@ -0,0 +1,99 @@
import styled, { css } from "styled-components";
import { TableRow, TableContainer } from "@docspace/shared/components/table";
import { Base } from "@docspace/shared/themes";
export const TableWrapper = styled(TableContainer)`
margin-top: 0px;
.header-container-text {
font-size: 12px;
}
.table-container_header {
position: absolute;
}
`;
const StyledRowWrapper = styled.div`
display: contents;
`;
const StyledTableRow = styled(TableRow)`
.table-container_cell {
text-overflow: ellipsis;
padding-inline-end: 8px;
}
.mr-8 {
margin-inline-end: 8px;
}
.textOverflow {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.description-text {
color: ${(props) => props.theme.oauth.list.descriptionColor};
}
.toggleButton {
display: contents;
input {
position: relative;
margin-inline-start: -8px;
}
}
.table-container_row-loader {
margin-left: 8px;
margin-right: 16px;
}
:hover {
.table-container_cell {
cursor: pointer;
background: ${(props) =>
`${props.theme.filesSection.tableView.row.backgroundActive} !important`};
margin-top: -1px;
border-top: ${(props) =>
`1px solid ${props.theme.filesSection.tableView.row.borderColor}`};
}
.table-container_file-name-cell {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-right: -24px;
padding-right: 24px;
`
: css`
margin-left: -24px;
padding-left: 24px;
`}
}
.table-container_row-context-menu-wrapper {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-left: -20px;
padding-left: 18px;
`
: css`
margin-right: -20px;
padding-right: 18px;
`}
}
}
`;
StyledTableRow.defaultProps = { theme: Base };
export { StyledRowWrapper, StyledTableRow };

View File

@ -0,0 +1,42 @@
import { IClientProps } from "@docspace/shared/utils/oauth/types";
import { TTranslation } from "@docspace/shared/types";
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
export interface TableViewProps {
items: IClientProps[];
sectionWidth: number;
userId?: string;
selection?: string[];
setSelection?: (clientId: string) => void;
getContextMenuItems?: (
t: TTranslation,
item: IClientProps,
) => ContextMenuModel[];
bufferSelection?: IClientProps | null;
activeClients?: string[];
hasNextPage?: boolean;
itemCount?: number;
fetchNextClients?: (startIndex: number) => Promise<void>;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
export interface HeaderProps {
sectionWidth: number;
tableRef: HTMLDivElement | null;
columnStorageName: string;
tagRef: (node: HTMLDivElement) => void;
}
export interface RowProps {
item: IClientProps;
isChecked: boolean;
inProgress: boolean;
tagCount: number;
getContextMenuItems?: (
t: TTranslation,
item: IClientProps,
) => ContextMenuModel[];
setSelection?: (clientId: string) => void;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}

View File

@ -0,0 +1,45 @@
import styled from "styled-components";
import { Text } from "@docspace/shared/components/text";
import {
Avatar,
AvatarRole,
AvatarSize,
} from "@docspace/shared/components/avatar";
const StyledAvatar = styled(Avatar)`
width: 16px;
margin-inline-end: 4px;
max-width: 100%;
height: 16px;
min-width: unset;
`;
interface CreatorCellProps {
avatar: string;
displayName: string;
}
const CreatorCell = ({ avatar, displayName }: CreatorCellProps) => {
return (
<>
<StyledAvatar
source={avatar}
size={AvatarSize.min}
role={AvatarRole.user}
className="textOverflow"
/>
<Text
className="description-text textOverflow"
fontWeight="600"
fontSize="13px"
>
{displayName}
</Text>
</>
);
};
export default CreatorCell;

View File

@ -0,0 +1,88 @@
import styled, { css } from "styled-components";
import { Text } from "@docspace/shared/components/text";
import { Checkbox } from "@docspace/shared/components/checkbox";
import { TableCell } from "@docspace/shared/components/table";
import { Loader, LoaderTypes } from "@docspace/shared/components/loader";
const StyledContainer = styled.div`
.table-container_row-checkbox {
margin-inline-start: -8px;
width: 16px;
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
padding: 16px 16px 16px 8px;
`
: css`
padding: 16px 8px 16px 16px;
`}
}
`;
const StyledImage = styled.img`
width: 32px;
height: 32px;
border-radius: 3px;
`;
interface NameCellProps {
name: string;
clientId: string;
icon?: string;
inProgress?: boolean;
isChecked?: boolean;
setSelection?: (clientId: string) => void;
}
const NameCell = ({
name,
icon,
clientId,
inProgress,
isChecked,
setSelection,
}: NameCellProps) => {
const onChange = () => {
setSelection?.(clientId);
};
return (
<>
{inProgress ? (
<Loader
className="table-container_row-loader"
type={LoaderTypes.oval}
size="16px"
/>
) : (
<TableCell
className="table-container_element-wrapper"
hasAccess
checked={isChecked}
>
<StyledContainer className="table-container_element-container">
<div className="table-container_element">
{icon && <StyledImage src={icon} alt="App icon" />}
</div>
<Checkbox
className="table-container_row-checkbox"
onChange={onChange}
isChecked={isChecked}
title={name}
/>
</StyledContainer>
</TableCell>
)}
<Text title={name} fontWeight="600" fontSize="13px">
{name}
</Text>
</>
);
};
export default NameCell;

View File

@ -0,0 +1,185 @@
import React from "react";
import { inject, observer } from "mobx-react";
import elementResizeDetectorMaker from "element-resize-detector";
import { UserStore } from "@docspace/shared/store/UserStore";
import { TableBody } from "@docspace/shared/components/table";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import Row from "./Row";
import Header from "./Header";
import { TableViewProps } from "./TableView.types";
import { TableWrapper } from "./TableView.styled";
const TABLE_VERSION = "1";
const COLUMNS_NAME = `oauthConfigColumnsSize_ver-${TABLE_VERSION}`;
const elementResizeDetector = elementResizeDetectorMaker({
strategy: "scroll",
callOnAdd: false,
});
const TableView = ({
items,
sectionWidth,
selection,
activeClients,
setSelection,
getContextMenuItems,
changeClientStatus,
userId,
hasNextPage,
itemCount,
fetchNextClients,
}: TableViewProps) => {
const tableRef = React.useRef<HTMLDivElement>(null);
const tagRef = React.useRef<HTMLDivElement | null>(null);
const [tagCount, setTagCount] = React.useState(0);
React.useEffect(() => {
return () => {
if (!tagRef?.current) return;
elementResizeDetector.uninstall(tagRef.current);
};
}, []);
const onResize = React.useCallback(
(node: HTMLElement) => {
const element = tagRef?.current ? tagRef?.current : node;
if (element) {
const { width } = element.getBoundingClientRect();
const columns = Math.floor(width / 120);
if (columns !== tagCount) setTagCount(columns);
}
},
[tagCount],
);
const onSetTagRef = React.useCallback(
(node: HTMLDivElement) => {
if (node) {
tagRef.current = node;
onResize(node);
elementResizeDetector.listenTo(node, onResize);
}
},
[onResize],
);
const clickOutside = React.useCallback(
(e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target) return;
if (
target.closest(".checkbox") ||
target.closest(".table-container_row-checkbox") ||
e.detail === 0
) {
return;
}
setSelection?.("");
},
[setSelection],
);
React.useEffect(() => {
window.addEventListener("click", clickOutside);
return () => {
window.removeEventListener("click", clickOutside);
};
}, [clickOutside, setSelection]);
const columnStorageName = `${COLUMNS_NAME}=${userId}`;
const fetchMoreFiles = React.useCallback(
async ({ startIndex }: { startIndex: number; stopIndex: number }) => {
await fetchNextClients?.(startIndex);
},
[fetchNextClients],
);
return (
<TableWrapper forwardedRef={tableRef} useReactWindow>
<Header
sectionWidth={sectionWidth}
tableRef={tableRef.current}
columnStorageName={columnStorageName}
tagRef={onSetTagRef}
/>
<TableBody
itemHeight={49}
useReactWindow
columnStorageName={columnStorageName}
columnInfoPanelStorageName=" "
filesLength={items.length}
fetchMoreFiles={fetchMoreFiles}
hasMoreFiles={hasNextPage || false}
itemCount={itemCount || 0}
>
{items.map((item) => (
<Row
key={item.clientId}
item={item}
isChecked={selection?.includes(item.clientId) || false}
inProgress={activeClients?.includes(item.clientId) || false}
setSelection={setSelection}
changeClientStatus={changeClientStatus}
getContextMenuItems={getContextMenuItems}
tagCount={tagCount}
/>
))}
</TableBody>
</TableWrapper>
);
};
export default inject(
({
userStore,
oauthStore,
}: {
userStore: UserStore;
oauthStore: OAuthStoreProps;
}) => {
const userId = userStore.user?.id;
const {
viewAs,
setViewAs,
selection,
setSelection,
setBufferSelection,
changeClientStatus,
getContextMenuItems,
activeClients,
hasNextPage,
itemCount,
fetchNextClients,
} = oauthStore;
return {
viewAs,
setViewAs,
userId,
changeClientStatus,
selection,
setSelection,
setBufferSelection,
activeClients,
getContextMenuItems,
hasNextPage,
itemCount,
fetchNextClients,
};
},
)(observer(TableView));

View File

@ -0,0 +1,75 @@
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { IClientProps } from "@docspace/shared/utils/oauth/types";
import { Text } from "@docspace/shared/components/text";
import { Consumer } from "@docspace/shared/utils/context";
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
import TableView from "./TableView";
import RowView from "./RowView";
import RegisterNewButton from "../RegisterNewButton";
export const StyledContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
.description {
margin-bottom: 20px;
max-width: 700px;
}
.add-button {
width: fit-content;
margin-bottom: 12px;
}
`;
interface ListProps {
clients: IClientProps[];
viewAs: ViewAsType;
currentDeviceType: DeviceUnionType;
}
const List = ({ clients, viewAs, currentDeviceType }: ListProps) => {
const { t } = useTranslation(["OAuth", "Common"]);
return (
<StyledContainer>
<Text
fontSize="12px"
fontWeight={400}
lineHeight="16px"
title={t("OAuthAppDescription")}
className="description"
>
{t("OAuthAppDescription")}
</Text>
<RegisterNewButton currentDeviceType={currentDeviceType} />
<Consumer>
{(context) =>
viewAs === "table" ? (
<TableView
items={clients || []}
sectionWidth={context.sectionWidth || 0}
/>
) : (
<RowView
items={clients || []}
sectionWidth={context.sectionWidth || 0}
/>
)
}
</Consumer>
</StyledContainer>
);
};
export default List;

View File

@ -0,0 +1,354 @@
import React from "react";
import { inject, observer } from "mobx-react";
import styled, { useTheme } from "styled-components";
import { useTranslation } from "react-i18next";
import { IClientProps } from "@docspace/shared/utils/oauth/types";
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
import { SocialButton } from "@docspace/shared/components/social-button";
import { Text } from "@docspace/shared/components/text";
import { Textarea } from "@docspace/shared/components/textarea";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { Base } from "@docspace/shared/themes";
import { generatePKCEPair } from "@docspace/shared/utils/oauth";
import { AuthenticationMethod } from "@docspace/shared/enums";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import OnlyofficeLight from "PUBLIC_DIR/images/onlyoffice.light.react.svg";
import OnlyofficeDark from "PUBLIC_DIR/images/onlyoffice.dark.react.svg";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
const StyledContainer = styled.div`
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 20px;
`;
const StyledPreviewContainer = styled.div`
width: 100%;
height: 152px;
box-sizing: border-box;
border: ${(props) => props.theme.oauth.previewDialog.border};
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
.social-button {
max-width: 226px;
padding: 11px 16px;
box-sizing: border-box;
display: flex;
gap: 16px;
.iconWrapper {
padding: 0;
margin: 0;
}
}
`;
StyledPreviewContainer.defaultProps = { theme: Base };
const StyledBlocksContainer = styled.div`
width: 100%;
height: auto;
display: flex;
flex-direction: column;
gap: 12px;
.block-container {
display: flex;
flex-direction: column;
gap: 4px;
}
`;
const htmlBlock = `<body>
<button id="docspace-button" class="docspace-button">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.89992 18.7913L1.47441 15.2914C0.841864 14.9858 0.841864 14.5136 1.47441 14.2359L4.05959 13.0137L8.87242 15.2914C9.50497 15.5969 10.5225 15.5969 11.1276 15.2914L15.9404 13.0137L18.5256 14.2359C19.1581 14.5414 19.1581 15.0136 18.5256 15.2914L11.1001 18.7913C10.5225 19.069 9.50497 19.069 8.89992 18.7913Z" fill="#FF6F3D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.87586 14.4606L1.47296 10.9566C0.842346 10.6507 0.842346 10.178 1.47296 9.89989L3.99543 8.7041L8.87586 11.0123C9.50647 11.3182 10.5209 11.3182 11.1241 11.0123L16.0046 8.7041L18.527 9.89989C19.1577 10.2058 19.1577 10.6785 18.527 10.9566L11.1241 14.4606C10.4935 14.7665 9.47906 14.7665 8.87586 14.4606Z" fill="#95C038"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.87586 10.1747L1.47296 6.72119C0.842346 6.41969 0.842346 5.95374 1.47296 5.67965L8.87586 2.22612C9.50647 1.92463 10.5209 1.92463 11.1241 2.22612L18.527 5.67965C19.1577 5.98115 19.1577 6.4471 18.527 6.72119L11.1241 10.1747C10.4935 10.4488 9.47906 10.4488 8.87586 10.1747Z" fill="#5DC0E8"/>
</svg>
Sign in with DocSpace
</button>
</body>`;
const styleBlock = `<style>
.docspace-button {
width: auto;
padding: 0 20px;
display: flex;
flex-direction: row;
align-items: center;
text-decoration: none;
border-radius: 2px;
height: 40px;
border: none;
stroke: none;
background: #ffffff;
box-shadow: rgba(0, 0, 0, 0.24) 0px 1px 1px, rgba(0, 0, 0, 0.12) 0px 0px 1px;
color: rgb(163, 169, 174);
font-weight: 600;
font-size: 14px;
line-height: 14px;
user-select: none;
font-family: Roboto, "Open Sans", sans-serif, Arial;
}
.docspace-button:hover {
box-shadow: rgba(0, 0, 0, 0.24) 0px 1px 1px, rgba(0, 0, 0, 0.12) 0px 0px 1px;
cursor: pointer;
color: #333333;
}
.docspace-button:active {
background-color: #F8F9F9;
color: #333333;
cursor: pointer;
}
.logo-svg {
width: 18px;
min-width: 18px;
height: 18px;
min-height: 18px;
margin: 11px 16px;
}
</style>`;
const linkParams =
"width=800,height=800,status=no,toolbar=no,menubar=no,resizable=yes,scrollbars=no";
interface PreviewDialogProps {
visible: boolean;
setPreviewDialogVisible?: (value: boolean) => void;
client?: IClientProps;
}
const PreviewDialog = ({
visible,
setPreviewDialogVisible,
client,
}: PreviewDialogProps) => {
const { t } = useTranslation(["OAuth", "Common", "Webhooks"]);
const theme = useTheme();
const [codeVerifier, setCodeVerifier] = React.useState("");
const [codeChallenge, setCodeChallenge] = React.useState("");
const [state, setState] = React.useState("");
const onClose = () => setPreviewDialogVisible?.(false);
const icon = theme.isBase ? OnlyofficeLight : OnlyofficeDark;
const scopesString = client?.scopes.join(" ");
const isClientSecretPost = !client?.authenticationMethods.includes(
AuthenticationMethod.none,
);
const encodingScopes = encodeURI(scopesString || "");
const getData = React.useCallback(() => {
const { verifier, challenge, state: s } = generatePKCEPair();
setCodeVerifier(verifier);
setCodeChallenge(challenge);
setState(s);
}, []);
React.useEffect(() => {
getData();
}, [getData]);
const getLink = () => {
return `${
window?.ClientConfig?.oauth2.origin
}/oauth2/authorize?response_type=code&client_id=${client?.clientId}&redirect_uri=${
client?.redirectUris[0]
}&scope=${encodingScopes}&state=${state}${
isClientSecretPost
? ""
: `&code_challenge_method=S256&code_challenge=${codeChallenge}`
}`;
};
const link = getLink();
const scriptBlock = `<script>
const button = document.getElementById('docspace-button')
function openOAuthPage() {
window.open(
"${link}",
"login",
${linkParams}
);
}
button.addEventListener('click', openOAuthPage)
</script>`;
return (
<ModalDialog
visible={visible}
displayType={ModalDialogType.aside}
onClose={onClose}
withFooterBorder
>
<ModalDialog.Header>{t("AuthButton")}</ModalDialog.Header>
<ModalDialog.Body>
<StyledContainer>
<StyledPreviewContainer>
<SocialButton
className="social-button"
label={t("SignIn")}
IconComponent={icon}
onClick={() => {
window.open(link, "login", linkParams);
}}
/>
</StyledPreviewContainer>
<StyledBlocksContainer>
<div className="block-container">
<Text fontWeight={600} lineHeight="20px" fontSize="13px" noSelect>
HTML
</Text>
<Textarea
heightTextArea={64}
enableCopy
isReadOnly
isDisabled
value={htmlBlock}
/>
</div>
<div className="block-container">
<Text fontWeight={600} lineHeight="20px" fontSize="13px" noSelect>
CSS
</Text>
<Textarea
heightTextArea={64}
enableCopy
isReadOnly
isDisabled
value={styleBlock}
/>
</div>
<div className="block-container">
<Text fontWeight={600} lineHeight="20px" fontSize="13px" noSelect>
JavaScript
</Text>
<Textarea
heightTextArea={64}
enableCopy
isReadOnly
isDisabled
value={scriptBlock}
/>
</div>
<div className="block-container">
<Text fontWeight={600} lineHeight="20px" fontSize="13px" noSelect>
{t("AuthorizeLink")}
</Text>
<Textarea
heightTextArea={64}
enableCopy
isReadOnly
isDisabled
value={link}
/>
</div>
<div className="block-container">
<Text fontWeight={600} lineHeight="20px" fontSize="13px" noSelect>
{t("Webhooks:State")}
</Text>
<Textarea
heightTextArea={64}
enableCopy
isReadOnly
isDisabled
value={state}
/>
</div>
{!isClientSecretPost && (
<div className="block-container">
<Text
fontWeight={600}
lineHeight="20px"
fontSize="13px"
noSelect
>
{t("CodeVerifier")}
</Text>
<Textarea
heightTextArea={64}
enableCopy
isReadOnly
isDisabled
value={codeVerifier}
/>
</div>
)}
</StyledBlocksContainer>
</StyledContainer>
</ModalDialog.Body>
<ModalDialog.Footer>
<Button
size={ButtonSize.normal}
scale
label={t("Common:OkButton")}
onClick={onClose}
/>
</ModalDialog.Footer>
</ModalDialog>
);
};
export default inject(
({
oauthStore,
settingsStore,
}: {
settingsStore: SettingsStore;
oauthStore: OAuthStoreProps;
}) => {
const { setPreviewDialogVisible, bufferSelection } = oauthStore;
const { theme } = settingsStore;
return {
setPreviewDialogVisible,
client: bufferSelection,
theme,
};
},
)(observer(PreviewDialog));

View File

@ -0,0 +1,5 @@
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
export interface RegisterNewButtonProps {
currentDeviceType?: DeviceUnionType;
}

View File

@ -0,0 +1,30 @@
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { RegisterNewButtonProps } from "./RegisterNewButton.types";
const RegisterNewButton = ({ currentDeviceType }: RegisterNewButtonProps) => {
const { t } = useTranslation(["OAuth", "Common"]);
const navigate = useNavigate();
const onClick = () => {
navigate("create");
};
return (
<Button
className="add-button"
size={
currentDeviceType !== "desktop" ? ButtonSize.normal : ButtonSize.small
}
label={t("RegisterNewApp")}
primary
onClick={onClick}
/>
);
};
export default RegisterNewButton;

View File

@ -0,0 +1,91 @@
import React from "react";
import { useParams } from "react-router-dom";
import { inject, observer } from "mobx-react";
import { useTranslation, Trans } from "react-i18next";
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { toastr } from "@docspace/shared/components/toast";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
interface ResetDialogProps {
isVisible?: boolean;
onClose?: () => void;
onReset?: (id: string) => Promise<void>;
}
const ResetDialog = (props: ResetDialogProps) => {
const { id } = useParams();
const { t, ready } = useTranslation(["OAuth", "Common"]);
const { isVisible, onClose, onReset } = props;
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
const onResetClick = async () => {
try {
setIsRequestRunning(true);
if (id) await onReset?.(id);
setIsRequestRunning(true);
onClose?.();
} catch (error: unknown) {
const e = error as TData;
toastr.error(e);
onClose?.();
}
};
return (
<ModalDialog
isLoading={!ready}
visible={isVisible}
onClose={onClose}
displayType={ModalDialogType.modal}
>
<ModalDialog.Header>{t("ResetHeader")}</ModalDialog.Header>
<ModalDialog.Body>
<Trans t={t} i18nKey="ResetDescription" ns="OAuth" />
</ModalDialog.Body>
<ModalDialog.Footer>
<Button
className="delete-button"
key="DeletePortalBtn"
label={t("Common:OkButton")}
size={ButtonSize.normal}
scale
primary
isLoading={isRequestRunning}
onClick={onResetClick}
/>
<Button
className="cancel-button"
key="CancelDeleteBtn"
label={t("Common:CancelButton")}
size={ButtonSize.normal}
scale
isDisabled={isRequestRunning}
onClick={onClose}
/>
</ModalDialog.Footer>
</ModalDialog>
);
};
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
const { setResetDialogVisible, regenerateSecret, resetDialogVisible } =
oauthStore;
const onClose = () => {
setResetDialogVisible(false);
};
const onReset = async (id: string) => {
await regenerateSecret(id);
};
return { isVisible: resetDialogVisible, onClose, onReset };
})(observer(ResetDialog));

View File

@ -25,26 +25,29 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useEffect, useState, useTransition } from "react";
import { Tabs } from "@docspace/shared/components/tabs";
import { Box } from "@docspace/shared/components/box";
import { inject, observer } from "mobx-react";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import config from "PACKAGE_FILE";
import { useNavigate, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Box } from "@docspace/shared/components/box";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import JavascriptSDK from "./JavascriptSDK";
import Webhooks from "./Webhooks";
import Api from "./Api";
import { useTranslation } from "react-i18next";
import SSOLoader from "./sub-components/ssoLoader";
import PluginSDK from "./PluginSDK";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
import OAuth from "./OAuth";
import SSOLoader from "./sub-components/ssoLoader";
const DeveloperToolsWrapper = (props) => {
const { currentDeviceType } = props;
const { currentDeviceType, identityServerEnabled } = props;
const navigate = useNavigate();
const location = useLocation();
@ -57,6 +60,7 @@ const DeveloperToolsWrapper = (props) => {
"Settings",
"WebPlugins",
"Common",
"OAuth",
]);
const [isPending, startTransition] = useTransition();
@ -95,6 +99,14 @@ const DeveloperToolsWrapper = (props) => {
},
];
if (identityServerEnabled) {
data.push({
id: "oauth",
name: t("OAuth:OAuth"),
content: <OAuth />,
});
}
const load = async () => {
//await loadBaseInfo();
};
@ -136,13 +148,16 @@ const DeveloperToolsWrapper = (props) => {
);
};
export default inject(({ setup, settingsStore }) => {
export default inject(({ setup, settingsStore, authStore }) => {
const { initSettings } = setup;
const { identityServerEnabled } = authStore.capabilities;
return {
currentDeviceType: settingsStore.currentDeviceType,
loadBaseInfo: async () => {
await initSettings();
},
identityServerEnabled,
};
})(observer(DeveloperToolsWrapper));

View File

@ -531,6 +531,14 @@ export const settingsTree = [
tKey: "Common:DeveloperTools",
isCategory: true,
},
{
id: "portal-settings_catalog-oauth",
key: "7-4",
icon: "",
link: "oauth",
tKey: "OAuth:OAuth",
isCategory: true,
},
],
},
{

View File

@ -41,6 +41,8 @@ import FileManagement from "./sub-components/file-management";
import InterfaceTheme from "./sub-components/interface-theme";
import { tablet } from "@docspace/shared/utils";
import { DeviceType } from "@docspace/shared/enums";
import AuthorizedApps from "./sub-components/authorized-apps";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
const Wrapper = styled.div`
@ -63,7 +65,13 @@ const StyledTabs = styled(Tabs)`
`;
const SectionBodyContent = (props) => {
const { showProfileLoader, profile, currentDeviceType, t } = props;
const {
showProfileLoader,
profile,
currentDeviceType,
identityServerEnabled,
t,
} = props;
const navigate = useNavigate();
const data = [
@ -84,6 +92,14 @@ const SectionBodyContent = (props) => {
},
];
if (identityServerEnabled) {
data.push({
id: "authorized-apps",
name: t("OAuth:AuthorizedApps"),
content: <AuthorizedApps />,
});
}
if (!profile?.isVisitor)
data.splice(2, 0, {
id: "file-management",
@ -120,16 +136,21 @@ const SectionBodyContent = (props) => {
);
};
export default inject(({ settingsStore, peopleStore, clientLoadingStore }) => {
export default inject(
({ settingsStore, peopleStore, clientLoadingStore, authStore }) => {
const { showProfileLoader } = clientLoadingStore;
const { targetUser: profile } = peopleStore.targetUserStore;
const { identityServerEnabled } = authStore.capabilities;
return {
profile,
currentDeviceType: settingsStore.currentDeviceType,
showProfileLoader,
identityServerEnabled,
};
})(
},
)(
observer(
withTranslation([
"Profile",
@ -141,6 +162,7 @@ export default inject(({ settingsStore, peopleStore, clientLoadingStore }) => {
"DeleteSelfProfileDialog",
"Notifications",
"ConnectDialog",
"OAuth",
])(SectionBodyContent),
),
);

View File

@ -0,0 +1,12 @@
import styled from "styled-components";
const StyledContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
`;
export { StyledContainer };

View File

@ -0,0 +1,33 @@
import { IClientProps } from "@docspace/shared/utils/oauth/types";
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
export interface AuthorizedAppsProps {
consents?: IClientProps[];
fetchConsents?: () => Promise<void>;
viewAs: ViewAsType;
setViewAs: (value: string) => void;
currentDeviceType: DeviceUnionType;
infoDialogVisible: boolean;
fetchScopes?: () => Promise<void>;
revokeDialogVisible: boolean;
setRevokeDialogVisible: (value: boolean) => void;
selection: string[];
bufferSelection: IClientProps;
revokeClient: (value: string[]) => Promise<void>;
}
export interface RevokeDialogProps {
visible: boolean;
onClose: () => void;
selection: string[];
bufferSelection: IClientProps;
onRevoke: (value: string[]) => Promise<void>;
currentDeviceType: DeviceUnionType;
}

View File

@ -0,0 +1,138 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import { Consumer } from "@docspace/shared/utils/context";
import { Text } from "@docspace/shared/components/text";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import useViewEffect from "SRC_DIR/Hooks/useViewEffect";
import OAuthStore from "SRC_DIR/store/OAuthStore";
import InfoDialog from "SRC_DIR/pages/PortalSettings/categories/developer-tools/OAuth/sub-components/InfoDialog";
import { StyledContainer } from "./AuthorizedApps.styled";
import { AuthorizedAppsProps } from "./AuthorizedApps.types";
import TableView from "./sub-components/TableView";
import RowView from "./sub-components/RowView";
import RevokeDialog from "./sub-components/RevokeDialog";
import EmptyScreen from "./sub-components/EmptyScreen";
const AuthorizedApps = ({
consents,
fetchConsents,
viewAs,
setViewAs,
currentDeviceType,
infoDialogVisible,
fetchScopes,
revokeDialogVisible,
setRevokeDialogVisible,
selection,
bufferSelection,
revokeClient,
}: AuthorizedAppsProps) => {
const { t } = useTranslation(["OAuth"]);
const getConsentList = React.useCallback(async () => {
fetchScopes?.();
await fetchConsents?.();
}, [fetchConsents, fetchScopes]);
React.useEffect(() => {
if (consents?.length) return;
getConsentList();
}, [consents?.length, getConsentList]);
useViewEffect({
view: viewAs,
setView: setViewAs,
currentDeviceType,
});
return (
<StyledContainer>
{consents && consents?.length > 0 ? (
<>
<Text fontSize="12px" fontWeight="400" lineHeight="16px">
{t("ProfileDescription")}
</Text>
<Consumer>
{(context) =>
viewAs === "table" ? (
<TableView
items={consents || []}
sectionWidth={context.sectionWidth || 0}
/>
) : (
<RowView
items={consents || []}
sectionWidth={context.sectionWidth || 0}
/>
)
}
</Consumer>
</>
) : (
<EmptyScreen t={t} />
)}
{infoDialogVisible && (
<InfoDialog visible={infoDialogVisible} isProfile />
)}
{revokeDialogVisible && (
<RevokeDialog
visible={revokeDialogVisible}
onClose={() => setRevokeDialogVisible(false)}
currentDeviceType={currentDeviceType}
onRevoke={revokeClient}
selection={selection}
bufferSelection={bufferSelection}
/>
)}
</StyledContainer>
);
};
export default inject(
({
oauthStore,
settingsStore,
}: {
oauthStore: OAuthStore;
settingsStore: SettingsStore;
}) => {
const {
consents,
fetchConsents,
fetchScopes,
viewAs,
setViewAs,
infoDialogVisible,
revokeDialogVisible,
setRevokeDialogVisible,
selection,
bufferSelection,
revokeClient,
} = oauthStore;
const { currentDeviceType } = settingsStore;
return {
consents,
fetchConsents,
viewAs,
setViewAs,
currentDeviceType,
infoDialogVisible,
fetchScopes,
revokeDialogVisible,
setRevokeDialogVisible,
selection,
bufferSelection,
revokeClient,
};
},
)(observer(AuthorizedApps));

View File

@ -0,0 +1,17 @@
import { EmptyScreenContainer } from "@docspace/shared/components/empty-screen-container";
import { TTranslation } from "@docspace/shared/types";
import EmptyScreenPersonsSvgUrl from "PUBLIC_DIR/images/empty_screen_oauth.svg?url";
const EmptyScreen = ({ t }: { t: TTranslation }) => {
return (
<EmptyScreenContainer
imageSrc={EmptyScreenPersonsSvgUrl}
imageAlt="Empty apps list"
headerText={t("NoAuthorizedApps")}
descriptionText={t("ProfileDescription")}
/>
);
};
export default EmptyScreen;

View File

@ -0,0 +1,104 @@
import React from "react";
import { useTranslation, Trans } from "react-i18next";
import {
ModalDialog,
ModalDialogType,
} from "@docspace/shared/components/modal-dialog";
import { Text } from "@docspace/shared/components/text";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { RevokeDialogProps } from "../AuthorizedApps.types";
const RevokeDialog = ({
visible,
onRevoke,
onClose,
selection,
bufferSelection,
currentDeviceType,
}: RevokeDialogProps) => {
const { t } = useTranslation(["OAuth", "Common"]);
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
const isMobile = currentDeviceType === "mobile";
const isGroup = selection.length > 1;
const name = bufferSelection?.name;
const firstDesc = isGroup ? (
t("RevokeConsentDescriptionGroup")
) : (
<Trans t={t} i18nKey="RevokeConsentDescription" ns="OAuth">
Once you revoke the consent to use the ONLYOFFICE DocSpace auth data in
the service {{ name }}, ONLYOFFICE DocSpace will automatically stop
logging into {{ name }}. Your account in {{ name }} will not be deleted.
</Trans>
);
const secondDesc = isGroup ? (
t("RevokeConsentLogin")
) : (
<Trans t={t} i18nKey="RevokeConsentLogin" ns="OAuth">
If you want to renew an automatic login into {{ name }} using ONLYOFFICE
DocSpace, you will be asked to grant access to your DocSpace account data.
</Trans>
);
const onRevokeAction = async () => {
if (isRequestRunning) return;
setIsRequestRunning(true);
if (isGroup) {
await onRevoke(selection);
} else {
await onRevoke([bufferSelection.clientId]);
}
setIsRequestRunning(false);
onClose();
};
const onCloseAction = () => {
if (isRequestRunning) return;
onClose();
};
return (
<ModalDialog
visible={visible}
isLarge
autoMaxHeight
withFooterBorder={isMobile}
onClose={onCloseAction}
displayType={ModalDialogType.modal}
>
<ModalDialog.Header>{t("RevokeConsent")}</ModalDialog.Header>
<ModalDialog.Body>
<Text style={{ marginBottom: "16px" }}>{firstDesc}</Text>
<Text>{secondDesc}</Text>
</ModalDialog.Body>
<ModalDialog.Footer>
<Button
label={t("Revoke")}
primary
scale={isMobile}
size={ButtonSize.normal}
isLoading={isRequestRunning}
onClick={onRevokeAction}
/>
<Button
label={t("Common:CancelButton")}
scale={isMobile}
size={ButtonSize.normal}
isDisabled={isRequestRunning}
onClick={onCloseAction}
/>
</ModalDialog.Footer>
</ModalDialog>
);
};
export default RevokeDialog;

View File

@ -0,0 +1,48 @@
import { useTranslation } from "react-i18next";
import { Row } from "@docspace/shared/components/row";
import { RowContent } from "./RowContent";
import { RowProps } from "./RowView.types";
export const OAuthRow = (props: RowProps) => {
const {
item,
sectionWidth,
isChecked,
inProgress,
getContextMenuItems,
setSelection,
} = props;
const { t } = useTranslation(["OAuth", "Common", "Files"]);
const contextOptions = getContextMenuItems?.(t, item, false, false) || [];
const element = (
<img style={{ borderRadius: "3px" }} src={item.logo} alt="App logo" />
);
return (
<Row
key={item.clientId}
contextOptions={contextOptions}
element={element}
mode="modern"
checked={isChecked}
inProgress={inProgress}
onSelect={() => setSelection && setSelection(item.clientId)}
onRowClick={() => {}}
className={`oauth2-row${isChecked ? " oauth2-row-selected" : ""}`}
>
<RowContent
sectionWidth={sectionWidth}
item={item}
isChecked={isChecked}
inProgress={inProgress}
setSelection={setSelection}
/>
</Row>
);
};

View File

@ -0,0 +1,40 @@
import { Text } from "@docspace/shared/components/text";
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
import {
StyledRowContent,
ContentWrapper,
FlexWrapper,
} from "./RowView.styled";
import { RowContentProps } from "./RowView.types";
export const RowContent = ({ sectionWidth, item }: RowContentProps) => {
return (
<StyledRowContent sectionWidth={sectionWidth}>
<ContentWrapper>
<FlexWrapper>
<Text
fontWeight={600}
fontSize="14px"
style={{ marginInlineEnd: "8px" }}
>
{item.name}
</Text>
</FlexWrapper>
<Text fontWeight={600} fontSize="12px" color="#A3A9AE">
<Link
color="#A3A9AE"
href={item.websiteUrl}
type={LinkType.page}
target={LinkTarget.blank}
isHovered
>
{item.websiteUrl}
</Link>
</Text>
</ContentWrapper>
{null}
</StyledRowContent>
);
};

View File

@ -0,0 +1,111 @@
import styled from "styled-components";
import { RowContainer } from "@docspace/shared/components/row-container";
import { RowContent } from "@docspace/shared/components/row-content";
import { tablet } from "@docspace/shared/utils/device";
export const StyledRowContainer = styled(RowContainer)`
margin-top: 0px;
.row-list-item {
padding-left: 21px;
}
.row-loader {
width: calc(100% - 46px) !important;
padding-left: 21px;
}
img {
width: 32px;
max-width: 32px;
height: 32px;
max-height: 32px;
}
.oauth2-row-selected {
background: ${(props) =>
props.theme.filesSection.rowView.checkedBackground};
cursor: pointer;
border-bottom: none;
margin-left: -24px;
margin-right: -24px;
padding-left: 24px;
padding-right: 24px;
@media ${tablet} {
margin-left: -16px;
margin-right: -16px;
padding-left: 16px;
padding-right: 16px;
}
}
.oauth2-row {
margin-top: -3px;
padding-top: 3px;
:hover {
background: ${(props) =>
props.theme.filesSection.rowView.checkedBackground};
cursor: pointer;
border-bottom: none;
margin-left: -24px;
margin-right: -24px;
padding-left: 24px;
padding-right: 24px;
@media ${tablet} {
margin-left: -16px;
margin-right: -16px;
padding-left: 16px;
padding-right: 16px;
}
}
}
`;
export const StyledRowContent = styled(RowContent)`
display: flex;
padding-bottom: 10px;
.rowMainContainer {
height: 100%;
width: 100%;
}
.mainIcons {
min-width: 76px;
}
`;
export const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
justify-items: center;
`;
export const ToggleButtonWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
label {
margin-top: 1px;
position: relative;
gap: 0px;
margin-right: -8px;
}
`;
export const FlexWrapper = styled.div`
display: flex;
`;

View File

@ -0,0 +1,46 @@
import { IClientProps } from "@docspace/shared/utils/oauth/types";
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
import { TTranslation } from "@docspace/shared/types";
export interface RowViewProps {
items: IClientProps[];
sectionWidth: number;
selection?: string[];
setSelection?: (clientId: string) => void;
getContextMenuItems?: (
t: TTranslation,
item: IClientProps,
isInfo: boolean,
isSettings: boolean,
) => ContextMenuModel[];
activeClients?: string[];
hasNextPage?: boolean;
itemCount?: number;
fetchNextConsents?: (startIndex: number) => Promise<void>;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
export interface RowProps {
item: IClientProps;
isChecked: boolean;
inProgress: boolean;
sectionWidth: number;
getContextMenuItems?: (
t: TTranslation,
item: IClientProps,
isInfo: boolean,
isSettings: boolean,
) => ContextMenuModel[];
setSelection?: (clientId: string) => void;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
export interface RowContentProps {
sectionWidth: number;
item: IClientProps;
isChecked: boolean;
inProgress: boolean;
setSelection?: (clientId: string) => void;
}

View File

@ -0,0 +1,86 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import { OAuthRow } from "./Row";
import { RowViewProps } from "./RowView.types";
import { StyledRowContainer } from "./RowView.styled";
const RowView = (props: RowViewProps) => {
const {
items,
sectionWidth,
changeClientStatus,
selection,
setSelection,
activeClients,
getContextMenuItems,
hasNextPage,
itemCount,
fetchNextConsents,
} = props;
const fetchMoreFiles = React.useCallback(
async ({ startIndex }: { startIndex: number; stopIndex: number }) => {
await fetchNextConsents?.(startIndex);
},
[fetchNextConsents],
);
return (
<StyledRowContainer
itemHeight={59}
filesLength={items.length}
fetchMoreFiles={fetchMoreFiles}
hasMoreFiles={hasNextPage || false}
itemCount={itemCount || 0}
useReactWindow
onScroll={() => {}}
>
{items.map((item) => (
<OAuthRow
key={item.clientId}
item={item}
isChecked={selection?.includes(item.clientId) || false}
inProgress={activeClients?.includes(item.clientId) || false}
setSelection={setSelection}
changeClientStatus={changeClientStatus}
getContextMenuItems={getContextMenuItems}
sectionWidth={sectionWidth}
/>
))}
</StyledRowContainer>
);
};
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
const {
viewAs,
setViewAs,
selection,
setSelection,
changeClientStatus,
getContextMenuItems,
activeClients,
consentHasNextPage,
consentItemCount,
fetchNextConsents,
} = oauthStore;
return {
viewAs,
setViewAs,
changeClientStatus,
selection,
setSelection,
activeClients,
getContextMenuItems,
hasNextPage: consentHasNextPage,
itemCount: consentItemCount,
fetchNextConsents,
};
})(observer(RowView));

View File

@ -0,0 +1,51 @@
import { useTranslation } from "react-i18next";
import { TTableColumn, TableHeader } from "@docspace/shared/components/table";
import { HeaderProps } from "./TableView.types";
const Header = (props: HeaderProps) => {
const { sectionWidth, tableRef, columnStorageName, tagRef } = props;
const { t } = useTranslation(["Common", "OAuth"]);
const defaultColumns: TTableColumn[] = [
{
key: "App",
title: t("Apps"),
resizable: true,
enable: true,
default: true,
active: false,
minWidth: 210,
},
{
key: "Website",
title: t("Website"),
resizable: true,
enable: true,
minWidth: 150,
},
{
key: "Access granted",
title: t("OAuth:AccessGranted"),
resizable: true,
enable: true,
minWidth: 150,
},
];
return (
<TableHeader
containerRef={{ current: tableRef }}
columns={defaultColumns}
columnStorageName={columnStorageName}
sectionWidth={sectionWidth}
showSettings={false}
useReactWindow
infoPanelVisible={false}
tagRef={tagRef}
/>
);
};
export default Header;

View File

@ -0,0 +1,86 @@
import { useTranslation } from "react-i18next";
import { TableCell } from "@docspace/shared/components/table";
import { Text } from "@docspace/shared/components/text";
import getCorrectDate from "@docspace/shared/utils/getCorrectDate";
import { getCookie } from "@docspace/shared/utils/cookie";
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
import NameCell from "./columns/name";
import { StyledRowWrapper, StyledTableRow } from "./TableView.styled";
import { RowProps } from "./TableView.types";
const Row = (props: RowProps) => {
const {
item,
isChecked,
inProgress,
getContextMenuItems,
setSelection,
} = props;
const { t } = useTranslation(["OAuth", "Common", "Files"]);
const contextOptions = getContextMenuItems?.(t, item, false, false);
const locale = getCookie("asc_language");
const modifiedDate = getCorrectDate(locale || "", item.modifiedOn || "");
const getContextMenuModel = () =>
getContextMenuItems ? getContextMenuItems(t, item, false, false) : [];
return (
<StyledRowWrapper className="handle">
<StyledTableRow
contextOptions={contextOptions}
getContextModel={getContextMenuModel}
>
<TableCell className="table-container_file-name-cell">
<NameCell
name={item.name}
icon={item.logo}
isChecked={isChecked}
inProgress={inProgress}
clientId={item.clientId}
setSelection={setSelection}
/>
</TableCell>
<TableCell className="">
<Text
as="span"
fontWeight={400}
className="mr-8 textOverflow description-text"
>
<Link
className="description-text"
href={item.websiteUrl}
type={LinkType.action}
target={LinkTarget.blank}
isHovered
>
{item.websiteUrl}
</Link>
</Text>
</TableCell>
<TableCell className="">
<Text
as="span"
fontWeight={400}
className="mr-8 textOverflow description-text"
>
{modifiedDate}
</Text>
</TableCell>
</StyledTableRow>
</StyledRowWrapper>
);
};
export default Row;

View File

@ -0,0 +1,99 @@
import styled, { css } from "styled-components";
import { TableRow, TableContainer } from "@docspace/shared/components/table";
import { Base } from "@docspace/shared/themes";
export const TableWrapper = styled(TableContainer)`
margin-top: 0px;
.header-container-text {
font-size: 12px;
}
.table-container_header {
position: absolute;
}
`;
const StyledRowWrapper = styled.div`
display: contents;
`;
const StyledTableRow = styled(TableRow)`
.table-container_cell {
text-overflow: ellipsis;
padding-inline-end: 8px;
}
.mr-8 {
margin-inline-end: 8px;
}
.textOverflow {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.description-text {
color: ${(props) => props.theme.oauth.list.descriptionColor};
}
.toggleButton {
display: contents;
input {
position: relative;
margin-inline-start: -8px;
}
}
.table-container_row-loader {
margin-left: 8px;
margin-right: 16px;
}
:hover {
.table-container_cell {
cursor: pointer;
background: ${(props) =>
`${props.theme.filesSection.tableView.row.backgroundActive} !important`};
margin-top: -1px;
border-top: ${(props) =>
`1px solid ${props.theme.filesSection.tableView.row.borderColor}`};
}
.table-container_file-name-cell {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-right: -24px;
padding-right: 24px;
`
: css`
margin-left: -24px;
padding-left: 24px;
`}
}
.table-container_row-context-menu-wrapper {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-left: -20px;
padding-left: 18px;
`
: css`
margin-right: -20px;
padding-right: 18px;
`}
}
}
`;
StyledTableRow.defaultProps = { theme: Base };
export { StyledRowWrapper, StyledTableRow };

View File

@ -0,0 +1,45 @@
import { IClientProps } from "@docspace/shared/utils/oauth/types";
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
import { TTranslation } from "@docspace/shared/types";
export interface TableViewProps {
items: IClientProps[];
sectionWidth: number;
userId?: string;
selection?: string[];
setSelection?: (clientId: string) => void;
getContextMenuItems?: (
t: TTranslation,
item: IClientProps,
isInfo: boolean,
isSettings: boolean,
) => ContextMenuModel[];
bufferSelection?: IClientProps | null;
activeClients?: string[];
hasNextPage?: boolean;
itemCount?: number;
fetchNextConsents?: (startIndex: number) => Promise<void>;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
export interface HeaderProps {
sectionWidth: number;
tableRef: HTMLDivElement | null;
columnStorageName: string;
tagRef?: (node: HTMLDivElement) => void;
}
export interface RowProps {
item: IClientProps;
isChecked: boolean;
inProgress: boolean;
getContextMenuItems?: (
t: TTranslation,
item: IClientProps,
isInfo: boolean,
isSettings: boolean,
) => ContextMenuModel[];
setSelection?: (clientId: string) => void;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}

View File

@ -0,0 +1,89 @@
import React from "react";
import styled, { css } from "styled-components";
import { Text } from "@docspace/shared/components/text";
import { Checkbox } from "@docspace/shared/components/checkbox";
import { TableCell } from "@docspace/shared/components/table";
import { Loader, LoaderTypes } from "@docspace/shared/components/loader";
const StyledContainer = styled.div`
.table-container_row-checkbox {
margin-inline-start: -8px;
width: 16px;
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
padding: 16px 16px 16px 8px;
`
: css`
padding: 16px 8px 16px 16px;
`}
}
`;
const StyledImage = styled.img`
width: 32px;
height: 32px;
border-radius: 3px;
`;
interface NameCellProps {
name: string;
clientId: string;
icon?: string;
inProgress?: boolean;
isChecked?: boolean;
setSelection?: (clientId: string) => void;
}
const NameCell = ({
name,
icon,
clientId,
inProgress,
isChecked,
setSelection,
}: NameCellProps) => {
const onChange = () => {
setSelection?.(clientId);
};
return (
<>
{inProgress ? (
<Loader
className="table-container_row-loader"
type={LoaderTypes.oval}
size="16px"
/>
) : (
<TableCell
className="table-container_element-wrapper"
hasAccess
checked={isChecked}
>
<StyledContainer className="table-container_element-container">
<div className="table-container_element">
{icon && <StyledImage src={icon} alt="App icon" />}
</div>
<Checkbox
className="table-container_row-checkbox"
onChange={onChange}
isChecked={isChecked}
title={name}
/>
</StyledContainer>
</TableCell>
)}
<Text title={name} fontWeight="600" fontSize="13px">
{name}
</Text>
</>
);
};
export default NameCell;

View File

@ -0,0 +1,138 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { TableBody } from "@docspace/shared/components/table";
import { UserStore } from "@docspace/shared/store/UserStore";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import Row from "./Row";
import Header from "./Header";
import { TableViewProps } from "./TableView.types";
import { TableWrapper } from "./TableView.styled";
const TABLE_VERSION = "1";
const COLUMNS_SIZE = `consentColumnsSize_ver-${TABLE_VERSION}`;
const TableView = ({
items,
sectionWidth,
selection,
activeClients,
setSelection,
getContextMenuItems,
changeClientStatus,
userId,
hasNextPage,
itemCount,
fetchNextConsents,
}: TableViewProps) => {
const tableRef = React.useRef<HTMLDivElement>(null);
const clickOutside = React.useCallback(
(e: MouseEvent) => {
const target = e.target as HTMLElement;
if (
target.closest(".checkbox") ||
target.closest(".table-container_row-checkbox") ||
e.detail === 0
) {
return;
}
setSelection?.("");
},
[setSelection],
);
React.useEffect(() => {
window.addEventListener("click", clickOutside);
return () => {
window.removeEventListener("click", clickOutside);
};
}, [clickOutside, setSelection]);
const columnStorageName = `${COLUMNS_SIZE}=${userId}`;
const fetchMoreFiles = React.useCallback(
async ({ startIndex }: { startIndex: number; stopIndex: number }) => {
await fetchNextConsents?.(startIndex);
},
[fetchNextConsents],
);
return (
<TableWrapper forwardedRef={tableRef} useReactWindow>
<Header
sectionWidth={sectionWidth}
tableRef={tableRef.current}
columnStorageName={columnStorageName}
/>
<TableBody
itemHeight={49}
useReactWindow
columnStorageName={columnStorageName}
columnInfoPanelStorageName=" "
filesLength={items.length}
fetchMoreFiles={fetchMoreFiles}
hasMoreFiles={hasNextPage || false}
itemCount={itemCount || 0}
>
{items.map((item) => (
<Row
key={item.clientId}
item={item}
isChecked={selection?.includes(item.clientId) || false}
inProgress={activeClients?.includes(item.clientId) || false}
setSelection={setSelection}
changeClientStatus={changeClientStatus}
getContextMenuItems={getContextMenuItems}
/>
))}
</TableBody>
</TableWrapper>
);
};
export default inject(
({
userStore,
oauthStore,
}: {
userStore: UserStore;
oauthStore: OAuthStoreProps;
}) => {
const userId = userStore.user?.id;
const {
viewAs,
setViewAs,
selection,
setSelection,
setBufferSelection,
changeClientStatus,
getContextMenuItems,
activeClients,
consentHasNextPage,
consentItemCount,
fetchNextConsents,
} = oauthStore;
return {
viewAs,
setViewAs,
userId,
changeClientStatus,
selection,
setSelection,
setBufferSelection,
activeClients,
getContextMenuItems,
hasNextPage: consentHasNextPage,
itemCount: consentItemCount,
fetchNextConsents,
};
},
)(observer(TableView));

View File

@ -81,6 +81,14 @@ const generalRoutes = [
</PrivateRoute>
),
},
{
path: "authorized-apps",
element: (
<PrivateRoute>
<Profile />
</PrivateRoute>
),
},
],
},
];

View File

@ -357,6 +357,24 @@ const Viewer = loadable(() =>
),
);
const OAuthCreatePage = loadable(() =>
componentLoader(
() =>
import(
"../pages/PortalSettings/categories/developer-tools/OAuth/OAuthCreatePage"
),
),
);
const OAuthEditPage = loadable(() =>
componentLoader(
() =>
import(
"../pages/PortalSettings/categories/developer-tools/OAuth/OAuthEditPage"
),
),
);
const PortalSettingsRoutes = {
path: "portal-settings/",
element: (
@ -596,6 +614,18 @@ const PortalSettingsRoutes = {
path: "developer-tools/webhooks/:id/:eventId",
element: <WebhookDetails />,
},
{
path: "developer-tools/oauth",
element: <DeveloperTools />,
},
{
path: "developer-tools/oauth/create",
element: <OAuthCreatePage />,
},
{
path: "developer-tools/oauth/:id",
element: <OAuthEditPage />,
},
{
path: "backup",
element: <Navigate to="backup/data-backup" replace />,

View File

@ -0,0 +1,957 @@
import { makeAutoObservable, runInAction } from "mobx";
import axios from "axios";
import api from "@docspace/shared/api";
import { TFile, TFolder } from "@docspace/shared/api/files/types";
import FilesFilter, {
TSortBy,
TSortOrder,
} from "@docspace/shared/api/files/filter";
import { TRoom } from "@docspace/shared/api/rooms/types";
import RoomsFilter from "@docspace/shared/api/rooms/filter";
import { toastr } from "@docspace/shared/components/toast";
import { ROOMS_PROVIDER_TYPE_NAME } from "@docspace/shared/constants";
import { isDesktop } from "@docspace/shared/utils";
import { getDaysRemaining, isPublicRoom } from "@docspace/shared/utils/common";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import { UserStore } from "@docspace/shared/store/UserStore";
import {
FileStatus,
FilterKeys,
FilterType,
FolderType,
RoomSearchArea,
RoomsProviderType,
RoomsType,
ShareAccessRights,
} from "@docspace/shared/enums";
import {
getCategoryTypeByFolderType,
getCategoryUrl,
} from "SRC_DIR/helpers/utils";
import { CategoryType } from "SRC_DIR/helpers/constants";
import FilesStore from "./FilesStore";
import SelectedFolderStore from "./SelectedFolderStore";
import TreeFoldersStore from "./TreeFoldersStore";
import ClientLoadingStore from "./ClientLoadingStore";
import PublicRoomStore from "./PublicRoomStore";
import InfoPanelStore from "./InfoPanelStore";
import PluginStore from "./PluginStore";
import FilesSettingsStore from "./FilesSettingsStore";
import ThirdPartyStore from "./ThirdPartyStore";
let requestCounter = 0;
const NotFoundHttpCode = 404;
const ForbiddenHttpCode = 403;
const PaymentRequiredHttpCode = 402;
const UnauthorizedHttpCode = 401;
class FilesListStore {
files: Map<string | number, TFile> = new Map();
folders: Map<string | number, TFolder | TRoom> = new Map();
isEmptyPage: boolean = false;
roomsController = new AbortController();
filesController = new AbortController();
constructor(
private settingsStore: SettingsStore,
private selectedFolderStore: SelectedFolderStore,
private treeFoldersStore: TreeFoldersStore,
private clientLoadingStore: ClientLoadingStore,
private userStore: UserStore,
private publicRoomStore: PublicRoomStore,
private infoPanelStore: InfoPanelStore,
private pluginStore: PluginStore,
private filesSettingsStore: FilesSettingsStore,
private thirdPartyStore: ThirdPartyStore,
private filesStore: FilesStore,
) {
makeAutoObservable(this);
}
setIsEmptyPage = (isEmptyPage: boolean) => {
this.isEmptyPage = isEmptyPage;
};
setFile = (file: TFile) => {
if (!this.files.has(file.id)) return;
this.files.set(file.id, file);
this.filesStore.createThumbnail(file);
};
setFiles = (files: TFile[]) => {
const { socketHelper } = this.settingsStore;
if (this.files.size > 0) {
const roomParts = Array.from(this.files.keys()).map((k) => `FILE-${k}`);
socketHelper.emit({
command: "unsubscribe",
data: {
roomParts,
individual: true,
},
});
}
const newFiles: Map<string | number, TFile> = new Map();
const newRoomParts: string[] = [];
files.forEach((value) => {
const key = value.id;
newFiles.set(key, value);
newRoomParts.push(`FILE-${key}`);
});
this.files = newFiles;
if (newRoomParts.length) {
socketHelper.emit({
command: "subscribe",
data: {
roomParts: newRoomParts,
individual: true,
},
});
}
this.filesStore.createThumbnails(files);
};
updateFileStatus = (id: string | number | undefined, status: FileStatus) => {
if (!id) return;
const item = this.files.get(id);
if (item) this.files.set(id, { ...item, fileStatus: status });
};
getFileInfo = async (id: string | number) => {
const fileInfo = await api.files.getFileInfo(id);
this.setFile(fileInfo);
return fileInfo;
};
setFolder = (folder: TRoom | TFolder) => {
if (!this.folders.has(folder.id)) return;
this.folders.set(folder.id, folder);
};
setFolders = (folders: TRoom[] | TFolder[]) => {
const { socketHelper } = this.settingsStore;
if (folders.length === 0) return;
if (this.folders.size > 0) {
const roomParts = Array.from(this.folders.keys()).map((k) => `DIR-${k}`);
socketHelper.emit({
command: "unsubscribe",
data: {
roomParts,
individual: true,
},
});
}
const newFolders: Map<string | number, TRoom | TFolder> = new Map();
const newRoomParts: string[] = [];
folders.forEach((value) => {
const key = value.id;
newFolders.set(key, value);
newRoomParts.push(`DIR-${key}`);
});
this.folders = newFolders;
socketHelper.emit({
command: "subscribe",
data: {
roomParts: newRoomParts,
individual: true,
},
});
};
addFolder = (folder: TRoom | TFolder) => {
const { socketHelper } = this.settingsStore;
const newFolders = new Map([
[folder.id, folder],
...this.folders.entries(),
]);
socketHelper.emit({
command: "subscribe",
data: {
roomParts: `DIR-${folder.id}`,
individual: true,
},
});
this.folders = newFolders;
};
removeFolder = (key: string | number) => {
this.folders.delete(key);
};
updateRoomMute = (id: string | number, mute: boolean) => {
const room = this.folders.get(id);
if (!room) return;
this.folders.set(id, { ...room, mute });
};
getFolderInfo = async (id: string | number) => {
const folderInfo = await api.files.getFolderInfo(id);
this.setFolder(folderInfo);
return folderInfo;
};
clearFiles = () => {
this.files = new Map();
this.folders = new Map();
this.selectedFolderStore.setSelectedFolder(null);
};
abortAllFetch = () => {
this.filesController.abort();
this.roomsController.abort();
this.filesController = new AbortController();
this.roomsController = new AbortController();
};
fetchFiles = async (
folderId: string | number,
filter: FilesFilter,
clearFilter: boolean = true,
withSubfolders: boolean = false,
clearSelection: boolean = true,
) => {
const { setSelectedNode } = this.treeFoldersStore;
if (this.clientLoadingStore.isLoading) {
this.abortAllFetch();
}
const filterData = filter ? filter.clone() : FilesFilter.getDefault();
filterData.folder = folderId;
if (folderId === "@my" && this.userStore.user?.isVisitor) {
const url = getCategoryUrl(CategoryType.Shared);
window.DocSpace.navigate(
`${url}?${RoomsFilter.getDefault().toUrlParams()}`,
);
return;
}
this.filesStore.setIsErrorRoomNotAvailable(false);
const filterStorageItem =
this.userStore.user?.id &&
localStorage.getItem(`UserFilter=${this.userStore.user.id}`);
if (filterStorageItem && !filter) {
const splitFilter = filterStorageItem.split(",");
filterData.sortBy = splitFilter[0] as TSortBy;
filterData.pageCount = +splitFilter[1];
filterData.sortOrder = splitFilter[2] as TSortOrder;
}
if (!this.settingsStore.withPaging) {
filterData.page = 0;
filterData.pageCount = 100;
}
const defaultFilter = FilesFilter.getDefault();
const { filterType, searchInContent } = filterData;
if (typeof filterData.withSubfolders !== "boolean")
filterData.withSubfolders = defaultFilter.withSubfolders;
if (typeof searchInContent !== "boolean")
filterData.searchInContent = defaultFilter.searchInContent;
if (!Object.keys(FilterType).find((key) => FilterType[key] === filterType))
filterData.filterType = defaultFilter.filterType;
setSelectedNode([`${folderId}`]);
try {
const folder = await api.files.getFolder(
folderId,
filterData,
this.filesController.signal,
);
let newTotal = folder.total;
// fixed row loader if total and items length is different
const itemsLength = folder.folders.length + folder.files.length;
if (itemsLength < filterData.pageCount) {
newTotal =
filterData.page > 0
? itemsLength + this.files.size + this.folders.size
: itemsLength;
}
filterData.total = newTotal;
if (
(folder.current.roomType === RoomsType.PublicRoom ||
folder.current.roomType === RoomsType.FormRoom ||
folder.current.roomType === RoomsType.CustomRoom) &&
!this.publicRoomStore.isPublicRoom
) {
await this.publicRoomStore.getExternalLinks(folder.current.id);
}
if (newTotal > 0) {
const lastPage = filterData.getLastPage();
if (filterData.page > lastPage) {
filterData.page = lastPage;
return this.fetchFiles(
folderId,
filterData,
clearFilter,
withSubfolders,
);
}
}
runInAction(() => {
if (!this.publicRoomStore.isPublicRoom) {
this.filesStore.categoryType = getCategoryTypeByFolderType(
folder.current.rootFolderType,
folder.current.parentId,
);
}
});
if (this.filesStore.isPreview) {
// save filter for after closing preview change url
this.filesStore.setTempFilter(filterData);
} else {
this.filesStore.setFilesFilter(filterData); // TODO: FILTER
}
const isPrivacyFolder =
folder.current.rootFolderType === FolderType.Privacy;
let inRoom = false;
const navigationPath = await Promise.all(
folder.pathParts.map(async (f, idx) => {
const { Rooms, Archive } = FolderType;
const isCurrentFolder = folder.current.id === f.id;
const folderInfo = isCurrentFolder
? folder.current
: { ...f, id: f.id };
const { title, roomType } = folderInfo;
inRoom = inRoom || (!!roomType && !isCurrentFolder);
const isRootRoom =
idx === 0 &&
(folder.current.rootFolderType === Rooms ||
folder.current.rootFolderType === Archive);
let shared;
let canCopyPublicLink;
if (idx === 1) {
let room = folder.current;
if (!isCurrentFolder) {
room = await api.files.getFolderInfo(folderId);
shared = room.shared;
canCopyPublicLink =
room.access === ShareAccessRights.RoomManager ||
room.access === ShareAccessRights.None;
if ("canCopyPublicLink" in room)
room.canCopyPublicLink = canCopyPublicLink;
this.infoPanelStore.setInfoPanelRoom(room);
}
const { mute } = room;
runInAction(() => {
this.filesStore.isMuteCurrentRoomNotifications = mute;
});
}
return {
id: folderId,
title,
isRoom: !!roomType,
roomType,
isRootRoom,
shared,
canCopyPublicLink,
};
}),
).then((res) => {
return res
.filter((item, index) => {
return index !== res.length - 1;
})
.reverse();
});
this.selectedFolderStore.setSelectedFolder({
folders: folder.folders,
...folder.current,
inRoom,
isRoom: !!folder.current.roomType,
pathParts: folder.pathParts,
navigationPath,
...{ new: folder.new },
// type,
});
runInAction(() => {
const isEmptyList = [...folder.folders, ...folder.files].length === 0;
if (filter && isEmptyList) {
const {
authorType,
roomId,
search,
withSubfolders: curWithSubFolders,
filterType: curFilterType,
searchInContent: curSearchInContent,
} = filter;
const isFiltered =
authorType ||
roomId ||
search ||
curWithSubFolders ||
curFilterType ||
curSearchInContent;
if (isFiltered) {
this.setIsEmptyPage(false);
} else {
this.setIsEmptyPage(isEmptyList);
}
} else {
this.setIsEmptyPage(isEmptyList);
}
this.setFolders(isPrivacyFolder && !isDesktop() ? [] : folder.folders);
this.setFiles(isPrivacyFolder && !isDesktop() ? [] : folder.files);
});
if (clearFilter) {
if (clearSelection) {
// Find not processed
const tempSelection = this.filesStore.selection.filter(
(f) =>
!this.filesStore.activeFiles.find((elem) => elem.id === f.id),
);
const tempBuffer =
this.filesStore.bufferSelection &&
this.filesStore.activeFiles.find(
(elem) => elem.id === this.filesStore.bufferSelection?.id,
) == null
? this.filesStore.bufferSelection
: null;
// console.log({ tempSelection, tempBuffer });
// Clear all selections
this.filesStore.setSelected("close");
// TODO: see bug 63479
if (this.selectedFolderStore?.id === folderId) {
// Restore not processed
if (tempSelection.length)
this.filesStore.setSelection(tempSelection);
if (tempBuffer) this.filesStore.setBufferSelection(tempBuffer);
}
}
}
this.clientLoadingStore.setIsSectionHeaderLoading(false);
const selectedFolder = {
selectedFolder: { ...this.selectedFolderStore },
};
if (this.filesStore.createdItem) {
const newItem = this.filesList.find(
(item) => item.id === this.filesStore.createdItem?.id,
);
if (newItem) {
this.filesStore.setBufferSelection(newItem);
this.filesStore.setScrollToItem({
id: newItem.id,
type: this.filesStore.createdItem?.type,
});
}
this.filesStore.setCreatedItem(null);
}
if (isPublicRoom()) {
return folder;
}
return selectedFolder;
} catch (err) {
if (err?.response?.status === 402)
this.filesStore.currentTariffStatusStore.setPortalTariff();
const isThirdPartyError = Number.isNaN(+folderId);
if (requestCounter > 0 && !isThirdPartyError) return;
requestCounter = +1;
const isUserError = [
NotFoundHttpCode,
ForbiddenHttpCode,
PaymentRequiredHttpCode,
UnauthorizedHttpCode,
].includes(err?.response?.status);
if (isUserError && !isThirdPartyError) {
this.filesStore.setIsErrorRoomNotAvailable(true);
} else if (axios.isCancel(err)) {
console.log("Request canceled", err.message);
} else {
toastr.error(err);
if (isThirdPartyError) {
const userId = this.userStore?.user?.id;
const searchArea = window.DocSpace.location.pathname.includes(
"shared",
)
? RoomSearchArea.Active
: RoomSearchArea.Archive;
window.DocSpace.navigate(
`${window.DocSpace.location.pathname}?${RoomsFilter.getDefault(userId, searchArea).toUrlParams(userId, true)}`,
);
}
return;
}
} finally {
if (window?.DocSpace?.location?.state?.highlightFileId) {
this.filesStore.setHighlightFile({
highlightFileId: window.DocSpace.location.state.highlightFileId,
isFileHasExst: window.DocSpace.location.state.isFileHasExst,
});
}
}
};
fetchRooms = async (
folderId,
filter,
clearFilter = true,
withSubfolders = false,
clearSelection = true,
withFilterLocalStorage = false,
) => {
const { setSelectedNode } = this.treeFoldersStore;
if (this.clientLoadingStore.isLoading) {
this.abortAllFetch();
}
const filterData = filter
? filter.clone()
: RoomsFilter.getDefault(this.userStore.user?.id);
if (!this.settingsStore.withPaging) {
const isCustomCountPage =
filter && filter.pageCount !== 100 && filter.pageCount !== 25;
if (!isCustomCountPage) {
filterData.page = 0;
filterData.pageCount = 100;
}
}
if (folderId) setSelectedNode([`${folderId}`]);
const defaultFilter = RoomsFilter.getDefault();
const { provider, quotaFilter, type } = filterData;
if (!ROOMS_PROVIDER_TYPE_NAME[provider])
filterData.provider = defaultFilter.provider;
if (
quotaFilter &&
quotaFilter !== FilterKeys.customQuota &&
quotaFilter !== FilterKeys.defaultQuota
)
filterData.quotaFilter = defaultFilter.quotaFilter;
if (type && !RoomsType[type]) filterData.type = defaultFilter.type;
try {
const rooms = await api.rooms.getRooms(
filterData,
this.roomsController.signal,
);
if (!folderId) setSelectedNode([`${rooms.current.id}`]);
filterData.total = rooms.total;
if (rooms.total > 0) {
const lastPage = filterData.getLastPage();
if (filterData.page > lastPage) {
filterData.page = lastPage;
return this.fetchRooms(
folderId,
filterData,
undefined,
undefined,
undefined,
true,
);
}
runInAction(() => {
this.filesStore.categoryType = getCategoryTypeByFolderType(
rooms.current.rootFolderType,
rooms.current.parentId,
);
});
this.filesStore.setRoomsFilter(filterData);
runInAction(() => {
const isEmptyList = rooms.folders.length === 0;
if (filter && isEmptyList) {
const {
subjectId,
filterValue,
type: curType,
withSubfolders: withRoomsSubfolders,
searchInContent: searchInContentRooms,
tags,
withoutTags,
quotaFilter: curQuotaFilter,
provider: curProvider,
} = filter;
const isFiltered =
subjectId ||
filterValue ||
curType ||
curProvider ||
withRoomsSubfolders ||
searchInContentRooms ||
tags ||
withoutTags ||
curQuotaFilter;
if (isFiltered) {
this.setIsEmptyPage(false);
} else {
this.setIsEmptyPage(isEmptyList);
}
} else {
this.setIsEmptyPage(isEmptyList);
}
this.setFolders(rooms.folders);
this.setFiles([]);
if (clearFilter) {
if (clearSelection) {
this.filesStore.setSelected("close");
}
}
this.infoPanelStore.setInfoPanelRoom(null);
this.selectedFolderStore.setSelectedFolder({
folders: rooms.folders,
...rooms.current,
pathParts: rooms.pathParts,
navigationPath: [],
...{ new: rooms.new },
});
this.clientLoadingStore.setIsSectionHeaderLoading(false);
const selectedFolder = {
selectedFolder: { ...this.selectedFolderStore },
};
if (this.filesStore.createdItem) {
const newItem = this.filesStore.filesList.find(
(item) => item.id === this.filesStore.createdItem?.id,
);
if (newItem) {
this.filesStore.setBufferSelection(newItem);
this.filesStore.setScrollToItem({
id: newItem.id,
type: this.filesStore.createdItem?.type,
});
}
this.filesStore.setCreatedItem(null);
}
this.filesStore.setIsErrorRoomNotAvailable(false);
return selectedFolder;
});
}
} catch (err) {
if (err?.response?.status === 402)
this.filesStore.currentTariffStatusStore.setPortalTariff();
if (axios.isCancel(err)) {
console.log("Request canceled", err.message);
} else {
toastr.error(err);
}
}
};
getFilesListItems = (items: (TFile | TFolder | TRoom)[]) => {
const { fileItemsList } = this.pluginStore;
const { enablePlugins } = this.settingsStore;
const { getIcon } = this.filesSettingsStore;
return items.map((item) => {
const { id, rootFolderId, access } = item;
let thirdPartyIcon = "";
let providerType = "";
if ("providerKey" in item) {
thirdPartyIcon = this.thirdPartyStore.getThirdPartyIcon(
item.providerKey,
"small",
);
}
if ("providerKey" in item) {
providerType =
RoomsProviderType[
Object.keys(RoomsProviderType).find(
(key) => key === item.providerKey,
)
];
}
let canOpenPlayer = false;
let needConvert = false;
if ("viewAccessibility" in item) {
canOpenPlayer =
item.viewAccessibility?.ImageView ||
item.viewAccessibility?.MediaView;
needConvert = item.viewAccessibility?.MustConvert;
}
const previewUrl = canOpenPlayer
? this.filesStore.getItemUrl(id, false, needConvert, canOpenPlayer)
: null;
const contextOptions = this.filesStore.getFilesContextOptions(item);
const isThirdPartyFolder =
"providerKey" in item && item.providerKey && id === rootFolderId;
const iconSize = this.filesStore.viewAs === "table" ? 24 : 32;
let isFolder = false;
if ("parentId" in item) {
this.folders.forEach((value) => {
if (value.id === item.id && value.parentId === item.parentId)
isFolder = true;
});
}
const { isRecycleBinFolder } = this.treeFoldersStore;
const folderUrl =
isFolder && this.filesStore.getItemUrl(id, isFolder, false, false);
const isEditing =
"fileStatus" in item && item.fileStatus === FileStatus.IsEditing;
const docUrl =
!canOpenPlayer &&
!isFolder &&
this.filesStore.getItemUrl(id, false, needConvert);
const href = isRecycleBinFolder
? null
: previewUrl || (!isFolder ? docUrl : folderUrl);
const isRoom = "roomType" in item && !!item.roomType;
const logo = "logo" in item ? item.logo : null;
const fileExst = "fileExst" in item ? item.fileExst : undefined;
const providerKey = "providerKey" in item ? item.providerKey : null;
const contentLength =
"contentLength" in item ? item.contentLength : undefined;
const roomType = "roomType" in item ? item.roomType : undefined;
const isArchive = "isArchive" in item ? item.isArchive : undefined;
const type = "type" in item ? item.type : undefined;
const icon =
isRoom && logo?.medium
? logo?.medium
: getIcon(
iconSize,
fileExst,
providerKey,
contentLength,
roomType,
isArchive,
type,
);
const defaultRoomIcon = isRoom
? getIcon(
iconSize,
fileExst,
providerKey,
contentLength,
roomType,
isArchive,
type,
)
: undefined;
const pluginOptions = {
fileTypeName: "",
isPlugin: false,
fileTileIcon: "",
};
if (enablePlugins && fileItemsList) {
fileItemsList.forEach(({ value }) => {
if (value.extension === fileExst) {
if (value.fileTypeName)
pluginOptions.fileTypeName = value.fileTypeName;
pluginOptions.isPlugin = true;
if (value.fileIconTile)
pluginOptions.fileTileIcon = value.fileIconTile;
}
});
}
const isForm = fileExst === ".oform";
const canCopyPublicLink =
access === ShareAccessRights.RoomManager ||
access === ShareAccessRights.None;
return {
...item,
access,
daysRemaining:
"autoDelete" in item &&
item.autoDelete &&
getDaysRemaining(item.autoDelete),
contentLength,
contextOptions,
fileExst,
icon,
defaultRoomIcon,
id,
isFolder,
logo,
rootFolderId,
providerKey,
canOpenPlayer,
previewUrl,
folderUrl,
href,
isThirdPartyFolder,
isEditing,
roomType,
isRoom,
isArchive,
thirdPartyIcon,
providerType,
...pluginOptions,
type,
isForm,
canCopyPublicLink,
};
});
};
get filesList() {
const newFolders = Array.from(this.folders.values());
newFolders.sort((a, b) => {
const firstValue = a.roomType ? 1 : 0;
const secondValue = b.roomType ? 1 : 0;
return secondValue - firstValue;
});
const items = [...newFolders, ...Array.from(this.files.values())];
if (items.length > 0 && this.isEmptyPage) {
this.setIsEmptyPage(false);
}
return this.getFilesListItems(items);
}
}
export default FilesListStore;

View File

@ -0,0 +1,35 @@
import { makeAutoObservable } from "mobx";
import { TFile, TFolder } from "@docspace/shared/api/files/types";
import { TRoom } from "@docspace/shared/api/rooms/types";
import { Nullable } from "@docspace/shared/types";
export type TSelection = (TFile | TFolder | TRoom)[];
export type TBufferSelection = Nullable<TFile | TFolder | TRoom>;
export type TSelected = "close" | "none";
class FilesSelectionStore {
selection: TSelection = [];
bufferSelection: TBufferSelection = null;
selected: TSelected = "close";
constructor() {
makeAutoObservable(this);
}
setSelection = (selection: TSelection) => {
this.selection = selection;
};
setBufferSelection = (bufferSelection: TBufferSelection) => {
this.bufferSelection = bufferSelection;
};
setSelected = (selected: TSelected) => {
this.selected = selected;
};
}
export default FilesSelectionStore;

View File

@ -0,0 +1,564 @@
/* eslint-disable no-console */
import { makeAutoObservable, runInAction } from "mobx";
import merge from "lodash/merge";
import cloneDeep from "lodash/cloneDeep";
import debounce from "lodash/debounce";
import api from "@docspace/shared/api";
import { TFile } from "@docspace/shared/api/files/types";
import { TOptSocket } from "@docspace/shared/utils/socket";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import { UserStore } from "@docspace/shared/store/UserStore";
import { Events, FileStatus } from "@docspace/shared/enums";
import { PDF_FORM_DIALOG_KEY } from "@docspace/shared/constants";
import FilesStore from "./FilesStore";
import ClientLoadingStore from "./ClientLoadingStore";
import SelectedFolderStore from "./SelectedFolderStore";
import TreeFoldersStore from "./TreeFoldersStore";
import InfoPanelStore from "./InfoPanelStore";
import FilesListStore from "./FilesListStore";
class FilesSocketStore {
constructor(
private settingsStore: Readonly<SettingsStore>,
private clientLoadingStore: Readonly<ClientLoadingStore>,
private selectedFolderStore: Readonly<SelectedFolderStore>,
private treeFoldersStore: Readonly<TreeFoldersStore>,
private infoPanelStore: Readonly<InfoPanelStore>,
private userStore: Readonly<UserStore>,
private filesListStore: Readonly<FilesListStore>,
private filesStore: Readonly<FilesStore>,
) {
makeAutoObservable(this);
const { socketHelper } = settingsStore;
socketHelper.on("s:modify-folder", async (opt) => {
const { socketSubscribers } = socketHelper;
if (opt && typeof opt !== "string" && opt.data) {
const data = JSON.parse(opt.data);
const pathParts = data.folderId
? `DIR-${data.folderId}`
: `DIR-${data.parentId}`;
if (
!socketSubscribers.has(pathParts) &&
!socketSubscribers.has(`DIR-${data.id}`)
) {
console.log("[WS] s:modify-folder: SKIP UNSUBSCRIBED", { data });
return;
}
}
console.log("[WS] s:modify-folder", opt);
if (
!(this.clientLoadingStore.isLoading || this.filesStore.operationAction)
) {
switch (typeof opt !== "string" && opt?.cmd) {
case "create":
this.wsModifyFolderCreate(opt);
break;
case "update":
this.wsModifyFolderUpdate(opt);
break;
case "delete":
this.wsModifyFolderDelete(opt);
break;
default:
break;
}
}
if (
typeof opt !== "string" &&
opt?.cmd &&
opt.id &&
(opt.type === "file" || opt.type === "folder") &&
(opt.cmd === "create" || opt.cmd === "delete")
) {
if (opt.type === "file") {
if (opt.cmd === "create") {
this.selectedFolderStore.increaseFilesCount();
} else {
this.selectedFolderStore.decreaseFilesCount();
}
} else if (opt.type === "folder")
if (opt.cmd === "create") {
this.selectedFolderStore.increaseFoldersCount();
} else {
this.selectedFolderStore.decreaseFoldersCount();
}
}
this.treeFoldersStore.updateTreeFoldersItem(opt);
});
socketHelper.on("s:update-history", (opt) => {
if (typeof opt === "string") return;
const { infoPanelSelection, fetchHistory } = this.infoPanelStore;
const { id, type } = opt;
let infoPanelSelectionType = "file";
if (infoPanelSelection?.isRoom || infoPanelSelection?.isFolder)
infoPanelSelectionType = "folder";
if (id === infoPanelSelection?.id && type === infoPanelSelectionType) {
console.log("[WS] s:update-history", id);
fetchHistory();
}
});
socketHelper.on("refresh-folder", (id) => {
const { socketSubscribers } = socketHelper;
const pathParts = `DIR-${id}`;
if (!socketSubscribers.has(pathParts)) return;
if (!id || this.clientLoadingStore.isLoading) return;
if (
this.selectedFolderStore.id?.toString() === id.toString() &&
this.settingsStore.withPaging // TODO: no longer deletes the folder in other tabs
) {
console.log("[WS] refresh-folder", id);
this.filesStore.fetchFiles(id, this.filesStore.filter);
}
});
socketHelper.on("s:markasnew-folder", (opt) => {
if (typeof opt === "string") return;
const { socketSubscribers } = socketHelper;
const { folderId, count } = opt;
const pathParts = `DIR-${folderId}`;
if (!socketSubscribers.has(pathParts)) return;
console.log(`[WS] markasnew-folder ${folderId}:${count}`);
const foundIndex =
folderId && this.filesStore.folders.findIndex((x) => x.id === folderId);
if (foundIndex === -1 || !foundIndex) return;
runInAction(() => {
this.filesStore.folders[foundIndex].new =
typeof count !== "undefined" && Number(count) >= 0 ? count : 0;
this.treeFoldersStore.fetchTreeFolders();
});
});
socketHelper.on("s:markasnew-file", (opt) => {
if (typeof opt === "string") return;
const { fileId, count } = opt;
if (!fileId) return;
const { socketSubscribers } = socketHelper;
const pathParts = `FILE-${fileId}`;
if (!socketSubscribers.has(pathParts)) return;
console.log(`[WS] markasnew-file ${fileId}:${count}`);
this.treeFoldersStore.fetchTreeFolders();
const fileStatus = this.filesListStore.files.get(fileId)?.fileStatus;
const status =
typeof count !== "undefined" && Number(count) > 0 && !fileStatus
? FileStatus.IsNew
: fileStatus === FileStatus.IsNew
? FileStatus.None
: fileStatus || FileStatus.None;
if (status !== fileStatus)
this.filesListStore.updateFileStatus(fileId, status);
});
// WAIT FOR RESPONSES OF EDITING FILE
socketHelper.on("s:start-edit-file", (id) => {
if (typeof id !== "string") return;
const { socketSubscribers } = socketHelper;
const pathParts = `FILE-${id}`;
if (!socketSubscribers.has(pathParts)) return;
console.log(`[WS] s:start-edit-file`, id);
const fileStatus = this.filesListStore.files.get(id)?.fileStatus;
this.filesStore.updateSelectionStatus(
id,
fileStatus || FileStatus.IsEditing,
true,
);
this.filesListStore.updateFileStatus(
id,
fileStatus || FileStatus.IsEditing,
);
});
socketHelper.on("s:modify-room", (option) => {
if (typeof option === "string") return;
switch (option.cmd) {
case "create-form":
this.wsCreatedPDFForm(option);
break;
default:
break;
}
});
socketHelper.on("s:stop-edit-file", (id) => {
if (typeof id !== "string") return;
const { socketSubscribers } = socketHelper;
const pathParts = `FILE-${id}`;
const { isVisible, infoPanelSelection, setInfoPanelSelection } =
this.infoPanelStore;
if (!socketSubscribers.has(pathParts)) return;
console.log(`[WS] s:stop-edit-file`, id);
const currFile = this.filesListStore.files.get(id);
const fileStatus = currFile?.fileStatus;
const status =
fileStatus === FileStatus.IsEditing
? FileStatus.None
: fileStatus || FileStatus.None;
this.filesStore.updateSelectionStatus(id, status, false);
this.filesListStore.updateFileStatus(id, status);
this.filesStore.getFileInfo(id).then((file) => {
if (
isVisible &&
file.id === infoPanelSelection?.id &&
infoPanelSelection?.fileExst === file.fileExst
) {
setInfoPanelSelection(merge(cloneDeep(infoPanelSelection), file));
}
});
this.filesStore.createThumbnail(currFile);
});
this.filesStore.createNewFilesQueue.on("resolve", this.onResolveNewFile);
}
wsModifyFolderCreate = async (opt: TOptSocket | string) => {
if (typeof opt === "string") return;
if (opt?.type === "file" && opt?.id && opt.data) {
const curFile = this.filesListStore.files.get(opt.id);
const file = JSON.parse(opt?.data);
if (this.selectedFolderStore.id !== file.folderId) {
const folder = this.filesListStore.folders.get(file.folderId);
if (folder)
this.filesListStore.folders.set(folder.id, {
...folder,
filesCount: folder.filesCount + 1,
});
return;
}
// To update a file version
if (curFile && !this.settingsStore.withPaging) {
if (
curFile.version !== file.version ||
curFile.versionGroup !== file.versionGroup
) {
curFile.version = file.version;
curFile.versionGroup = file.versionGroup;
}
this.filesStore.checkSelection(file);
}
if (curFile) return;
setTimeout(() => {
const foundFile = this.filesListStore.files.get(file.id);
if (foundFile) {
// console.log("Skip in timeout");
return null;
}
this.filesStore.createNewFilesQueue.enqueue(() => {
const foundedFile = this.filesListStore.files.get(file.id);
if (foundedFile) {
// console.log("Skip in queue");
return null;
}
return api.files.getFileInfo(file.id);
});
}, 300);
} else if (opt?.type === "folder" && opt?.id && opt?.data) {
const curFolder = this.filesListStore.folders.get(opt.id);
if (curFolder) return;
const folder = JSON.parse(opt?.data);
if (
this.selectedFolderStore.id?.toString() !== folder.parentId.toString()
) {
const parentFolder = this.filesListStore.folders.get(folder?.parentId);
if (parentFolder)
this.filesListStore.folders.set(parentFolder.id, {
...parentFolder,
filesCount: folder.filesCount + 1,
});
}
if (
this.selectedFolderStore.id !== folder.parentId ||
(folder.roomType &&
folder.createdBy.id === this.userStore?.user?.id &&
this.filesStore.roomCreated)
) {
return this.filesStore.setRoomCreated(false);
}
const folderInfo = await api.files.getFolderInfo(folder.id);
console.log("[WS] create new folder", folderInfo.id, folderInfo.title);
const newFolders = new Map([
[folder.id, folderInfo],
...this.filesListStore.folders.entries(),
]);
if (
this.filesListStore.folders.size > this.filesStore.filter.pageCount &&
this.settingsStore.withPaging
) {
this.filesListStore.removeFolder(Array.from(newFolders.keys()).pop()); // Remove last
}
const newFilter = this.filesStore.filter;
newFilter.total += 1;
runInAction(() => {
this.filesStore.setFilter(newFilter);
this.filesListStore.addFolder(folderInfo);
});
}
};
wsModifyFolderUpdate = (opt: TOptSocket | string) => {
if (typeof opt === "string") return;
if (opt?.type === "file" && opt?.data) {
const file = JSON.parse(opt?.data);
if (!file || !file.id) return;
this.filesStore.getFileInfo(file.id); // this.setFile(file);
console.log("[WS] update file", file.id, file.title);
this.filesStore.checkSelection(file);
} else if (opt?.type === "folder" && opt?.data) {
const folder = JSON.parse(opt?.data);
if (!folder || !folder.id) return;
api.files
.getFolderInfo(folder.id)
.then(this.filesListStore.setFolder)
.catch(() => {
// console.log("Folder deleted")
});
console.log("[WS] update folder", folder.id, folder.title);
if (this.filesStore.selection?.length) {
const foundIndex = this.filesStore.selection?.findIndex(
(x) => x.id === folder.id,
);
if (foundIndex > -1) {
runInAction(() => {
this.filesStore.selection[foundIndex] = folder;
});
}
}
if (this.filesStore.bufferSelection) {
const foundIndex = [this.filesStore.bufferSelection].findIndex(
(x) => x.id === folder.id,
);
if (foundIndex > -1) {
runInAction(() => {
this.filesStore.bufferSelection[foundIndex] = folder;
});
}
}
if (folder.id === this.selectedFolderStore.id) {
this.selectedFolderStore.setSelectedFolder({ ...folder });
}
}
};
wsModifyFolderDelete = (opt: TOptSocket | string) => {
if (typeof opt === "string") return;
if (opt?.type === "file" && opt?.id) {
const foundIndex = this.filesStore.files.findIndex(
(x) => x.id === opt?.id,
);
if (foundIndex === -1) return;
console.log(
"[WS] delete file",
this.filesStore.files[foundIndex].id,
this.filesStore.files[foundIndex].title,
);
const tempActionFilesIds = JSON.parse(
JSON.stringify(this.filesStore.tempActionFilesIds),
);
tempActionFilesIds.push(this.filesStore.files[foundIndex].id);
this.filesStore.setTempActionFilesIds(tempActionFilesIds);
this.debounceRemoveFiles();
// Hide pagination when deleting files
runInAction(() => {
this.filesStore.isHidePagination = true;
});
runInAction(() => {
if (
this.filesStore.files.length === 0 &&
this.filesStore.folders.length === 0 &&
this.filesStore.pageItemsLength > 1
) {
this.filesStore.isLoadingFilesFind = true;
}
});
} else if (opt?.type === "folder" && opt?.id) {
const foundIndex = this.filesStore.folders.findIndex(
(x) => x.id === opt?.id,
);
if (foundIndex == -1) return;
console.log(
"[WS] delete folder",
this.filesStore.folders[foundIndex].id,
this.filesStore.folders[foundIndex].title,
);
const tempActionFoldersIds = JSON.parse(
JSON.stringify(this.filesStore.tempActionFoldersIds),
);
tempActionFoldersIds.push(this.filesStore.folders[foundIndex].id);
this.filesStore.setTempActionFoldersIds(tempActionFoldersIds);
this.debounceRemoveFolders();
runInAction(() => {
this.filesStore.isHidePagination = true;
});
runInAction(() => {
if (
this.filesStore.files.length === 0 &&
this.filesStore.folders.length === 0 &&
this.filesStore.pageItemsLength > 1
) {
this.filesStore.isLoadingFilesFind = true;
}
});
}
};
wsCreatedPDFForm = (option: TOptSocket) => {
if (!option.data) return;
const file = JSON.parse(option.data);
if (this.selectedFolderStore.id !== file.folderId) return;
const localKey = `${PDF_FORM_DIALOG_KEY}-${this.userStore?.user?.id}`;
const isFirst = JSON.parse(localStorage.getItem(localKey) ?? "true");
const event = new CustomEvent(Events.CREATE_PDF_FORM_FILE, {
detail: {
file,
isFill: !option.isOneMember,
isFirst,
},
});
if (isFirst) localStorage.setItem(localKey, "false");
window?.dispatchEvent(event);
};
onResolveNewFile = (fileInfo: TFile) => {
if (!fileInfo) return;
if (this.filesStore.files.findIndex((x) => x.id === fileInfo.id) > -1)
return;
if (this.selectedFolderStore.id !== fileInfo.folderId) return;
console.log("[WS] create new file", { fileInfo });
const newFiles = [fileInfo, ...this.filesStore.files];
if (
newFiles.length > this.filesStore.filter.pageCount &&
this.settingsStore.withPaging
) {
newFiles.pop(); // Remove last
}
const newFilter = this.filesStore.filter;
newFilter.total += 1;
runInAction(() => {
this.filesStore.setFilter(newFilter);
this.filesStore.setFiles(newFiles);
});
this.debounceFetchTreeFolders();
};
debounceFetchTreeFolders = debounce(() => {
this.treeFoldersStore.fetchTreeFolders();
}, 1000);
debounceRemoveFiles = debounce(() => {
this.filesStore.removeFiles(this.filesStore.tempActionFilesIds);
}, 1000);
debounceRemoveFolders = debounce(() => {
this.filesStore.removeFiles(null, this.filesStore.tempActionFoldersIds);
}, 1000);
}
export default FilesSocketStore;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,740 @@
import { makeAutoObservable, runInAction } from "mobx";
import {
addClient,
updateClient,
changeClientStatus,
regenerateSecret,
deleteClient,
getClientList,
getScopeList,
getConsentList,
revokeUserClient,
} from "@docspace/shared/api/oauth";
import {
IClientListProps,
IClientProps,
IClientReqDTO,
TScope,
} from "@docspace/shared/utils/oauth/types";
import { toastr } from "@docspace/shared/components/toast";
import { AuthenticationMethod } from "@docspace/shared/enums";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { UserStore } from "@docspace/shared/store/UserStore";
import { Nullable, TTranslation } from "@docspace/shared/types";
import EnableReactSvgUrl from "PUBLIC_DIR/images/enable.react.svg?url";
import RemoveReactSvgUrl from "PUBLIC_DIR/images/remove.react.svg?url";
import PencilReactSvgUrl from "PUBLIC_DIR/images/pencil.react.svg?url";
import CodeReactSvgUrl from "PUBLIC_DIR/images/code.react.svg?url";
import ExternalLinkReactSvgUrl from "PUBLIC_DIR/images/external.link.react.svg?url";
import OauthRevokeSvgUrl from "PUBLIC_DIR/images/oauth.revoke.svg?url";
import SettingsIcon from "PUBLIC_DIR/images/catalog.settings.react.svg?url";
import DeleteIcon from "PUBLIC_DIR/images/delete.react.svg?url";
const PAGE_LIMIT = 100;
export type ViewAsType = "table" | "row";
export interface OAuthStoreProps {
isInit: boolean;
setIsInit: (value: boolean) => void;
viewAs: ViewAsType;
setViewAs: (value: ViewAsType) => void;
infoDialogVisible: boolean;
setInfoDialogVisible: (value: boolean) => void;
revokeDialogVisible: boolean;
setRevokeDialogVisible: (value: boolean) => void;
previewDialogVisible: boolean;
setPreviewDialogVisible: (value: boolean) => void;
disableDialogVisible: boolean;
setDisableDialogVisible: (value: boolean) => void;
resetDialogVisible: boolean;
setResetDialogVisible: (value: boolean) => void;
deleteDialogVisible: boolean;
setDeleteDialogVisible: (value: boolean) => void;
clientsIsLoading: boolean;
setClientsIsLoading: (value: boolean) => void;
consentsIsLoading: boolean;
setConsentsIsLoading: (value: boolean) => void;
clientSecret: string;
setClientSecret: (value: string) => void;
editClient: (clientId: string) => void;
clients: IClientProps[];
fetchClients: () => Promise<void>;
fetchNextClients: (startIndex: number) => Promise<void>;
consents: IClientProps[];
fetchConsents: () => Promise<void>;
fetchNextConsents: (startIndex: number) => Promise<void>;
saveClient: (client: IClientReqDTO) => Promise<void>;
updateClient: (clientId: string, client: IClientReqDTO) => Promise<void>;
changeClientStatus: (clientId: string, status: boolean) => Promise<void>;
regenerateSecret: (clientId: string) => Promise<string | undefined>;
deleteClient: (clientId: string[]) => Promise<void>;
revokeClient: (clientId: string[]) => Promise<void>;
userStore: Nullable<UserStore>;
currentPage: number;
nextPage: Nullable<number>;
itemCount: number;
consentCurrentPage: number;
consentNextPage: Nullable<number>;
consentItemCount: number;
selection: string[];
setSelection: (clientId: string) => void;
bufferSelection: IClientProps | null;
setBufferSelection: (clientId: string) => void;
activeClients: string[];
setActiveClient: (clientId: string) => void;
scopes: TScope[];
fetchScopes: () => Promise<void>;
getContextMenuItems: (
t: TTranslation,
item: IClientProps,
isInfo?: boolean,
isSettings?: boolean,
) => ContextMenuModel[];
clientList: IClientProps[];
isEmptyClientList: boolean;
hasNextPage: boolean;
consentHasNextPage: boolean;
scopeList: TScope[];
}
class OAuthStore implements OAuthStoreProps {
userStore: Nullable<UserStore> = null;
viewAs: ViewAsType = "table";
currentPage: number = 0;
nextPage: Nullable<number> = null;
itemCount: number = 0;
consentCurrentPage: number = 0;
consentNextPage: Nullable<number> = null;
consentItemCount: number = 0;
infoDialogVisible: boolean = false;
previewDialogVisible: boolean = false;
disableDialogVisible: boolean = false;
deleteDialogVisible: boolean = false;
resetDialogVisible: boolean = false;
selection: string[] = [];
bufferSelection: IClientProps | null = null;
clients: IClientProps[] = [];
activeClients: string[] = [];
scopes: TScope[] = [];
clientsIsLoading: boolean = true;
consentsIsLoading: boolean = true;
clientSecret: string = "";
consents: IClientProps[] = [];
isInit: boolean = false;
revokeDialogVisible: boolean = false;
constructor(userStore: UserStore) {
this.userStore = userStore;
makeAutoObservable(this);
}
setRevokeDialogVisible = (value: boolean) => {
this.revokeDialogVisible = value;
};
setIsInit = (value: boolean) => {
this.isInit = value;
};
setViewAs = (value: ViewAsType) => {
this.viewAs = value;
};
setInfoDialogVisible = (value: boolean) => {
this.infoDialogVisible = value;
};
setPreviewDialogVisible = (value: boolean) => {
this.previewDialogVisible = value;
};
setDisableDialogVisible = (value: boolean) => {
this.disableDialogVisible = value;
};
setDeleteDialogVisible = (value: boolean) => {
this.deleteDialogVisible = value;
};
setResetDialogVisible = (value: boolean) => {
this.resetDialogVisible = value;
};
setClientSecret = (value: string) => {
this.clientSecret = value;
};
setSelection = (clientId?: string) => {
if (!clientId) {
this.selection = [];
} else if (this.selection.includes(clientId)) {
this.selection = this.selection.filter((s) => s !== clientId);
} else {
this.selection.push(clientId);
}
};
setBufferSelection = (clientId: string) => {
const client = this.clients.find((c) => c.clientId === clientId);
if (client) {
this.bufferSelection = { ...client, scopes: [...client.scopes] };
} else {
const consent = this.consents.find((c) => c.clientId === clientId);
if (consent)
this.bufferSelection = { ...consent, scopes: [...consent.scopes] };
}
};
setClientsIsLoading = (value: boolean) => {
this.clientsIsLoading = value;
};
setConsentsIsLoading = (value: boolean) => {
this.consentsIsLoading = value;
};
setActiveClient = (clientId?: string) => {
if (!clientId) {
this.activeClients = [];
} else if (this.activeClients.includes(clientId)) {
this.activeClients = this.activeClients.filter((s) => s !== clientId);
} else {
this.activeClients.push(clientId);
}
};
editClient = (clientId: string) => {
this.setInfoDialogVisible(false);
this.setPreviewDialogVisible(false);
window?.DocSpace?.navigate(
`/portal-settings/developer-tools/oauth/${clientId}`,
);
};
fetchClients = async () => {
try {
this.setClientsIsLoading(true);
const clientList: IClientListProps = await getClientList(0, PAGE_LIMIT);
runInAction(() => {
this.clients = [...clientList.data];
this.selection = [];
this.currentPage = clientList.page;
this.nextPage = clientList.next;
if (clientList.next) {
this.itemCount = clientList.data.length + 2;
} else {
this.itemCount = clientList.data.length;
}
});
this.setClientsIsLoading(false);
} catch (e) {
const err = e as TData;
toastr.error(err);
}
};
fetchNextClients = async (startIndex: number) => {
if (this.clientsIsLoading) return;
this.setClientsIsLoading(true);
const page = startIndex / PAGE_LIMIT;
runInAction(() => {
this.currentPage = page + 1;
});
const clientList: IClientListProps = await getClientList(
this.nextPage || page,
PAGE_LIMIT,
);
runInAction(() => {
this.currentPage = clientList.page;
this.nextPage = clientList.next || null;
this.clients = [...this.clients, ...clientList.data];
this.itemCount += clientList.data.length;
});
this.setClientsIsLoading(false);
};
fetchConsents = async () => {
try {
this.setClientsIsLoading(true);
const consentList = await getConsentList(0, PAGE_LIMIT);
runInAction(() => {
this.consents = [...consentList.consents];
this.selection = [];
this.consentCurrentPage = consentList.page;
this.consentNextPage = consentList.next;
if (consentList.next) {
this.consentItemCount = consentList.data.length + 2;
} else {
this.consentItemCount = consentList.data.length;
}
});
this.setClientsIsLoading(false);
} catch (e) {
const err = e as TData;
toastr.error(err);
}
};
fetchNextConsents = async (startIndex: number) => {
if (this.consentsIsLoading) return;
this.setConsentsIsLoading(true);
const page = startIndex / PAGE_LIMIT;
runInAction(() => {
this.consentCurrentPage = page + 1;
});
const consentList = await getConsentList(this.nextPage || page, PAGE_LIMIT);
runInAction(() => {
this.currentPage = consentList.page;
this.nextPage = consentList.next || null;
this.consents = [...this.consents, ...consentList.consents];
this.consentItemCount += consentList.data.length;
});
this.setConsentsIsLoading(false);
};
saveClient = async (client: IClientReqDTO) => {
try {
const newClient = await addClient(client);
const creatorDisplayName = this.userStore?.user?.displayName;
const creatorAvatar = this.userStore?.user?.avatarSmall;
runInAction(() => {
this.clients = [
{ ...newClient, enabled: true, creatorDisplayName, creatorAvatar },
...this.clients,
];
});
} catch (e) {
const err = e as TData;
toastr.error(err);
}
};
updateClient = async (clientId: string, client: IClientReqDTO) => {
try {
await updateClient(clientId, client);
const idx = this.clients.findIndex((c) => c.clientId === clientId);
const newClient = { ...this.clients[idx] };
newClient.name = client.name;
newClient.allowedOrigins = client.allowed_origins;
newClient.logo = client.logo;
newClient.description = client.description;
newClient.isPublic = client.is_public;
if (
client.allow_pkce &&
!newClient.authenticationMethods.includes(AuthenticationMethod.none)
)
newClient.authenticationMethods.push(AuthenticationMethod.none);
if (idx > -1) {
runInAction(() => {
this.clients[idx] = {
...newClient,
};
});
}
} catch (e) {
const err = e as TData;
toastr.error(err);
}
};
changeClientStatus = async (clientId: string, status: boolean) => {
try {
await changeClientStatus(clientId, status);
const idx = this.clients.findIndex((c) => c.clientId === clientId);
if (idx > -1) {
runInAction(() => {
this.clients[idx] = { ...this.clients[idx], enabled: status };
});
}
} catch (e) {
const err = e as TData;
toastr.error(err);
}
};
regenerateSecret = async (clientId: string) => {
try {
const { client_secret: clientSecret } = await regenerateSecret(clientId);
this.setClientSecret(clientSecret);
return clientSecret;
} catch (e) {
const err = e as TData;
toastr.error(err);
}
};
deleteClient = async (clientsId: string[]) => {
try {
const requests: Promise<void>[] = [];
clientsId.forEach((id) => {
this.setActiveClient(id);
requests.push(deleteClient(id));
});
await Promise.all(requests);
runInAction(() => {
this.clients = this.clients.filter(
(c) => !clientsId.includes(c.clientId),
);
});
this.setActiveClient("");
} catch (e) {
const err = e as TData;
toastr.error(err);
}
};
fetchScopes = async () => {
try {
const scopes = await getScopeList();
this.scopes = scopes;
} catch (e) {
const err = e as TData;
toastr.error(err);
}
};
revokeClient = async (clientsId: string[]) => {
try {
const requests: Promise<void>[] = [];
clientsId.forEach((id) => {
this.setActiveClient(id);
requests.push(revokeUserClient(id));
});
await Promise.all(requests);
runInAction(() => {
this.consents = this.consents.filter(
(c) => !clientsId.includes(c.clientId),
);
});
this.setActiveClient("");
} catch (e) {
const err = e as TData;
toastr.error(err);
}
};
getContextMenuItems = (
t: TTranslation,
item: IClientProps,
isInfo?: boolean,
isSettings: boolean = true,
) => {
const { clientId } = item;
const isGroupContext = this.selection.length > 1;
const onShowInfo = () => {
this.setBufferSelection(clientId);
this.setPreviewDialogVisible(false);
this.setInfoDialogVisible(true);
this.setDisableDialogVisible(false);
this.setDeleteDialogVisible(false);
};
const onRevoke = () => {
if (!isGroupContext) this.setBufferSelection(clientId);
this.setPreviewDialogVisible(false);
this.setInfoDialogVisible(false);
this.setRevokeDialogVisible(true);
this.setDisableDialogVisible(false);
this.setDeleteDialogVisible(false);
};
const onDisable = () => {
this.setBufferSelection(clientId);
this.setPreviewDialogVisible(false);
this.setInfoDialogVisible(false);
this.setRevokeDialogVisible(false);
this.setDisableDialogVisible(true);
this.setDeleteDialogVisible(false);
};
const openOption = {
key: "open",
icon: ExternalLinkReactSvgUrl,
label: t("Files:Open"),
onClick: () => window.open(item.websiteUrl, "_blank"),
isDisabled: isInfo,
};
const infoOption = {
key: "info",
icon: SettingsIcon,
label: t("Common:Info"),
onClick: onShowInfo,
isDisabled: isInfo,
};
const revokeOptions = [
{
key: "revoke",
icon: OauthRevokeSvgUrl,
label: t("Revoke"),
onClick: onRevoke,
isDisabled: false,
},
];
if (!isSettings) {
const items: ContextMenuModel[] = [];
if (!isGroupContext) {
items.push(openOption);
if (!isInfo) items.push(infoOption);
items.push({
key: "separator",
isSeparator: true,
});
}
items.push(...revokeOptions);
return items;
}
const onDelete = () => {
this.setBufferSelection(clientId);
this.setPreviewDialogVisible(false);
this.setInfoDialogVisible(false);
this.setRevokeDialogVisible(false);
this.setDisableDialogVisible(false);
this.setDeleteDialogVisible(true);
};
const onShowPreview = () => {
this.setBufferSelection(clientId);
this.setPreviewDialogVisible(true);
this.setInfoDialogVisible(false);
this.setRevokeDialogVisible(false);
this.setDisableDialogVisible(false);
this.setDeleteDialogVisible(false);
};
const onEnable = async (status: boolean) => {
this.setPreviewDialogVisible(false);
this.setInfoDialogVisible(false);
this.setRevokeDialogVisible(false);
this.setDisableDialogVisible(false);
this.setDeleteDialogVisible(false);
if (isGroupContext) {
try {
const actions: Promise<void>[] = [];
this.selection.forEach((s) => {
this.setActiveClient(s);
actions.push(this.changeClientStatus(s, status));
});
await Promise.all(actions);
this.setActiveClient("");
this.setSelection("");
} catch (e) {
const err = e as TData;
toastr.error(err);
}
} else {
this.setActiveClient(clientId);
await this.changeClientStatus(clientId, status);
this.setActiveClient("");
this.setSelection("");
// TODO OAuth, show toast
}
};
const editOption = {
key: "edit",
icon: PencilReactSvgUrl,
label: t("Common:Edit"),
onClick: () => this.editClient(clientId),
};
const authButtonOption = {
key: "auth-button",
icon: CodeReactSvgUrl,
label: t("AuthButton"),
onClick: onShowPreview,
};
const enableOption = {
key: "enable",
icon: EnableReactSvgUrl,
label: t("Common:Enable"),
onClick: () => onEnable(true),
};
const disableOption = {
key: "disable",
icon: RemoveReactSvgUrl,
label: t("Common:Disable"),
onClick: onDisable,
};
const contextOptions = [
{
key: "Separator dropdownItem",
isSeparator: true,
},
{
key: "delete",
label: t("Common:Delete"),
icon: DeleteIcon,
onClick: () => onDelete(),
},
];
if (isGroupContext) {
let enabled = false;
this.selection.forEach((s) => {
enabled =
enabled ||
this.clientList.find((client) => client.clientId === s)?.enabled ||
false;
});
if (enabled) {
contextOptions.unshift(disableOption);
} else {
contextOptions.unshift(enableOption);
}
} else {
if (item.enabled) {
contextOptions.unshift(disableOption);
} else {
contextOptions.unshift(enableOption);
}
if (!isInfo) contextOptions.unshift(infoOption);
contextOptions.unshift(authButtonOption);
contextOptions.unshift(editOption);
}
return contextOptions;
};
get clientList() {
return this.clients;
}
get isEmptyClientList() {
return this.clientList.length === 0;
}
get hasNextPage() {
return !!this.nextPage;
}
get consentHasNextPage() {
return !!this.consentNextPage;
}
get scopeList() {
return this.scopes;
}
}
export default OAuthStore;

View File

@ -45,13 +45,13 @@ import { TLogo, TRoomSecurity } from "@docspace/shared/api/rooms/types";
import { setDocumentTitle } from "../helpers/utils";
export type TNavigationPath = {
id: number;
id: number | string;
title: string;
isRoom: boolean;
roomType: RoomsType;
isRootRoom: boolean;
shared: boolean;
canCopyPublicLink: boolean;
roomType?: RoomsType;
isRootRoom?: boolean;
shared?: boolean;
canCopyPublicLink?: boolean;
};
type ExcludeTypes = SettingsStore | Function;
@ -115,6 +115,8 @@ class SelectedFolderStore {
isRoom = false;
inRoom = false;
isArchive = false;
logo: TLogo | null = null;
@ -127,9 +129,7 @@ class SelectedFolderStore {
security: TFolderSecurity | TRoomSecurity | null = null;
type = null;
inRoom = false;
type: FolderType | null = null;
isFolder = true;
@ -349,6 +349,22 @@ class SelectedFolderStore {
});
}
};
increaseFilesCount = () => {
this.filesCount += 1;
};
decreaseFilesCount = () => {
this.filesCount -= 1;
};
increaseFoldersCount = () => {
this.foldersCount += 1;
};
decreaseFoldersCount = () => {
this.foldersCount -= 1;
};
}
export default SelectedFolderStore;

View File

@ -297,4 +297,4 @@ class ThirdPartyStore {
}
}
export default new ThirdPartyStore();
export default ThirdPartyStore;

View File

@ -50,7 +50,7 @@ import LdapFormStore from "./LdapFormStore";
import FilesStore from "./FilesStore";
import SelectedFolderStore from "./SelectedFolderStore";
import TreeFoldersStore from "./TreeFoldersStore";
import thirdPartyStore from "./ThirdPartyStore";
import ThirdPartyStore from "./ThirdPartyStore";
import FilesSettingsStore from "./FilesSettingsStore";
import FilesActionsStore from "./FilesActionsStore";
import MediaViewerDataStore from "./MediaViewerDataStore";
@ -81,6 +81,13 @@ import PluginStore from "./PluginStore";
import InfoPanelStore from "./InfoPanelStore";
import CampaignsStore from "./CampaignsStore";
import OAuthStore from "./OAuthStore";
import FilesSocketStore from "./FilesSocketStore";
const thirdPartyStore = new ThirdPartyStore();
const oauthStore = new OAuthStore(userStore);
const selectedFolderStore = new SelectedFolderStore(settingsStore);
const pluginStore = new PluginStore(
@ -153,6 +160,16 @@ const filesStore = new FilesStore(
settingsStore,
);
const filesSocketStore = new FilesSocketStore(
settingsStore,
clientLoadingStore,
selectedFolderStore,
treeFoldersStore,
infoPanelStore,
userStore,
filesStore,
);
const mediaViewerDataStore = new MediaViewerDataStore(
filesStore,
publicRoomStore,
@ -320,6 +337,7 @@ const store = {
profileActionsStore,
filesStore,
filesSocketStore,
filesSettingsStore,
mediaViewerDataStore,
@ -348,6 +366,7 @@ const store = {
clientLoadingStore,
publicRoomStore,
oauthStore,
pluginStore,
storageManagement,
campaignsStore,

View File

@ -25,7 +25,7 @@
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react": "^18.2.53",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.0.4",

View File

@ -27,9 +27,9 @@
declare module "*.ico?url" {
const content: string;
export default content;
}
}
declare module "*.svg?url" {
const content: string;
export default content;
}
}

View File

@ -27,7 +27,7 @@
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react": "^18.2.53",
"@types/react-dom": "^18",
"@types/react-google-recaptcha": "^2.1.9",
"babel-plugin-styled-components": "^2.1.4",

View File

@ -0,0 +1,8 @@
{
"Consent": "Consent",
"ConsentSubHeader": "{{name}} would like the ability to access the following data in <strong>your DocSpace account</strong>:",
"ConsentDescription": "Data shared with <strong>{{displayName}}</strong> will be governed by <strong>{{nameApp}}</strong> <6>privacy policy</6> and <6>terms of service</6>. You can revoke this consent at any time in your DocSpace account settings.",
"ToContinue": "To continue to",
"SignedInAs": "Signed in as",
"NotYou": "Not you?"
}

View File

@ -16,5 +16,8 @@
"SsoSettingsDisabled": "Single sign-on is disabled",
"SsoSettingsEmptyToken": "Authentication token could not be found",
"SsoSettingsNotValidToken": "Invalid authentication token",
"SsoSettingsUserTerminated": "This user is disabled"
"SsoSettingsUserTerminated": "This user is disabled",
"OAuthApplicationEmpty": "Application could not be found",
"OAuthApplicationDisabled": "This application is disabled",
"OAuthClientEmpty": "Client id could not be found"
}

View File

@ -0,0 +1,4 @@
{
"BackToSignIn": "Back to sign in",
"MorePortals": "You have more than one accounts. Please choose one of them"
}

View File

@ -0,0 +1,52 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { IClientProps } from "@docspace/shared/utils/oauth/types";
import Consent from "@/components/Consent";
import { getOAuthClient, getScopeList, getUser } from "@/utils/actions";
async function Page({
searchParams,
}: {
searchParams: { [key: string]: string };
}) {
const clientId = searchParams.clientId ?? searchParams.client_id;
const [client, scopes, user] = await Promise.all([
getOAuthClient(clientId),
getScopeList(),
getUser(),
]);
if (!client || (client && !("clientId" in client)) || !scopes || !user)
return "";
return (
<Consent client={client as IClientProps} scopes={scopes} user={user} />
);
}
export default Page;

View File

@ -24,6 +24,7 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { cookies } from "next/headers";
import dynamic from "next/dynamic";
@ -82,7 +83,7 @@ export default async function Layout({
<GreetingContainer
greetingSettings={objectSettings?.greetingSettings}
/>
<FormWrapper id="login-form">{children}</FormWrapper>
{children}
</ColorTheme>
</LoginContent>
</Scrollbar>

View File

@ -24,29 +24,42 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { getSettings } from "@/utils/actions";
import { getOAuthClient, getSettings } from "@/utils/actions";
import Login from "@/components/Login";
import LoginForm from "@/components/LoginForm";
import ThirdParty from "@/components/ThirdParty";
import RecoverAccess from "@/components/RecoverAccess";
import Register from "@/components/Register";
import { FormWrapper } from "@docspace/shared/components/form-wrapper";
async function Page() {
const settings = await getSettings();
async function Page({
searchParams,
}: {
searchParams: { [key: string]: string };
}) {
const clientId = searchParams.client_id;
const [settings, client] = await Promise.all([
getSettings(),
clientId ? getOAuthClient(clientId) : undefined,
]);
return (
<FormWrapper id="login-form">
<Login>
{settings && typeof settings !== "string" && (
<>
<LoginForm
hashSettings={settings?.passwordHash}
cookieSettingsEnabled={settings?.cookieSettingsEnabled}
clientId={clientId}
client={client}
reCaptchaPublicKey={settings?.recaptchaPublicKey}
reCaptchaType={settings?.recaptchaType}
/>
<ThirdParty />
{!clientId && <ThirdParty />}
{settings.enableAdmMess && <RecoverAccess />}
{settings.enabledJoin && (
{settings.enabledJoin && !clientId && (
<Register
id="login_register"
enabledJoin
@ -58,6 +71,7 @@ async function Page() {
</>
)}
</Login>
</FormWrapper>
);
}

View File

@ -0,0 +1,23 @@
import TenantList from "@/components/TenantList";
import { getSettings } from "@/utils/actions";
export default async function Page({
searchParams,
}: {
searchParams: { [key: string]: string };
}) {
const settings = await getSettings();
const { portals } = JSON.parse(searchParams.portals);
const clientId = searchParams.clientId;
if (typeof settings !== "object") return;
return (
<TenantList
portals={portals}
clientId={clientId}
baseDomain={settings.baseDomain}
/>
);
}

View File

@ -33,7 +33,7 @@ import { LANGUAGE, SYSTEM_THEME_KEY } from "@docspace/shared/constants";
import StyledComponentsRegistry from "@/utils/registry";
import { Providers } from "@/providers";
import { getColorTheme, getSettings } from "@/utils/actions";
import { getColorTheme, getConfig, getSettings } from "@/utils/actions";
import "../styles/globals.scss";
@ -62,9 +62,10 @@ export default async function RootLayout({
const startOtherOperationsDate = new Date();
const [settings, colorTheme] = await Promise.all([
const [settings, colorTheme, config] = await Promise.all([
getSettings(),
getColorTheme(),
getConfig(),
]);
timers.otherOperations =
@ -73,6 +74,7 @@ export default async function RootLayout({
if (settings === "access-restricted") redirectUrl = `/${settings}`;
if (settings === "portal-not-found") {
const hdrs = headers();
const config = await (
await fetch(`${baseUrl}/static/scripts/config.json`)
).json();
@ -132,7 +134,6 @@ export default async function RootLayout({
systemTheme: systemTheme?.value as ThemeKeys,
}}
redirectURL={redirectUrl}
timers={timers}
>
<Toast isSSR />
{children}

View File

@ -0,0 +1,251 @@
/* eslint-disable @next/next/no-img-element */
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
"use client";
import React from "react";
import styled from "styled-components";
import { useTranslation, Trans } from "react-i18next";
import ScopeList from "@docspace/shared/utils/oauth/ScopeList";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { Text } from "@docspace/shared/components/text";
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
import {
Avatar,
AvatarRole,
AvatarSize,
} from "@docspace/shared/components/avatar";
import { deleteCookie } from "@docspace/shared/utils/cookie";
import { IClientProps, TScope } from "@docspace/shared/utils/oauth/types";
import { TUser } from "@docspace/shared/api/people/types";
import api from "@docspace/shared/api";
import OAuthClientInfo from "./ConsentInfo";
import { useRouter } from "next/navigation";
import { FormWrapper } from "@docspace/shared/components/form-wrapper";
const StyledButtonContainer = styled.div`
margin-top: 32px;
margin-bottom: 16px;
width: 100%;
display: flex;
flex-direction: row;
gap: 8px;
`;
const StyledDescriptionContainer = styled.div`
width: 100%;
margin-bottom: 16px;
p {
width: 100%;
}
`;
const StyledUserContainer = styled.div`
width: 100%;
padding-top: 16px;
border-top: 1px solid
${(props) => props.theme.oauth.infoDialog.separatorColor};
.block {
height: 40px;
display: flex;
align-items: center;
gap: 8px;
}
`;
interface IConsentProps {
client: IClientProps;
scopes: TScope[];
user: TUser;
}
const Consent = ({ client, scopes, user }: IConsentProps) => {
const { t } = useTranslation(["Consent", "Common"]);
const router = useRouter();
const [isAllowRunning, setIsAllowRunning] = React.useState(false);
const [isDenyRunning, setIsDenyRunning] = React.useState(false);
const onAllowClick = async () => {
if (!("clientId" in client)) return;
if (isAllowRunning || isDenyRunning) return;
setIsAllowRunning(true);
const clientId = client.clientId;
let clientState = "";
const scope = client.scopes;
const cookie = document.cookie.split(";");
cookie.forEach((c) => {
if (c.includes("client_state"))
clientState = c.replace("client_state=", "").trim();
});
await api.oauth.onOAuthSubmit(clientId, clientState, scope);
setIsAllowRunning(false);
};
const onDenyClick = async () => {
if (!("clientId" in client)) return;
if (isAllowRunning || isDenyRunning) return;
setIsDenyRunning(true);
const clientId = client.clientId;
let clientState = "";
const cookie = document.cookie.split(";");
cookie.forEach((c) => {
if (c.includes("client_state"))
clientState = c.replace("client_state=", "").trim();
});
deleteCookie("client_state");
await api.oauth.onOAuthCancel(clientId, clientState);
setIsDenyRunning(false);
};
const onChangeUserClick = async () => {
await api.user.logout();
router.push(`/?client_id=${client.clientId}&type=oauth2`);
};
return (
<FormWrapper>
<OAuthClientInfo
name={client.name}
logo={client.logo}
websiteUrl={client.websiteUrl}
isConsentScreen
/>
<ScopeList
t={t}
selectedScopes={client.scopes || []}
scopes={scopes || []}
/>
<StyledButtonContainer>
<Button
onClick={onAllowClick}
label={"Allow"}
size={ButtonSize.normal}
scale
primary
isDisabled={isDenyRunning}
isLoading={isAllowRunning}
/>
<Button
onClick={onDenyClick}
label={"Deny"}
size={ButtonSize.normal}
scale
isDisabled={isAllowRunning}
isLoading={isDenyRunning}
/>
</StyledButtonContainer>
<StyledDescriptionContainer>
<Text fontWeight={400} fontSize={"13px"} lineHeight={"20px"}>
<Trans t={t} i18nKey={"ConsentDescription"} ns="Consent">
Data shared with {{ displayName: user.displayName }} will be
governed by {{ nameApp: client.name }}
<Link
className={"login-link"}
type={LinkType.page}
isHovered={false}
href={client.policyUrl}
target={LinkTarget.blank}
noHover
>
privacy policy
</Link>
and
<Link
className={"login-link"}
type={LinkType.page}
isHovered={false}
href={client.termsUrl}
target={LinkTarget.blank}
noHover
>
terms of service
</Link>
. You can revoke this consent at any time in your DocSpace account
settings.
</Trans>
</Text>
</StyledDescriptionContainer>
<StyledUserContainer>
<div className="block">
<Avatar
size={AvatarSize.min}
role={AvatarRole.user}
source={user.avatarSmall || ""}
/>
<div className="user-info">
<Text lineHeight={"20px"}>
{t("SignedInAs")} {user.email}
</Text>
<Link
className={"login-link"}
type={LinkType.action}
isHovered={false}
noHover
lineHeight={"20px"}
onClick={onChangeUserClick}
>
{t("NotYou")}
</Link>
</div>
</div>
</StyledUserContainer>
</FormWrapper>
);
};
export default Consent;

View File

@ -0,0 +1,111 @@
/* eslint-disable @next/next/no-img-element */
import React from "react";
import styled from "styled-components";
import { Trans, useTranslation } from "react-i18next";
import { Text } from "@docspace/shared/components/text";
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
const StyledOAuthContainer = styled.div`
width: 100%;
height: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin-bottom: 32px;
img {
width: 32px;
height: 32px;
}
.row {
width: 100%;
text-align: center;
}
.login-link {
cursor: normal;
}
`;
interface IOAuthClientInfo {
name: string;
logo: string;
websiteUrl: string;
isConsentScreen?: boolean;
}
const OAuthClientInfo = ({
name,
logo,
websiteUrl,
isConsentScreen,
}: IOAuthClientInfo) => {
const { t } = useTranslation(["Consent", "Common"]);
return (
<StyledOAuthContainer>
{!isConsentScreen && (
<Text
className="row"
fontWeight={600}
fontSize="16px"
lineHeight="22px"
>
{t("Common:LoginButton")}
</Text>
)}
<img src={logo} alt={"client-logo"} />
<Text
className="row"
fontWeight={isConsentScreen ? 400 : 600}
fontSize="16px"
lineHeight="22px"
>
{isConsentScreen ? (
<Trans t={t} i18nKey={"ConsentSubHeader"} ns="Consent">
<Link
className={"login-link"}
type={LinkType.page}
isHovered={false}
href={websiteUrl}
target={LinkTarget.blank}
noHover
fontWeight={600}
fontSize="16px"
>
{name}
</Link>{" "}
would like the ability to access the following data in{" "}
<strong>your DocSpace account</strong>:
</Trans>
) : (
<>
{t("Consent:ToContinue")}{" "}
<Link
className={"login-link"}
type={LinkType.page}
isHovered={false}
href={websiteUrl}
target={LinkTarget.blank}
noHover
fontWeight={600}
fontSize="16px"
>
{name}
</Link>
</>
)}
</Text>
</StyledOAuthContainer>
);
};
export default OAuthClientInfo;

View File

@ -29,7 +29,7 @@
import React, { useLayoutEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { useSearchParams } from "next/navigation";
import { usePathname, useSearchParams } from "next/navigation";
import { useTheme } from "styled-components";
import { Text } from "@docspace/shared/components/text";
@ -48,6 +48,7 @@ const GreetingContainer = ({ greetingSettings }: GreetingContainersProps) => {
const logoUrl = getLogoUrl(WhiteLabelLogoType.LoginPage, !theme.isBase);
const searchParams = useSearchParams();
const pathname = usePathname();
const [invitationLinkData, setInvitationLinkData] = useState({
email: "",
@ -84,7 +85,9 @@ const GreetingContainer = ({ greetingSettings }: GreetingContainersProps) => {
textAlign="center"
className="greeting-title"
>
{greetingSettings}
{pathname === "/tenant-list"
? "Choose your portal"
: greetingSettings}
</Text>
)}

View File

@ -35,8 +35,13 @@ import { Text } from "@docspace/shared/components/text";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import ErrorContainer from "@docspace/shared/components/error-container/ErrorContainer";
import { getMessageFromKey, getMessageKeyTranslate } from "@/utils";
import {
getMessageFromKey,
getMessageKeyTranslate,
getOAuthMessageKeyTranslation,
} from "@/utils";
import { PRODUCT_NAME } from "@docspace/shared/constants";
import { OAuth2ErrorKey } from "@/utils/enums";
const homepage = "/";
@ -62,7 +67,14 @@ const InvalidError = ({ match }: InvalidErrorProps) => {
}, [router]);
const message = getMessageFromKey(match?.messageKey ? +match.messageKey : 1);
const errorTitle = match?.messageKey
const oauthError = getOAuthMessageKeyTranslation(
t,
match?.oauthMessageKey as OAuth2ErrorKey | undefined,
);
const errorTitle = oauthError
? oauthError
: match?.messageKey
? getMessageKeyTranslate(t, message)
: t("Common:ExpiredLink");

Some files were not shown because too many files have changed in this diff Show More