Skip to content

Commit 50f9267

Browse files
authored
Merge pull request #308 from code0-tech/feat/#305
Manage user sessions in user settings
2 parents c638d39 + 98d8f80 commit 50f9267

14 files changed

Lines changed: 280 additions & 34 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {UserSettingsPage} from "@edition/user/pages/UserSettingsPage";
2+
3+
export default UserSettingsPage
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Default() {
2+
return null
3+
}

src/app/(dashboard)/layout.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ interface ApplicationLayoutProps {
2626
children: React.ReactNode
2727
bar: React.ReactNode
2828
tab: React.ReactNode
29+
modal: React.ReactNode
2930
}
3031

31-
const ApplicationLayout: React.FC<ApplicationLayoutProps> = ({children, bar, tab}) => {
32+
const ApplicationLayout: React.FC<ApplicationLayoutProps> = ({children, bar, tab, modal}) => {
3233

3334
const client = useApolloClient()
3435
const pathname = usePathname()
@@ -83,9 +84,12 @@ const ApplicationLayout: React.FC<ApplicationLayoutProps> = ({children, bar, tab
8384
</Flex>
8485
}>
8586
<Layout showLayoutSplitter={false} pr={0.7} layoutGap={"0"} topContent={<>{bar}</>}>
86-
<Layout>
87-
<>{children}</>
88-
</Layout>
87+
<>
88+
<Layout>
89+
<>{children}</>
90+
</Layout>
91+
{modal}
92+
</>
8993
</Layout>
9094
</Layout>
9195
</ApplicationMiddlewareComponent>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {UserSettingsPage} from "@edition/user/pages/UserSettingsPage";
2+
3+
export default UserSettingsPage

src/packages/ce/src/application/pages/ApplicationPage.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ export const ApplicationPage = () => {
119119
</Text>
120120
</Flex>
121121
<Spacing spacing={"xs"}/>
122-
<Button w={"100%"} disabled color={"tertiary"}>Edit Profile</Button>
122+
<Link href={"/users/@me/settings"}>
123+
<Button w={"100%"} color={"tertiary"}>Edit Profile</Button>
124+
</Link>
123125
<Spacing spacing={"xs"}/>
124126
<Button color={"primary"} disabled w={"100%"}>
125127
Upgrade to Pro

src/packages/ce/src/application/views/ApplicationBarView.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
IconFolders,
1313
IconInbox,
1414
IconLogout,
15-
IconSearch,
15+
IconSearch, IconSettings,
1616
IconUser
1717
} from "@tabler/icons-react";
1818
import {ApplicationBreadcrumbView} from "@edition/application/views/ApplicationBreadcrumbView";
@@ -151,15 +151,20 @@ export const ApplicationBarView: React.FC = () => {
151151
}
152152

153153
return <UserMenuComponent userId={currentSession.user?.id!!}>
154-
<Link href={`/users/${userIndex}`}>
154+
<Link href={`/users/@me`}>
155155
<MenuItem>
156156
<IconUser size={16}/>Profile
157157
</MenuItem>
158158
</Link>
159+
<Link href={`/users/@me/settings`}>
160+
<MenuItem>
161+
<IconSettings size={16}/>Settings
162+
</MenuItem>
163+
</Link>
159164
<MenuSeparator/>
160165
<Link href={"/workspaces"}>
161166
<MenuItem>
162-
<IconBuilding size={16}/>Organizations
167+
<IconBuilding size={16}/>Workspaces
163168
</MenuItem>
164169
</Link>
165170
<Link href={`/namespace/${userNamespaceIndex}`}>

src/packages/ce/src/namespace/views/NamespaceOverviewPersonalLeftView.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ export const NamespaceOverviewPersonalLeftView: React.FC = () => {
8686
</Text>
8787
</Flex>
8888
<Spacing spacing={"xs"}/>
89-
<Button w={"100%"} color={"tertiary"}>Edit Profile</Button>
89+
<Link href={"/users/@me/settings"}>
90+
<Button w={"100%"} color={"tertiary"}>Edit Profile</Button>
91+
</Link>
9092
<Spacing spacing={"xs"}/>
91-
<Button color={"primary"} w={"100%"}>
93+
<Button disabled color={"primary"} w={"100%"}>
9294
Upgrade to Pro
9395
<AuroraBackground/>
9496
</Button>

src/packages/ce/src/user/components/UserEditDialogComponent.tsx

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ import CardSection from "@code0-tech/pictor/dist/components/card/CardSection";
3030
import {Tab, TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab";
3131
import {User, UsersUpdateInput} from "@code0-tech/sagittarius-graphql-types";
3232
import {UserService} from "@edition/user/services/User.service";
33+
import {useUserSession} from "@edition/user/hooks/User.session.hook";
3334
import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook";
3435
import {IconAt, IconBackground, IconLock, IconMail, IconSettings2, IconShieldLock} from "@tabler/icons-react";
3536
import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout";
3637
import {motion} from "framer-motion";
38+
import {UserSessionsDataTableComponent} from "@edition/user/components/UserSessionsDataTableComponent";
3739

3840
export interface UserEditDialogComponentProps {
3941
userId?: User['id']
@@ -49,11 +51,14 @@ export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (
4951
const userStore = useStore(UserService)
5052
const [, startTransition] = React.useTransition()
5153

54+
const currentSession = useUserSession()
5255
const user = React.useMemo(
5356
() => userService.getById(userId),
5457
[userStore, userId]
5558
)
5659

60+
const isSelf = !!user && !!currentSession?.user?.id && user.id === currentSession.user.id
61+
5762
const initialValues = React.useMemo(() => ({
5863
firstname: user?.firstname ?? null,
5964
lastname: user?.lastname ?? null,
@@ -110,8 +115,10 @@ export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (
110115
if (values.firstname !== (user?.firstname ?? null)) payload.firstname = values.firstname
111116
if (values.lastname !== (user?.lastname ?? null)) payload.lastname = values.lastname
112117
if (values.readme !== (user?.readme ?? null)) payload.readme = values.readme
113-
if (values.admin !== (user?.admin ?? false)) payload.admin = values.admin
114-
if (values.blocked !== (user?.blocked ?? false)) payload.blocked = values.blocked
118+
if (!isSelf) {
119+
if (values.admin !== (user?.admin ?? false)) payload.admin = values.admin
120+
if (values.blocked !== (user?.blocked ?? false)) payload.blocked = values.blocked
121+
}
115122
if (values.password) {
116123
payload.password = values.password
117124
payload.passwordRepeat = values.repeatPassword
@@ -185,16 +192,24 @@ export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (
185192
<Text size={"md"}>General</Text>
186193
</Button>
187194
</TabTrigger>
188-
<TabTrigger value={"permissions"} w={"100%"} asChild>
195+
{!isSelf && (
196+
<TabTrigger value={"permissions"} w={"100%"} asChild>
197+
<Button paddingSize={"xxs"} variant={"none"} justify={"start"}>
198+
<IconShieldLock size={13}/>
199+
<Text size={"md"}>Access</Text>
200+
</Button>
201+
</TabTrigger>
202+
)}
203+
<TabTrigger value={"security"} w={"100%"} asChild>
189204
<Button paddingSize={"xxs"} variant={"none"} justify={"start"}>
190-
<IconShieldLock size={13}/>
191-
<Text size={"md"}>Access</Text>
205+
<IconSettings2 opacity={0} size={13}/>
206+
<Text size={"md"}>Security</Text>
192207
</Button>
193208
</TabTrigger>
194-
<TabTrigger value={"security"} w={"100%"} asChild>
209+
<TabTrigger value={"sessions"} w={"100%"} asChild>
195210
<Button paddingSize={"xxs"} variant={"none"} justify={"start"}>
196211
<IconSettings2 opacity={0} size={13}/>
197-
<Text size={"md"}>Security</Text>
212+
<Text size={"md"}>Sessions</Text>
198213
</Button>
199214
</TabTrigger>
200215
</TabList>
@@ -256,7 +271,8 @@ export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (
256271
description={"A short bio or notes shown on the user's profile."}
257272
{...inputs.getInputProps("readme")}/>
258273
</TabContent>
259-
<TabContent value={"permissions"} style={{overflow: "hidden"}}>
274+
<TabContent value={"permissions"}
275+
style={{overflow: "hidden", display: isSelf ? "none" : undefined}}>
260276
<Flex justify={"space-between"} align={"center"}>
261277
<Text size={"lg"} hierarchy={"primary"} display={"block"}>Access</Text>
262278
<Button paddingSize={"xxs"} color={"success"} variant={"none"}
@@ -314,6 +330,24 @@ export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (
314330
onChange={() => validate("repeatPassword")}
315331
{...inputs.getInputProps("repeatPassword")}/>
316332
</TabContent>
333+
<TabContent value={"sessions"}
334+
style={{
335+
overflow: "hidden",
336+
height: "100%",
337+
display: "flex",
338+
flexDirection: "column"
339+
}}>
340+
<Flex justify={"space-between"} align={"center"}>
341+
<Text size={"lg"} hierarchy={"primary"} display={"block"}>Sessions</Text>
342+
</Flex>
343+
<Spacing spacing={"xs"}/>
344+
<Text size={"md"} hierarchy={"tertiary"}>
345+
Active sign-in sessions for @{user?.username ?? ""}. Log out a session to revoke
346+
its access.
347+
</Text>
348+
<Spacing spacing={"md"}/>
349+
<UserSessionsDataTableComponent userId={userId}/>
350+
</TabContent>
317351
</Card>
318352
</Layout>
319353
</Tab>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"use client"
2+
3+
import React from "react";
4+
import {
5+
DataTable,
6+
DataTableColumn,
7+
ScrollArea,
8+
ScrollAreaScrollbar, ScrollAreaThumb,
9+
ScrollAreaViewport,
10+
Text,
11+
useService,
12+
useStore
13+
} from "@code0-tech/pictor";
14+
import {User, UserSession} from "@code0-tech/sagittarius-graphql-types";
15+
import {UserService} from "@edition/user/services/User.service";
16+
import {UserSessionsDataTableRowComponent} from "@edition/user/components/UserSessionsDataTableRowComponent";
17+
18+
export interface UserSessionsDataTableComponentProps {
19+
userId: User['id']
20+
}
21+
22+
export const UserSessionsDataTableComponent: React.FC<UserSessionsDataTableComponentProps> = (props) => {
23+
24+
const {userId} = props
25+
26+
const userService = useService(UserService)
27+
const userStore = useStore(UserService)
28+
29+
const sessions = React.useMemo(
30+
() => (userService.getById(userId)?.sessions?.nodes ?? []).filter((session): session is UserSession => !!session && !!session.active),
31+
[userStore, userId]
32+
)
33+
34+
return <ScrollArea w={"100%"} h={"100%"} type={"scroll"}>
35+
<ScrollAreaViewport>
36+
<DataTable filter={{}}
37+
sort={{}}
38+
emptyComponent={<DataTableColumn>
39+
<Text>No sessions found for this user.</Text>
40+
</DataTableColumn>}
41+
data={sessions}>
42+
{(session) => <UserSessionsDataTableRowComponent userId={userId} sessionId={session.id}/>}
43+
</DataTable>
44+
</ScrollAreaViewport>
45+
<ScrollAreaScrollbar orientation={"vertical"}>
46+
<ScrollAreaThumb/>
47+
</ScrollAreaScrollbar>
48+
</ScrollArea>
49+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"use client"
2+
3+
import React from "react";
4+
import {User, UserSession} from "@code0-tech/sagittarius-graphql-types";
5+
import {Button, DataTableColumn, Flex, Text, useService, useStore} from "@code0-tech/pictor";
6+
import {UserService} from "@edition/user/services/User.service";
7+
import {useUserSession} from "@edition/user/hooks/User.session.hook";
8+
import {formatDistanceToNow} from "date-fns";
9+
import {IconDeviceDesktop, IconLogout} from "@tabler/icons-react";
10+
import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook";
11+
12+
export interface UserSessionsDataTableRowComponentProps {
13+
userId: User['id']
14+
sessionId: UserSession['id']
15+
}
16+
17+
export const UserSessionsDataTableRowComponent: React.FC<UserSessionsDataTableRowComponentProps> = (props) => {
18+
19+
const {userId, sessionId} = props
20+
21+
const userService = useService(UserService)
22+
const userStore = useStore(UserService)
23+
const currentSession = useUserSession()
24+
25+
const session = React.useMemo(
26+
() => userService.getById(userId)?.sessions?.nodes?.find(session => session?.id === sessionId) as UserSession | undefined,
27+
[userStore, userId, sessionId]
28+
)
29+
30+
const isCurrent = !!sessionId && sessionId === currentSession?.id
31+
32+
const logout = React.useCallback(() => {
33+
if (!sessionId) return
34+
userService.usersLogout({userSessionId: sessionId}).then(payload => {
35+
if ((payload?.errors?.length ?? 0) <= 0) {
36+
addIslandSuccessNotification({
37+
message: "Logged out session"
38+
})
39+
}
40+
})
41+
}, [sessionId])
42+
43+
if (!session) return null
44+
45+
return <>
46+
<DataTableColumn pr={2.5}>
47+
<Flex align={"center"} style={{gap: "0.7rem"}}>
48+
<IconDeviceDesktop size={16}/>
49+
<Flex style={{flexDirection: "column"}}>
50+
<Text size={"md"} hierarchy={"primary"}>
51+
{isCurrent ? "This device" : "Session"}
52+
</Text>
53+
{session.createdAt && (
54+
<Text size={"xs"} hierarchy={"tertiary"}>
55+
Created {formatDistanceToNow(new Date(session.createdAt), {addSuffix: true})}
56+
</Text>
57+
)}
58+
</Flex>
59+
60+
</Flex>
61+
</DataTableColumn>
62+
<DataTableColumn pr={2.5}>
63+
{session.updatedAt && (
64+
<Text hierarchy={"tertiary"}>
65+
Last used {formatDistanceToNow(new Date(session.updatedAt), {addSuffix: true})}
66+
</Text>
67+
)}
68+
</DataTableColumn>
69+
<DataTableColumn>
70+
<Button p={0.5} color={"secondary"} variant={"none"} onClick={logout}>
71+
<IconLogout size={16}/>
72+
</Button>
73+
</DataTableColumn>
74+
</>
75+
}

0 commit comments

Comments
 (0)