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 { 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} • `}
|
||||||
</p>
|
Last active: {format(new Date(session.refreshed_at || session.created_at), 'PPpp')}
|
||||||
<p className="text-sm text-muted-foreground">
|
{session.aal === 'aal2' && ' • MFA'}
|
||||||
Expires: {format(new Date(session.expires_at), 'PPpp')}
|
|
||||||
</p>
|
</p>
|
||||||
|
{session.not_after && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Expires: {format(new Date(session.not_after), 'PPpp')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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