-
Notifications
You must be signed in to change notification settings - Fork 29
Add Account Settings page #8672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
8f2428c
58c2d49
9367076
33b0b1a
4a4c460
8e18e2a
77e602a
09d50ea
14e624b
cb70499
dd6c8e6
4cd2ecd
6a4b559
d022413
82fd616
aa95b90
a2002cb
b7543ea
749861a
44e0966
76616d0
8716091
55bf833
513011b
01ae877
67e8819
121c91a
9ee0b24
203ae75
df8e81d
b094806
a91f88b
38c426e
511c4fb
d0cd5b3
66086aa
1903218
d61a522
f2ee79d
2ed707a
1f74811
125ed0f
e489f9d
c8e6a4e
a84f9fc
0418bbf
c12b0a3
b3ef603
58af2a4
52728d5
235be66
f734e92
0260a0e
8b8fb57
0bb572d
6762eee
e6a69d5
6e8e483
c3be491
7d98ca1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,94 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { InfoCircleOutlined, SwapOutlined } from "@ant-design/icons"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { getAuthToken, revokeAuthToken } from "admin/rest_api"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { Button, Descriptions, Popover, Spin, Typography } from "antd"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { useWkSelector } from "libs/react_hooks"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { useEffect, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { AccountSettingsTitle } from "./account_profile_view"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const { Text } = Typography; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
function AccountAuthTokenView() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
const activeUser = useWkSelector((state) => state.activeUser); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const [isLoading, setIsLoading] = useState<boolean>(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const [currentToken, setCurrentToken] = useState<string>(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
fetchData(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
}, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
async function fetchData(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
const token = await getAuthToken(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
setCurrentToken(token); | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const handleRevokeToken = async (): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
await revokeAuthToken(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
const token = await getAuthToken(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
setCurrentToken(token); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||
setIsLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const APIitems = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
label: "Auth Token", | ||||||||||||||||||||||||||||||||||||||||||||||||||
children: ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
<Text code copyable> | ||||||||||||||||||||||||||||||||||||||||||||||||||
{currentToken} | ||||||||||||||||||||||||||||||||||||||||||||||||||
</Text> | ||||||||||||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
label: ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
<> | ||||||||||||||||||||||||||||||||||||||||||||||||||
<span className="icon-margin-right">Token Revocation</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
<Popover | ||||||||||||||||||||||||||||||||||||||||||||||||||
content="Revoke your token if it has been compromised or if you suspect someone else has gained | ||||||||||||||||||||||||||||||||||||||||||||||||||
access to it. This will invalidate all active sessions." | ||||||||||||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||
<InfoCircleOutlined /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
</Popover> | ||||||||||||||||||||||||||||||||||||||||||||||||||
</> | ||||||||||||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||||||||||
children: ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
<Button icon={<SwapOutlined />} onClick={handleRevokeToken}> | ||||||||||||||||||||||||||||||||||||||||||||||||||
Revoke and Generate New Token | ||||||||||||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
...(activeUser | ||||||||||||||||||||||||||||||||||||||||||||||||||
? [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
label: "Organization ID", | ||||||||||||||||||||||||||||||||||||||||||||||||||
children: ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
<Text code copyable> | ||||||||||||||||||||||||||||||||||||||||||||||||||
{activeUser.organization} | ||||||||||||||||||||||||||||||||||||||||||||||||||
</Text> | ||||||||||||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||||||||||||||||||||||||
: []), | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add null safety check for activeUser.organization. While ...(activeUser
? [
{
label: "Organization ID",
children: (
<Text code copyable>
- {activeUser.organization}
+ {activeUser.organization || "N/A"}
</Text>
),
},
]
: []), 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
label: "API Documentation", | ||||||||||||||||||||||||||||||||||||||||||||||||||
children: <a href="https://docs.webknossos.org/webknossos-py/index.html">Read the docs</a>, | ||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
<div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
<AccountSettingsTitle | ||||||||||||||||||||||||||||||||||||||||||||||||||
title="API Authorization" | ||||||||||||||||||||||||||||||||||||||||||||||||||
description="Access the WEBKNOSSO Python API with your API token" | ||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||
<Spin size="large" spinning={isLoading}> | ||||||||||||||||||||||||||||||||||||||||||||||||||
<Descriptions column={2} layout="vertical" colon={false} items={APIitems} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
</Spin> | ||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
export default AccountAuthTokenView; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
import { LockOutlined } from "@ant-design/icons"; | ||
import { Alert, Button, Descriptions, Form, Input, List, Space } from "antd"; | ||
import Request from "libs/request"; | ||
import Toast from "libs/toast"; | ||
import messages from "messages"; | ||
import { useState } from "react"; | ||
import { type RouteComponentProps, withRouter } from "react-router-dom"; | ||
import { logoutUserAction } from "viewer/model/actions/user_actions"; | ||
import Store from "viewer/store"; | ||
import { AccountSettingsTitle } from "./account_profile_view"; | ||
const FormItem = Form.Item; | ||
const { Password } = Input; | ||
|
||
type Props = { | ||
history: RouteComponentProps["history"]; | ||
}; | ||
philippotto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const MIN_PASSWORD_LENGTH = 8; | ||
|
||
function AccountPasswordView({ history }: Props) { | ||
const [form] = Form.useForm(); | ||
const [isResetPasswordVisible, setResetPasswordVisible] = useState(false); | ||
|
||
function onFinish(formValues: Record<string, any>) { | ||
Request.sendJSONReceiveJSON("/api/auth/changePassword", { | ||
data: formValues, | ||
}).then(async () => { | ||
Toast.success(messages["auth.reset_pw_confirmation"]); | ||
await Request.receiveJSON("/api/auth/logout"); | ||
history.push("/auth/login"); | ||
Store.dispatch(logoutUserAction()); | ||
}); | ||
} | ||
hotzenklotz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
function checkPasswordsAreMatching(value: string, otherPasswordFieldKey: string[]) { | ||
const otherFieldValue = form.getFieldValue(otherPasswordFieldKey); | ||
|
||
if (value && otherFieldValue) { | ||
if (value !== otherFieldValue) { | ||
return Promise.reject(new Error(messages["auth.registration_password_mismatch"])); | ||
} else if (form.getFieldError(otherPasswordFieldKey).length > 0) { | ||
// If the other password field still has errors, revalidate it. | ||
form.validateFields([otherPasswordFieldKey]); | ||
} | ||
} | ||
|
||
return Promise.resolve(); | ||
} | ||
|
||
function getPasswordComponent() { | ||
return isResetPasswordVisible ? ( | ||
<Form onFinish={onFinish} form={form}> | ||
<FormItem | ||
name="oldPassword" | ||
rules={[ | ||
{ | ||
required: true, | ||
message: messages["auth.reset_old_password"], | ||
}, | ||
]} | ||
> | ||
<Password | ||
prefix={ | ||
<LockOutlined | ||
style={{ | ||
fontSize: 13, | ||
}} | ||
/> | ||
} | ||
placeholder="Old Password" | ||
/> | ||
</FormItem> | ||
<FormItem | ||
hasFeedback | ||
name={["password", "password1"]} | ||
rules={[ | ||
{ | ||
required: true, | ||
message: messages["auth.reset_new_password"], | ||
}, | ||
{ | ||
min: MIN_PASSWORD_LENGTH, | ||
message: messages["auth.registration_password_length"], | ||
}, | ||
{ | ||
validator: (_, value: string) => | ||
checkPasswordsAreMatching(value, ["password", "password2"]), | ||
}, | ||
]} | ||
> | ||
<Password | ||
prefix={ | ||
<LockOutlined | ||
style={{ | ||
fontSize: 13, | ||
}} | ||
/> | ||
} | ||
placeholder="New Password" | ||
/> | ||
</FormItem> | ||
<FormItem | ||
hasFeedback | ||
name={["password", "password2"]} | ||
rules={[ | ||
{ | ||
required: true, | ||
message: messages["auth.reset_new_password2"], | ||
}, | ||
{ | ||
min: MIN_PASSWORD_LENGTH, | ||
message: messages["auth.registration_password_length"], | ||
}, | ||
{ | ||
validator: (_, value: string) => | ||
checkPasswordsAreMatching(value, ["password", "password1"]), | ||
}, | ||
]} | ||
> | ||
<Password | ||
prefix={ | ||
<LockOutlined | ||
style={{ | ||
fontSize: 13, | ||
}} | ||
/> | ||
} | ||
placeholder="Confirm New Password" | ||
/> | ||
</FormItem> | ||
<Alert | ||
type="info" | ||
message={messages["auth.reset_logout"]} | ||
showIcon | ||
style={{ | ||
marginBottom: 24, | ||
}} | ||
/> | ||
<FormItem> | ||
<Space> | ||
<Button onClick={() => setResetPasswordVisible(false)}>Cancel</Button> | ||
<Button type="primary" htmlType="submit"> | ||
Update Password | ||
</Button> | ||
</Space> | ||
</FormItem> | ||
</Form> | ||
) : ( | ||
<> | ||
<Space.Compact> | ||
<Input.Password visibilityToggle={false} disabled value="******************" /> | ||
<Button type="primary" onClick={handleResetPassword}> | ||
Reset Password | ||
</Button> | ||
</Space.Compact> | ||
</> | ||
); | ||
} | ||
|
||
function handleResetPassword() { | ||
setResetPasswordVisible(true); | ||
} | ||
|
||
const passwordItems = [ | ||
{ | ||
label: "Password", | ||
children: getPasswordComponent(), | ||
}, | ||
]; | ||
|
||
const passKeyList = [ | ||
{ | ||
name: "passkey1", | ||
details: "2024-05-01", | ||
}, | ||
{ | ||
name: "passkey2", | ||
details: "2025-05-01", | ||
}, | ||
]; | ||
|
||
return ( | ||
<div> | ||
<AccountSettingsTitle title="Password" description="Manage and update your password" /> | ||
<Descriptions | ||
column={2} | ||
layout="vertical" | ||
colon={false} | ||
items={passwordItems} | ||
style={{ marginBottom: "3rem" }} | ||
/> | ||
|
||
<AccountSettingsTitle title="Passkeys" description="Login passwordless with Passkeys" /> | ||
<List | ||
className="demo-loadmore-list" | ||
itemLayout="horizontal" | ||
dataSource={passKeyList} | ||
renderItem={(item) => ( | ||
<List.Item actions={[<a key="list-delete">Delete</a>]}> | ||
<List.Item.Meta title={item.name} description={item.details} /> | ||
</List.Item> | ||
)} | ||
/> | ||
</div> | ||
); | ||
} | ||
|
||
export default withRouter<RouteComponentProps, any>(AccountPasswordView); |
Uh oh!
There was an error while loading. Please reload this page.