mirror of
https://codeberg.org/MarkusThielker/next-ory.git
synced 2025-07-02 04:59:17 +00:00
NORY-1: add user session management
This commit is contained in:
parent
a74e7f3ebd
commit
e2a8f1d2a4
9 changed files with 934 additions and 143 deletions
79
authentication/src/components/accountSessions.tsx
Normal file
79
authentication/src/components/accountSessions.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
'use client';
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { kratos } from '@/ory';
|
||||
import { Session } from '@ory/client';
|
||||
import SessionItem from '@/components/sessionItem';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
|
||||
export default function AccountSessions() {
|
||||
|
||||
const [current, setCurrent] = useState<Session>();
|
||||
const [sessions, setSessions] = useState<Session[]>();
|
||||
|
||||
const invalidateSession = useCallback(async (id: string) => {
|
||||
|
||||
console.log('Disabling session with id', id);
|
||||
|
||||
kratos.disableMySession({ id: id })
|
||||
.then(() => console.log('Disabled session with id', id))
|
||||
.catch(() => console.error('Error while disabling session with id', id))
|
||||
.finally(() => {
|
||||
loadSessions().then((response) => {
|
||||
setCurrent(response.current);
|
||||
setSessions(response.sessions);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadSessions = useCallback(async () => {
|
||||
|
||||
console.log('Refreshing sessions');
|
||||
|
||||
const current = await kratos.toSession();
|
||||
const sessions = await kratos.listMySessions();
|
||||
|
||||
console.log(current.data);
|
||||
console.log(sessions.data);
|
||||
|
||||
return {
|
||||
current: current.data,
|
||||
sessions: sessions.data,
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions().then(response => {
|
||||
setCurrent(response.current);
|
||||
setSessions(response.sessions);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4 w-full max-w-md">
|
||||
{
|
||||
current ?
|
||||
<SessionItem session={current!!} showInvalidate={false} invalidateSession={invalidateSession}/>
|
||||
:
|
||||
<></>
|
||||
}
|
||||
{
|
||||
sessions && sessions.length > 0 ?
|
||||
<Separator/>
|
||||
:
|
||||
<></>
|
||||
}
|
||||
{
|
||||
sessions?.map(item => {
|
||||
return (
|
||||
<SessionItem
|
||||
key={item.id}
|
||||
session={item}
|
||||
showInvalidate={true}
|
||||
invalidateSession={invalidateSession}/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
145
authentication/src/components/accountSettings.tsx
Normal file
145
authentication/src/components/accountSettings.tsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { SettingsFlow, UpdateSettingsFlowBody } from '@ory/client';
|
||||
import { Flow } from '@/ory';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
interface AccountSettingsProps {
|
||||
flow: SettingsFlow | undefined;
|
||||
updateFlow: (body: UpdateSettingsFlowBody) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function AccountSettings({ flow, updateFlow }: AccountSettingsProps) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4 w-full max-w-md">
|
||||
{
|
||||
flow?.ui.nodes.some(({ group }) => group === 'profile') && (
|
||||
<Card className="w-full max-w-md animate-fadeIn">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Profile
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Flow
|
||||
onSubmit={updateFlow}
|
||||
flow={flow}
|
||||
only="profile"
|
||||
hideGlobalMessages/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
{
|
||||
flow?.ui.nodes.some(({ group }) => group === 'password') && (
|
||||
<Card className="w-full max-w-md animate-fadeIn">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Password
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Flow
|
||||
onSubmit={updateFlow}
|
||||
flow={flow}
|
||||
only="password"
|
||||
hideGlobalMessages/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
{
|
||||
flow?.ui.nodes.some(({ group }) => group === 'totp') && (
|
||||
<Card className="w-full max-w-md animate-fadeIn">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
MFA
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Flow
|
||||
onSubmit={updateFlow}
|
||||
flow={flow}
|
||||
only="totp"
|
||||
hideGlobalMessages/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
{
|
||||
flow?.ui.nodes.some(({ group }) => group === 'oidc') && (
|
||||
<Card className="w-full max-w-md animate-fadeIn">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Connect Socials
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Flow
|
||||
onSubmit={updateFlow}
|
||||
flow={flow}
|
||||
only="oidc"
|
||||
hideGlobalMessages/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
{
|
||||
flow?.ui.nodes.some(({ group }) => group === 'link') && (
|
||||
<Card className="w-full max-w-md animate-fadeIn">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Connect Socials
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Flow
|
||||
onSubmit={updateFlow}
|
||||
flow={flow}
|
||||
only="link"
|
||||
hideGlobalMessages/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
{
|
||||
flow?.ui.nodes.some(({ group }) => group === 'webauthn') && (
|
||||
<Card className="w-full max-w-md animate-fadeIn">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Connect Socials
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Flow
|
||||
onSubmit={updateFlow}
|
||||
flow={flow}
|
||||
only="webauthn"
|
||||
hideGlobalMessages/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
{
|
||||
flow?.ui.nodes.some(({ group }) => group === 'lookup_secret') && (
|
||||
<Card className="w-full max-w-md animate-fadeIn">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Recovery Codes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Flow
|
||||
onSubmit={updateFlow}
|
||||
flow={flow}
|
||||
only="lookup_secret"
|
||||
hideGlobalMessages/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
53
authentication/src/components/sessionItem.tsx
Normal file
53
authentication/src/components/sessionItem.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Session, SessionDevice } from '@ory/client';
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface SessionItemProps {
|
||||
session: Session;
|
||||
showInvalidate: boolean;
|
||||
invalidateSession: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function SessionItem({ session, showInvalidate, invalidateSession }: SessionItemProps) {
|
||||
|
||||
if (!session.devices || session.devices.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [device, setDevice] = useState<SessionDevice>(session.devices[0]);
|
||||
|
||||
const parser = new UAParser(device.user_agent);
|
||||
const result = parser.getResult();
|
||||
|
||||
return (
|
||||
<Card className="relative w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{result.os.name}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{result.browser.name}, version {result.browser.version} <br/>
|
||||
Signed in since {new Date(session.authenticated_at!!).toLocaleString()}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{
|
||||
showInvalidate ?
|
||||
<Button
|
||||
className="absolute top-4 right-4"
|
||||
onClick={() => invalidateSession(session.id)}>
|
||||
Invalidate
|
||||
</Button>
|
||||
:
|
||||
<Badge
|
||||
className="absolute top-4 right-4">
|
||||
This session
|
||||
</Badge>
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
37
authentication/src/components/ui/badge.tsx
Normal file
37
authentication/src/components/ui/badge.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {
|
||||
}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
55
authentication/src/components/ui/tabs.tsx
Normal file
55
authentication/src/components/ui/tabs.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
Loading…
Add table
Add a link
Reference in a new issue