Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
708049bb
Unverified
Commit
708049bb
authored
Feb 18, 2023
by
boojack
Committed by
GitHub
Feb 18, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add SSO related UI (#1118)
* feat: add SSO related UI * chore: update
parent
65aa51d5
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
451 additions
and
9 deletions
+451
-9
CreateIdentityProviderDialog.tsx
web/src/components/CreateIdentityProviderDialog.tsx
+279
-0
SettingDialog.tsx
web/src/components/SettingDialog.tsx
+20
-9
SSOSection.tsx
web/src/components/Settings/SSOSection.tsx
+90
-0
api.ts
web/src/helpers/api.ts
+16
-0
idp.d.ts
web/src/types/modules/idp.d.ts
+46
-0
No files found.
web/src/components/CreateIdentityProviderDialog.tsx
0 → 100644
View file @
708049bb
import
{
useEffect
,
useState
}
from
"react"
;
import
{
Button
,
Divider
,
Input
,
List
,
Radio
,
RadioGroup
,
Typography
}
from
"@mui/joy"
;
import
*
as
api
from
"../helpers/api"
;
import
{
generateDialog
}
from
"./Dialog"
;
import
Icon
from
"./Icon"
;
import
toastHelper
from
"./Toast"
;
interface
Props
extends
DialogProps
{
identityProvider
?:
IdentityProvider
;
confirmCallback
?:
()
=>
void
;
}
const
CreateIdentityProviderDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
confirmCallback
,
destroy
,
identityProvider
}
=
props
;
const
[
basicInfo
,
setBasicInfo
]
=
useState
({
name
:
""
,
identifierFilter
:
""
,
});
const
[
type
,
setType
]
=
useState
<
IdentityProviderType
>
(
"OAUTH2"
);
const
[
oauth2Config
,
setOAuth2Config
]
=
useState
<
IdentityProviderOAuth2Config
>
({
clientId
:
""
,
clientSecret
:
""
,
authUrl
:
""
,
tokenUrl
:
""
,
userInfoUrl
:
""
,
scopes
:
[],
fieldMapping
:
{
identifier
:
""
,
displayName
:
""
,
email
:
""
,
},
});
const
[
oauth2Scopes
,
setOAuth2Scopes
]
=
useState
<
string
>
(
""
);
const
isCreating
=
identityProvider
===
undefined
;
useEffect
(()
=>
{
if
(
identityProvider
)
{
setBasicInfo
({
name
:
identityProvider
.
name
,
identifierFilter
:
identityProvider
.
identifierFilter
,
});
setType
(
identityProvider
.
type
);
if
(
identityProvider
.
type
===
"OAUTH2"
)
{
setOAuth2Config
(
identityProvider
.
config
.
oauth2Config
);
setOAuth2Scopes
(
identityProvider
.
config
.
oauth2Config
.
scopes
.
join
(
" "
));
}
}
},
[]);
const
handleCloseBtnClick
=
()
=>
{
destroy
();
};
const
allowConfirmAction
=
()
=>
{
if
(
basicInfo
.
name
===
""
)
{
return
false
;
}
if
(
type
===
"OAUTH2"
)
{
if
(
oauth2Config
.
clientId
===
""
||
oauth2Config
.
clientSecret
===
""
||
oauth2Config
.
authUrl
===
""
||
oauth2Config
.
tokenUrl
===
""
||
oauth2Config
.
userInfoUrl
===
""
||
oauth2Scopes
===
""
||
oauth2Config
.
fieldMapping
.
identifier
===
""
)
{
return
false
;
}
}
return
true
;
};
const
handleConfirmBtnClick
=
async
()
=>
{
try
{
if
(
isCreating
)
{
await
api
.
createIdentityProvider
({
...
basicInfo
,
type
:
type
,
config
:
{
oauth2Config
:
{
...
oauth2Config
,
scopes
:
oauth2Scopes
.
split
(
" "
),
},
},
});
}
else
{
await
api
.
patchIdentityProvider
({
id
:
identityProvider
?.
id
,
type
:
type
,
...
basicInfo
,
config
:
{
oauth2Config
:
{
...
oauth2Config
,
scopes
:
oauth2Scopes
.
split
(
" "
),
},
},
});
}
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toastHelper
.
error
(
error
.
response
.
data
.
message
);
}
destroy
();
if
(
confirmCallback
)
{
confirmCallback
();
}
};
const
setPartialOAuth2Config
=
(
state
:
Partial
<
IdentityProviderOAuth2Config
>
)
=>
{
setOAuth2Config
({
...
oauth2Config
,
...
state
,
});
};
return
(
<>
<
div
className=
"dialog-header-container !w-96"
>
<
p
className=
"title-text"
>
{
isCreating
?
"Create SSO"
:
"Update SSO"
}
</
p
>
<
button
className=
"btn close-btn"
onClick=
{
handleCloseBtnClick
}
>
<
Icon
.
X
/>
</
button
>
</
div
>
<
div
className=
"dialog-content-container"
>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Type
</
Typography
>
<
RadioGroup
className=
"mb-2"
value=
{
type
}
>
<
List
>
<
Radio
value=
"OAUTH2"
label=
"OAuth 2.0"
/>
</
List
>
</
RadioGroup
>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Name
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Name"
value=
{
basicInfo
.
name
}
onChange=
{
(
e
)
=>
setBasicInfo
({
...
basicInfo
,
name
:
e
.
target
.
value
,
})
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Identifier filter
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Identifier filter"
value=
{
basicInfo
.
identifierFilter
}
onChange=
{
(
e
)
=>
setBasicInfo
({
...
basicInfo
,
identifierFilter
:
e
.
target
.
value
,
})
}
fullWidth
/>
<
Divider
className=
"!my-2"
/>
{
type
===
"OAUTH2"
&&
(
<>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Client ID
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Client ID"
value=
{
oauth2Config
.
clientId
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
clientId
:
e
.
target
.
value
})
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Client secret
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Client secret"
value=
{
oauth2Config
.
clientSecret
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
clientSecret
:
e
.
target
.
value
})
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Authorization endpoint
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Authorization endpoint"
value=
{
oauth2Config
.
authUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
authUrl
:
e
.
target
.
value
})
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Token endpoint
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Token endpoint"
value=
{
oauth2Config
.
tokenUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
tokenUrl
:
e
.
target
.
value
})
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
User info endpoint
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"User info endpoint"
value=
{
oauth2Config
.
userInfoUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
userInfoUrl
:
e
.
target
.
value
})
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Scopes
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Scopes"
value=
{
oauth2Scopes
}
onChange=
{
(
e
)
=>
setOAuth2Scopes
(
e
.
target
.
value
)
}
fullWidth
/>
<
Divider
className=
"!my-2"
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Identifier
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"User ID key"
value=
{
oauth2Config
.
fieldMapping
.
identifier
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
identifier
:
e
.
target
.
value
}
})
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Display name
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"User name key"
value=
{
oauth2Config
.
fieldMapping
.
displayName
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
displayName
:
e
.
target
.
value
}
})
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Email
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"User email key"
value=
{
oauth2Config
.
fieldMapping
.
email
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
email
:
e
.
target
.
value
}
})
}
fullWidth
/>
</>
)
}
<
div
className=
"mt-2 w-full flex flex-row justify-end items-center space-x-1"
>
<
Button
variant=
"plain"
color=
"neutral"
onClick=
{
handleCloseBtnClick
}
>
Cancel
</
Button
>
<
Button
onClick=
{
handleConfirmBtnClick
}
disabled=
{
!
allowConfirmAction
()
}
>
{
isCreating
?
"Create"
:
"Update"
}
</
Button
>
</
div
>
</
div
>
</>
);
};
function
showCreateIdentityProviderDialog
(
identityProvider
?:
IdentityProvider
,
confirmCallback
?:
()
=>
void
)
{
generateDialog
(
{
className
:
"create-identity-provider-dialog"
,
dialogName
:
"create-identity-provider-dialog"
,
},
CreateIdentityProviderDialog
,
{
identityProvider
,
confirmCallback
}
);
}
export
default
showCreateIdentityProviderDialog
;
web/src/components/SettingDialog.tsx
View file @
708049bb
...
...
@@ -3,17 +3,18 @@ import { useTranslation } from "react-i18next";
import
{
useGlobalStore
,
useUserStore
}
from
"../store/module"
;
import
Icon
from
"./Icon"
;
import
{
generateDialog
}
from
"./Dialog"
;
import
BetaBadge
from
"./BetaBadge"
;
import
MyAccountSection
from
"./Settings/MyAccountSection"
;
import
PreferencesSection
from
"./Settings/PreferencesSection"
;
import
MemberSection
from
"./Settings/MemberSection"
;
import
SystemSection
from
"./Settings/SystemSection"
;
import
StorageSection
from
"./Settings/StorageSection"
;
import
BetaBadge
from
"./BetaBadge
"
;
import
SSOSection
from
"./Settings/SSOSection
"
;
import
"../less/setting-dialog.less"
;
type
Props
=
DialogProps
;
type
SettingSection
=
"my-account"
|
"preferences"
|
"
storage"
|
"member"
|
"system
"
;
type
SettingSection
=
"my-account"
|
"preferences"
|
"
member"
|
"system"
|
"storage"
|
"sso
"
;
interface
State
{
selectedSection
:
SettingSection
;
...
...
@@ -47,13 +48,13 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
onClick=
{
()
=>
handleSectionSelectorItemClick
(
"my-account"
)
}
className=
{
`section-item ${state.selectedSection === "my-account" ? "selected" : ""}`
}
>
<
span
className=
"icon-text"
>
🤠
</
span
>
{
t
(
"setting.my-account"
)
}
<
Icon
.
User
className=
"w-4 h-auto mr-2 opacity-80"
/
>
{
t
(
"setting.my-account"
)
}
</
span
>
<
span
onClick=
{
()
=>
handleSectionSelectorItemClick
(
"preferences"
)
}
className=
{
`section-item ${state.selectedSection === "preferences" ? "selected" : ""}`
}
>
<
span
className=
"icon-text"
>
🏟
</
span
>
{
t
(
"setting.preference"
)
}
<
Icon
.
Cog
className=
"w-4 h-auto mr-2 opacity-80"
/
>
{
t
(
"setting.preference"
)
}
</
span
>
</
div
>
{
user
?.
role
===
"HOST"
?
(
...
...
@@ -64,20 +65,28 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
onClick=
{
()
=>
handleSectionSelectorItemClick
(
"member"
)
}
className=
{
`section-item ${state.selectedSection === "member" ? "selected" : ""}`
}
>
<
span
className=
"icon-text"
>
👤
</
span
>
{
t
(
"setting.member"
)
}
<
Icon
.
Users
className=
"w-4 h-auto mr-2 opacity-80"
/
>
{
t
(
"setting.member"
)
}
</
span
>
<
span
onClick=
{
()
=>
handleSectionSelectorItemClick
(
"system"
)
}
className=
{
`section-item ${state.selectedSection === "system" ? "selected" : ""}`
}
>
<
span
className=
"icon-text"
>
🛠️
</
span
>
{
t
(
"setting.system"
)
}
<
Icon
.
Settings2
className=
"w-4 h-auto mr-2 opacity-80"
/
>
{
t
(
"setting.system"
)
}
</
span
>
{
globalStore
.
isDev
()
&&
(
<
span
onClick=
{
()
=>
handleSectionSelectorItemClick
(
"storage"
)
}
className=
{
`section-item ${state.selectedSection === "storage" ? "selected" : ""}`
}
>
<
span
className=
"icon-text"
>
💾
</
span
>
{
t
(
"setting.storage"
)
}
<
BetaBadge
/>
<
Icon
.
Database
className=
"w-4 h-auto mr-2 opacity-80"
/>
{
t
(
"setting.storage"
)
}
<
BetaBadge
/>
</
span
>
)
}
{
globalStore
.
isDev
()
&&
(
<
span
onClick=
{
()
=>
handleSectionSelectorItemClick
(
"sso"
)
}
className=
{
`section-item ${state.selectedSection === "sso" ? "selected" : ""}`
}
>
<
Icon
.
Key
className=
"w-4 h-auto mr-2 opacity-80"
/>
SSO
<
BetaBadge
/>
</
span
>
)
}
</
div
>
...
...
@@ -89,12 +98,14 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
<
MyAccountSection
/>
)
:
state
.
selectedSection
===
"preferences"
?
(
<
PreferencesSection
/>
)
:
state
.
selectedSection
===
"storage"
?
(
<
StorageSection
/>
)
:
state
.
selectedSection
===
"member"
?
(
<
MemberSection
/>
)
:
state
.
selectedSection
===
"system"
?
(
<
SystemSection
/>
)
:
state
.
selectedSection
===
"storage"
?
(
<
StorageSection
/>
)
:
state
.
selectedSection
===
"sso"
?
(
<
SSOSection
/>
)
:
null
}
</
div
>
</
div
>
...
...
web/src/components/Settings/SSOSection.tsx
0 → 100644
View file @
708049bb
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
*
as
api
from
"../../helpers/api"
;
import
showCreateIdentityProviderDialog
from
"../CreateIdentityProviderDialog"
;
import
Dropdown
from
"../common/Dropdown"
;
import
{
showCommonDialog
}
from
"../Dialog/CommonDialog"
;
import
toastHelper
from
"../Toast"
;
const
SSOSection
=
()
=>
{
const
{
t
}
=
useTranslation
();
const
[
identityProviderList
,
setIdentityProviderList
]
=
useState
<
IdentityProvider
[]
>
([]);
useEffect
(()
=>
{
fetchIdentityProviderList
();
},
[]);
const
fetchIdentityProviderList
=
async
()
=>
{
const
{
data
:
{
data
:
identityProviderList
},
}
=
await
api
.
getIdentityProviderList
();
setIdentityProviderList
(
identityProviderList
);
};
const
handleDeleteIdentityProvider
=
async
(
identityProvider
:
IdentityProvider
)
=>
{
showCommonDialog
({
title
:
"Confirm delete"
,
content
:
"Are you sure to delete this SSO? THIS ACTION IS IRREVERSIABLE❗"
,
style
:
"warning"
,
dialogName
:
"delete-identity-provider-dialog"
,
onConfirm
:
async
()
=>
{
try
{
await
api
.
deleteIdentityProvider
(
identityProvider
.
id
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toastHelper
.
error
(
error
.
response
.
data
.
message
);
}
await
fetchIdentityProviderList
();
},
});
};
return
(
<
div
className=
"section-container"
>
<
div
className=
"mt-4 mb-2 w-full flex flex-row justify-start items-center"
>
<
span
className=
"font-mono text-sm text-gray-400 mr-2"
>
SSO List
</
span
>
<
button
className=
"btn-normal px-2 py-0 leading-7"
onClick=
{
()
=>
showCreateIdentityProviderDialog
(
undefined
,
fetchIdentityProviderList
)
}
>
{
t
(
"common.create"
)
}
</
button
>
</
div
>
<
div
className=
"mt-2 w-full flex flex-col"
>
{
identityProviderList
.
map
((
identityProvider
)
=>
(
<
div
key=
{
identityProvider
.
id
}
className=
"py-2 w-full border-t last:border-b flex flex-row items-center justify-between"
>
<
div
className=
"flex flex-row items-center"
>
<
p
className=
"ml-2"
>
{
identityProvider
.
name
}
<
span
className=
"text-sm ml-1 opacity-40"
>
(
{
identityProvider
.
type
}
)
</
span
>
</
p
>
</
div
>
<
div
className=
"flex flex-row items-center"
>
<
Dropdown
actionsClassName=
"!w-28"
actions=
{
<>
<
button
className=
"w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick=
{
()
=>
showCreateIdentityProviderDialog
(
identityProvider
,
fetchIdentityProviderList
)
}
>
Edit
</
button
>
<
button
className=
"w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick=
{
()
=>
handleDeleteIdentityProvider
(
identityProvider
)
}
>
{
t
(
"common.delete"
)
}
</
button
>
</>
}
/>
</
div
>
</
div
>
))
}
</
div
>
</
div
>
);
};
export
default
SSOSection
;
web/src/helpers/api.ts
View file @
708049bb
...
...
@@ -222,6 +222,22 @@ export function deleteStorage(storageId: StorageId) {
return
axios
.
delete
(
`/api/storage/
${
storageId
}
`
);
}
export
function
getIdentityProviderList
()
{
return
axios
.
get
<
ResponseObject
<
IdentityProvider
[]
>>
(
`/api/idp`
);
}
export
function
createIdentityProvider
(
identityProviderCreate
:
IdentityProviderCreate
)
{
return
axios
.
post
<
ResponseObject
<
IdentityProvider
>>
(
`/api/idp`
,
identityProviderCreate
);
}
export
function
patchIdentityProvider
(
identityProviderPatch
:
IdentityProviderPatch
)
{
return
axios
.
patch
<
ResponseObject
<
IdentityProvider
>>
(
`/api/idp/
${
identityProviderPatch
.
id
}
`
,
identityProviderPatch
);
}
export
function
deleteIdentityProvider
(
id
:
IdentityProviderId
)
{
return
axios
.
delete
(
`/api/idp/
${
id
}
`
);
}
export
async
function
getRepoStarCount
()
{
const
{
data
}
=
await
axios
.
get
(
`https://api.github.com/repos/usememos/memos`
,
{
headers
:
{
...
...
web/src/types/modules/idp.d.ts
0 → 100644
View file @
708049bb
type
IdentityProviderId
=
number
;
type
IdentityProviderType
=
"OAUTH2"
;
interface
FieldMapping
{
identifier
:
string
;
displayName
:
string
;
email
:
string
;
}
interface
IdentityProviderOAuth2Config
{
clientId
:
string
;
clientSecret
:
string
;
authUrl
:
string
;
tokenUrl
:
string
;
userInfoUrl
:
string
;
scopes
:
string
[];
fieldMapping
:
FieldMapping
;
}
interface
IdentityProviderConfig
{
oauth2Config
:
IdentityProviderOAuth2Config
;
}
interface
IdentityProvider
{
id
:
IdentityProviderId
;
name
:
string
;
type
:
IdentityProviderType
;
identifierFilter
:
string
;
config
:
IdentityProviderConfig
;
}
interface
IdentityProviderCreate
{
name
:
string
;
type
:
IdentityProviderType
;
identifierFilter
:
string
;
config
:
IdentityProviderConfig
;
}
interface
IdentityProviderPatch
{
id
:
IdentityProviderId
;
type
:
IdentityProviderType
;
name
?:
string
;
identifierFilter
?:
string
;
config
?:
IdentityProviderConfig
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment