feat: Integrate auth.sessions with RPC functions

This commit is contained in:
gpt-engineer-app[bot]
2025-10-14 18:26:22 +00:00
parent 098ca9f3b0
commit b129f88f3a
3 changed files with 121 additions and 80 deletions

View File

@@ -23,20 +23,15 @@ import type { UserIdentity, OAuthProvider } from '@/types/identity';
import { toast as sonnerToast } from '@/components/ui/sonner'; import { toast as sonnerToast } from '@/components/ui/sonner';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
interface DeviceInfo { interface AuthSession {
browser?: string;
userAgent?: string;
os?: string;
device?: string;
}
interface UserSession {
id: string; id: string;
device_info: DeviceInfo | null;
last_activity: string;
created_at: string; created_at: string;
expires_at: string; updated_at: string;
session_token: string; refreshed_at: string | null;
user_agent: string | null;
ip: unknown;
not_after: string | null;
aal: 'aal1' | 'aal2' | 'aal3' | null;
} }
export function SecurityTab() { export function SecurityTab() {
@@ -49,7 +44,7 @@ export function SecurityTab() {
const [disconnectingProvider, setDisconnectingProvider] = useState<OAuthProvider | null>(null); const [disconnectingProvider, setDisconnectingProvider] = useState<OAuthProvider | null>(null);
const [hasPassword, setHasPassword] = useState(false); const [hasPassword, setHasPassword] = useState(false);
const [addingPassword, setAddingPassword] = useState(false); const [addingPassword, setAddingPassword] = useState(false);
const [sessions, setSessions] = useState<UserSession[]>([]); const [sessions, setSessions] = useState<AuthSession[]>([]);
const [loadingSessions, setLoadingSessions] = useState(true); const [loadingSessions, setLoadingSessions] = useState(true);
// Load user identities on mount // Load user identities on mount
@@ -183,33 +178,23 @@ export function SecurityTab() {
const fetchSessions = async () => { const fetchSessions = async () => {
if (!user) return; if (!user) return;
const { data, error } = await supabase const { data, error } = await supabase.rpc('get_my_sessions');
.from('user_sessions')
.select('*')
.eq('user_id', user.id)
.order('last_activity', { ascending: false });
if (error) { if (error) {
console.error('Error fetching sessions:', error); console.error('Error fetching sessions:', error);
toast({
title: 'Error',
description: 'Failed to load sessions',
variant: 'destructive'
});
} else { } else {
const typedSessions: UserSession[] = (data || []).map(session => ({ setSessions(data || []);
id: session.id,
device_info: session.device_info as DeviceInfo | null,
last_activity: session.last_activity,
created_at: session.created_at,
expires_at: session.expires_at,
session_token: session.session_token
}));
setSessions(typedSessions);
} }
setLoadingSessions(false); setLoadingSessions(false);
}; };
const revokeSession = async (sessionId: string) => { const revokeSession = async (sessionId: string) => {
const { error } = await supabase const { error } = await supabase.rpc('revoke_my_session', { session_id: sessionId });
.from('user_sessions')
.delete()
.eq('id', sessionId);
if (error) { if (error) {
toast({ toast({
@@ -226,13 +211,30 @@ export function SecurityTab() {
} }
}; };
const getDeviceIcon = (deviceInfo: DeviceInfo | null) => { const getDeviceIcon = (userAgent: string | null) => {
const ua = deviceInfo?.userAgent?.toLowerCase() || ''; if (!userAgent) return <Monitor className="w-4 h-4" />;
if (ua.includes('mobile')) return <Smartphone className="w-4 h-4" />;
if (ua.includes('tablet')) return <Tablet className="w-4 h-4" />; const ua = userAgent.toLowerCase();
if (ua.includes('mobile') || ua.includes('android') || ua.includes('iphone')) {
return <Smartphone className="w-4 h-4" />;
}
if (ua.includes('tablet') || ua.includes('ipad')) {
return <Tablet className="w-4 h-4" />;
}
return <Monitor className="w-4 h-4" />; return <Monitor className="w-4 h-4" />;
}; };
const getBrowserName = (userAgent: string | null) => {
if (!userAgent) return 'Unknown Browser';
const ua = userAgent.toLowerCase();
if (ua.includes('firefox')) return 'Firefox';
if (ua.includes('chrome') && !ua.includes('edg')) return 'Chrome';
if (ua.includes('safari') && !ua.includes('chrome')) return 'Safari';
if (ua.includes('edg')) return 'Edge';
return 'Unknown Browser';
};
// Get connected accounts with identity data // Get connected accounts with identity data
const connectedAccounts = [ const connectedAccounts = [
{ {
@@ -415,17 +417,21 @@ export function SecurityTab() {
{sessions.map((session) => ( {sessions.map((session) => (
<div key={session.id} className="flex items-start justify-between p-3 border rounded-lg"> <div key={session.id} className="flex items-start justify-between p-3 border rounded-lg">
<div className="flex gap-3"> <div className="flex gap-3">
{getDeviceIcon(session.device_info)} {getDeviceIcon(session.user_agent)}
<div> <div>
<p className="font-medium"> <p className="font-medium">
{session.device_info?.browser || 'Unknown Browser'} {getBrowserName(session.user_agent)}
</p> </p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Last active: {format(new Date(session.last_activity), 'PPpp')} {session.ip && `${session.ip}`}
Last active: {format(new Date(session.refreshed_at || session.created_at), 'PPpp')}
{session.aal === 'aal2' && ' • MFA'}
</p> </p>
{session.not_after && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Expires: {format(new Date(session.expires_at), 'PPpp')} Expires: {format(new Date(session.not_after), 'PPpp')}
</p> </p>
)}
</div> </div>
</div> </div>
<Button <Button

View File

@@ -2659,45 +2659,6 @@ export type Database = {
} }
Relationships: [] Relationships: []
} }
user_sessions: {
Row: {
created_at: string
device_info: Json | null
expires_at: string
id: string
ip_address: unknown | null
ip_address_hash: string | null
last_activity: string
session_token: string
user_agent: string | null
user_id: string
}
Insert: {
created_at?: string
device_info?: Json | null
expires_at?: string
id?: string
ip_address?: unknown | null
ip_address_hash?: string | null
last_activity?: string
session_token: string
user_agent?: string | null
user_id: string
}
Update: {
created_at?: string
device_info?: Json | null
expires_at?: string
id?: string
ip_address?: unknown | null
ip_address_hash?: string | null
last_activity?: string
session_token?: string
user_agent?: string | null
user_id?: string
}
Relationships: []
}
user_top_list_items: { user_top_list_items: {
Row: { Row: {
created_at: string created_at: string
@@ -3000,6 +2961,19 @@ export type Database = {
Args: { _profile_user_id: string; _viewer_id?: string } Args: { _profile_user_id: string; _viewer_id?: string }
Returns: Json Returns: Json
} }
get_my_sessions: {
Args: Record<PropertyKey, never>
Returns: {
aal: "aal1" | "aal2" | "aal3"
created_at: string
id: string
ip: unknown
not_after: string
refreshed_at: string
updated_at: string
user_agent: string
}[]
}
get_submission_item_dependencies: { get_submission_item_dependencies: {
Args: { item_id: string } Args: { item_id: string }
Returns: { Returns: {
@@ -3068,6 +3042,10 @@ export type Database = {
Args: { moderator_id: string; submission_id: string } Args: { moderator_id: string; submission_id: string }
Returns: boolean Returns: boolean
} }
revoke_my_session: {
Args: { session_id: string }
Returns: undefined
}
rollback_to_version: { rollback_to_version: {
Args: { Args: {
p_changed_by: string p_changed_by: string

View File

@@ -0,0 +1,57 @@
-- Create function to get user's own sessions from auth.sessions
CREATE OR REPLACE FUNCTION public.get_my_sessions()
RETURNS TABLE (
id uuid,
created_at timestamptz,
updated_at timestamptz,
refreshed_at timestamptz,
user_agent text,
ip inet,
not_after timestamptz,
aal auth.aal_level
)
SECURITY DEFINER
SET search_path = auth, public
LANGUAGE plpgsql
AS $$
BEGIN
-- Only return sessions for the authenticated user
RETURN QUERY
SELECT
s.id,
s.created_at,
s.updated_at,
s.refreshed_at,
s.user_agent,
s.ip,
s.not_after,
s.aal
FROM auth.sessions s
WHERE s.user_id = auth.uid()
ORDER BY s.refreshed_at DESC NULLS LAST, s.created_at DESC;
END;
$$;
-- Grant execute to authenticated users
GRANT EXECUTE ON FUNCTION public.get_my_sessions() TO authenticated;
-- Create function to revoke user's own session
CREATE OR REPLACE FUNCTION public.revoke_my_session(session_id uuid)
RETURNS void
SECURITY DEFINER
SET search_path = auth, public
LANGUAGE plpgsql
AS $$
BEGIN
-- Only delete own sessions
DELETE FROM auth.sessions
WHERE id = session_id
AND user_id = auth.uid();
END;
$$;
-- Grant execute to authenticated users
GRANT EXECUTE ON FUNCTION public.revoke_my_session(uuid) TO authenticated;
-- Drop the unused public.user_sessions table
DROP TABLE IF EXISTS public.user_sessions CASCADE;