mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
feat: Integrate auth.sessions with RPC functions
This commit is contained in:
@@ -23,20 +23,15 @@ import type { UserIdentity, OAuthProvider } from '@/types/identity';
|
||||
import { toast as sonnerToast } from '@/components/ui/sonner';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
|
||||
interface DeviceInfo {
|
||||
browser?: string;
|
||||
userAgent?: string;
|
||||
os?: string;
|
||||
device?: string;
|
||||
}
|
||||
|
||||
interface UserSession {
|
||||
interface AuthSession {
|
||||
id: string;
|
||||
device_info: DeviceInfo | null;
|
||||
last_activity: string;
|
||||
created_at: string;
|
||||
expires_at: string;
|
||||
session_token: string;
|
||||
updated_at: string;
|
||||
refreshed_at: string | null;
|
||||
user_agent: string | null;
|
||||
ip: unknown;
|
||||
not_after: string | null;
|
||||
aal: 'aal1' | 'aal2' | 'aal3' | null;
|
||||
}
|
||||
|
||||
export function SecurityTab() {
|
||||
@@ -49,7 +44,7 @@ export function SecurityTab() {
|
||||
const [disconnectingProvider, setDisconnectingProvider] = useState<OAuthProvider | null>(null);
|
||||
const [hasPassword, setHasPassword] = useState(false);
|
||||
const [addingPassword, setAddingPassword] = useState(false);
|
||||
const [sessions, setSessions] = useState<UserSession[]>([]);
|
||||
const [sessions, setSessions] = useState<AuthSession[]>([]);
|
||||
const [loadingSessions, setLoadingSessions] = useState(true);
|
||||
|
||||
// Load user identities on mount
|
||||
@@ -183,33 +178,23 @@ export function SecurityTab() {
|
||||
const fetchSessions = async () => {
|
||||
if (!user) return;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('user_sessions')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.order('last_activity', { ascending: false });
|
||||
const { data, error } = await supabase.rpc('get_my_sessions');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching sessions:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to load sessions',
|
||||
variant: 'destructive'
|
||||
});
|
||||
} else {
|
||||
const typedSessions: UserSession[] = (data || []).map(session => ({
|
||||
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);
|
||||
setSessions(data || []);
|
||||
}
|
||||
setLoadingSessions(false);
|
||||
};
|
||||
|
||||
const revokeSession = async (sessionId: string) => {
|
||||
const { error } = await supabase
|
||||
.from('user_sessions')
|
||||
.delete()
|
||||
.eq('id', sessionId);
|
||||
const { error } = await supabase.rpc('revoke_my_session', { session_id: sessionId });
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
@@ -226,13 +211,30 @@ export function SecurityTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const getDeviceIcon = (deviceInfo: DeviceInfo | null) => {
|
||||
const ua = deviceInfo?.userAgent?.toLowerCase() || '';
|
||||
if (ua.includes('mobile')) return <Smartphone className="w-4 h-4" />;
|
||||
if (ua.includes('tablet')) return <Tablet className="w-4 h-4" />;
|
||||
const getDeviceIcon = (userAgent: string | null) => {
|
||||
if (!userAgent) return <Monitor 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" />;
|
||||
};
|
||||
|
||||
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
|
||||
const connectedAccounts = [
|
||||
{
|
||||
@@ -415,17 +417,21 @@ export function SecurityTab() {
|
||||
{sessions.map((session) => (
|
||||
<div key={session.id} className="flex items-start justify-between p-3 border rounded-lg">
|
||||
<div className="flex gap-3">
|
||||
{getDeviceIcon(session.device_info)}
|
||||
{getDeviceIcon(session.user_agent)}
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{session.device_info?.browser || 'Unknown Browser'}
|
||||
{getBrowserName(session.user_agent)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Last active: {format(new Date(session.last_activity), 'PPpp')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Expires: {format(new Date(session.expires_at), 'PPpp')}
|
||||
{session.ip && `${session.ip} • `}
|
||||
Last active: {format(new Date(session.refreshed_at || session.created_at), 'PPpp')}
|
||||
{session.aal === 'aal2' && ' • MFA'}
|
||||
</p>
|
||||
{session.not_after && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Expires: {format(new Date(session.not_after), 'PPpp')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -2659,45 +2659,6 @@ export type Database = {
|
||||
}
|
||||
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: {
|
||||
Row: {
|
||||
created_at: string
|
||||
@@ -3000,6 +2961,19 @@ export type Database = {
|
||||
Args: { _profile_user_id: string; _viewer_id?: string }
|
||||
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: {
|
||||
Args: { item_id: string }
|
||||
Returns: {
|
||||
@@ -3068,6 +3042,10 @@ export type Database = {
|
||||
Args: { moderator_id: string; submission_id: string }
|
||||
Returns: boolean
|
||||
}
|
||||
revoke_my_session: {
|
||||
Args: { session_id: string }
|
||||
Returns: undefined
|
||||
}
|
||||
rollback_to_version: {
|
||||
Args: {
|
||||
p_changed_by: string
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user