mirror of
https://github.com/mito-systems/ranger-app.git
synced 2026-05-05 21:24:07 +00:00
ok
This commit is contained in:
parent
92333ec559
commit
bea4b6a1b4
@ -3,7 +3,7 @@ import supabase, { supabaseAdmin } from './supabaseClient';
|
||||
import { generateImageHash } from './imageHashService';
|
||||
import { v4 as uuidv4, v5 as uuidv5, validate as validateUuid } from 'uuid';
|
||||
|
||||
// Namespace for deterministic UUID generation (using v5)
|
||||
// Namespace for deterministic UUID generation (using v5) - as fallback only
|
||||
const UUID_NAMESPACE = '1b671a64-40d5-491e-99b0-da01ff1f3341';
|
||||
|
||||
// Point values for different actions
|
||||
@ -12,20 +12,63 @@ export const POINTS = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure user ID is a valid UUID
|
||||
* Ensure a user exists in the database
|
||||
*/
|
||||
function ensureUuid(userId: string): string {
|
||||
export async function ensureUserExists(userId: string, email: string, name?: string) {
|
||||
try {
|
||||
// Validate that the userId is a valid UUID format
|
||||
if (validateUuid(userId)) {
|
||||
return userId;
|
||||
} else {
|
||||
// Generate a deterministic UUID v5 from the string userId
|
||||
return uuidv5(userId, UUID_NAMESPACE);
|
||||
if (!userId) {
|
||||
console.warn('Cannot ensure user exists with undefined userId');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('Ensuring user exists:', { userId, email });
|
||||
|
||||
// IMPORTANT: Do NOT generate a new UUID, only validate format
|
||||
if (!validateUuid(userId)) {
|
||||
console.warn('Invalid UUID format for user:', userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Call the Supabase function to ensure user exists
|
||||
const { data, error } = await supabaseAdmin.rpc('ensure_user_exists', {
|
||||
user_id_param: userId,
|
||||
email_param: email,
|
||||
name_param: name || null
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Error ensuring user exists:', error);
|
||||
|
||||
// Fallback to direct insert
|
||||
try {
|
||||
const { data: insertData, error: insertError } = await supabaseAdmin
|
||||
.from('users')
|
||||
.insert({
|
||||
id: userId,
|
||||
email: email,
|
||||
name: name || null,
|
||||
first_login: new Date().toISOString(),
|
||||
last_login: new Date().toISOString()
|
||||
})
|
||||
.onConflict('id')
|
||||
.merge({ last_login: new Date().toISOString() });
|
||||
|
||||
if (insertError) {
|
||||
console.error('Error inserting user:', insertError);
|
||||
return null;
|
||||
}
|
||||
|
||||
return userId;
|
||||
} catch (err) {
|
||||
console.error('Error in fallback user creation:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log('Error processing UUID, generating a new one:', error);
|
||||
return uuidv5(userId, UUID_NAMESPACE);
|
||||
console.error('Unexpected error in ensureUserExists:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,19 +92,34 @@ export async function awardPointsForImage(
|
||||
};
|
||||
}
|
||||
|
||||
// Validate user ID format
|
||||
if (!validateUuid(userId)) {
|
||||
console.warn('Invalid UUID format for user:', userId);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Invalid user ID format'
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Award points starting values:', { userId, email });
|
||||
|
||||
// Convert userId to UUID format
|
||||
const userIdUUID = ensureUuid(userId);
|
||||
console.log(`Using UUID ${userIdUUID} for user ${userId}`);
|
||||
// Ensure user exists in database
|
||||
const userUuid = await ensureUserExists(userId, email);
|
||||
if (!userUuid) {
|
||||
console.error('Failed to ensure user exists');
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to register user'
|
||||
};
|
||||
}
|
||||
|
||||
// Generate hash for the image
|
||||
const imageHash = generateImageHash(imageBuffer);
|
||||
console.log(`Processing points for image ${imageHash.substring(0, 8)}... for user ${userIdUUID}`);
|
||||
console.log(`Processing points for image ${imageHash.substring(0, 8)}... for user ${userId}`);
|
||||
|
||||
// Check if this image has already been awarded points
|
||||
const { data: existingImage, error: checkError } = await supabaseAdmin.rpc('check_duplicate_upload', {
|
||||
user_id_param: userIdUUID,
|
||||
user_id_param: userId,
|
||||
image_hash_param: imageHash
|
||||
});
|
||||
|
||||
@ -73,7 +131,7 @@ export async function awardPointsForImage(
|
||||
.from('point_transactions')
|
||||
.select('*')
|
||||
.eq('image_hash', imageHash)
|
||||
.eq('user_id', userIdUUID)
|
||||
.eq('user_id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!directError && existingTransaction) {
|
||||
@ -94,7 +152,7 @@ export async function awardPointsForImage(
|
||||
// Create transaction using RPC
|
||||
const { data: transactionData, error: rpcError } = await supabaseAdmin
|
||||
.rpc('create_point_transaction', {
|
||||
user_id_param: userIdUUID,
|
||||
user_id_param: userId,
|
||||
points_param: POINTS.IMAGE_UPLOAD,
|
||||
transaction_type_param: 'image_upload',
|
||||
image_hash_param: imageHash,
|
||||
@ -110,7 +168,7 @@ export async function awardPointsForImage(
|
||||
const { data: insertData, error: insertError } = await supabaseAdmin
|
||||
.from('point_transactions')
|
||||
.insert({
|
||||
user_id: userIdUUID,
|
||||
user_id: userId,
|
||||
points: POINTS.IMAGE_UPLOAD,
|
||||
transaction_type: 'image_upload',
|
||||
image_hash: imageHash,
|
||||
@ -148,6 +206,42 @@ export async function awardPointsForImage(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user by email
|
||||
*/
|
||||
export async function findUserByEmail(email: string) {
|
||||
if (!email) return null;
|
||||
|
||||
try {
|
||||
const { data, error } = await supabaseAdmin.rpc('find_user_by_email', {
|
||||
email_param: email
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
console.log('Error or no data finding user by email:', error);
|
||||
|
||||
// Fallback to direct query
|
||||
const { data: directData, error: directError } = await supabaseAdmin
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('email', email)
|
||||
.maybeSingle();
|
||||
|
||||
if (directError || !directData) {
|
||||
console.log('Error or no data in direct user lookup:', directError);
|
||||
return null;
|
||||
}
|
||||
|
||||
return directData;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error in findUserByEmail:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's total points
|
||||
*/
|
||||
@ -160,58 +254,52 @@ export async function getUserPoints(userId: string) {
|
||||
|
||||
console.log('Fetching points for user:', userId);
|
||||
|
||||
// Generate potential IDs to try
|
||||
const identifiersToTry = [userId];
|
||||
|
||||
// If userId is an email, add a UUID version
|
||||
if (userId.includes('@')) {
|
||||
const emailUuid = uuidv5(userId, UUID_NAMESPACE);
|
||||
identifiersToTry.push(emailUuid);
|
||||
}
|
||||
// If it's not a UUID, generate one
|
||||
else if (!validateUuid(userId)) {
|
||||
const generatedUuid = uuidv5(userId, UUID_NAMESPACE);
|
||||
identifiersToTry.push(generatedUuid);
|
||||
// If the userId is not a UUID and looks like an email, try to find by email
|
||||
if (!validateUuid(userId) && userId.includes('@')) {
|
||||
const user = await findUserByEmail(userId);
|
||||
if (user) {
|
||||
userId = user.id;
|
||||
console.log('Found user by email, using ID:', userId);
|
||||
} else {
|
||||
console.log('No user found by email, returning 0 points');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Will try these identifiers for points calculation:', identifiersToTry);
|
||||
// Validate final user ID format
|
||||
if (!validateUuid(userId)) {
|
||||
console.warn('Invalid UUID format for user:', userId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Try getting total points using the RPC function
|
||||
for (const idToTry of identifiersToTry) {
|
||||
const userUuid = validateUuid(idToTry) ? idToTry : uuidv5(idToTry, UUID_NAMESPACE);
|
||||
try {
|
||||
const { data: totalPoints, error } = await supabaseAdmin.rpc('get_user_total_points', {
|
||||
user_id_param: userId
|
||||
});
|
||||
|
||||
try {
|
||||
const { data: totalPoints, error } = await supabaseAdmin.rpc('get_user_total_points', {
|
||||
user_id_param: userUuid
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
console.log(`Total points for user ${userUuid}: ${totalPoints}`);
|
||||
return totalPoints || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error getting points for ID ${userUuid}:`, err);
|
||||
if (!error) {
|
||||
console.log(`Total points for user ${userId}: ${totalPoints}`);
|
||||
return totalPoints || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error getting points for ID ${userId}:`, err);
|
||||
}
|
||||
|
||||
// Fallback to direct calculation
|
||||
for (const idToTry of identifiersToTry) {
|
||||
const userUuid = validateUuid(idToTry) ? idToTry : uuidv5(idToTry, UUID_NAMESPACE);
|
||||
|
||||
try {
|
||||
const { data: transactions, error } = await supabaseAdmin
|
||||
.from('point_transactions')
|
||||
.select('points')
|
||||
.eq('user_id', userUuid);
|
||||
|
||||
if (!error && transactions && transactions.length > 0) {
|
||||
const totalPoints = transactions.reduce((sum, tx) => sum + (tx.points || 0), 0);
|
||||
console.log(`Calculated ${totalPoints} points from ${transactions.length} transactions`);
|
||||
return totalPoints;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error calculating points for ID ${userUuid}:`, err);
|
||||
try {
|
||||
const { data: transactions, error } = await supabaseAdmin
|
||||
.from('point_transactions')
|
||||
.select('points')
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (!error && transactions && transactions.length > 0) {
|
||||
const totalPoints = transactions.reduce((sum, tx) => sum + (tx.points || 0), 0);
|
||||
console.log(`Calculated ${totalPoints} points from ${transactions.length} transactions`);
|
||||
return totalPoints;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error calculating points for ID ${userId}:`, err);
|
||||
}
|
||||
|
||||
// If all methods fail, return 0
|
||||
@ -234,51 +322,52 @@ export async function getUserTransactions(userId: string) {
|
||||
|
||||
console.log('Fetching transactions for user:', userId);
|
||||
|
||||
// Generate potential IDs to try
|
||||
const identifiersToTry = [userId];
|
||||
|
||||
// If userId is an email, add a UUID version
|
||||
if (userId.includes('@')) {
|
||||
const emailUuid = uuidv5(userId, UUID_NAMESPACE);
|
||||
identifiersToTry.push(emailUuid);
|
||||
}
|
||||
// If it's not a UUID, generate one
|
||||
else if (!validateUuid(userId)) {
|
||||
const generatedUuid = uuidv5(userId, UUID_NAMESPACE);
|
||||
identifiersToTry.push(generatedUuid);
|
||||
// If the userId is not a UUID and looks like an email, try to find by email
|
||||
if (!validateUuid(userId) && userId.includes('@')) {
|
||||
const user = await findUserByEmail(userId);
|
||||
if (user) {
|
||||
userId = user.id;
|
||||
console.log('Found user by email, using ID:', userId);
|
||||
} else {
|
||||
console.log('No user found by email, returning empty transactions');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Will try these identifiers for transaction lookup:', identifiersToTry);
|
||||
// Validate final user ID format
|
||||
if (!validateUuid(userId)) {
|
||||
console.warn('Invalid UUID format for user:', userId);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try each identifier
|
||||
for (const idToTry of identifiersToTry) {
|
||||
const userUuid = validateUuid(idToTry) ? idToTry : uuidv5(idToTry, UUID_NAMESPACE);
|
||||
// Try the RPC function first
|
||||
try {
|
||||
const { data: transactions, error: rpcError } = await supabaseAdmin.rpc('get_user_transactions', {
|
||||
user_id_param: userId
|
||||
});
|
||||
|
||||
try {
|
||||
// Try the RPC function first
|
||||
const { data: transactions, error: rpcError } = await supabaseAdmin.rpc('get_user_transactions', {
|
||||
user_id_param: userUuid
|
||||
});
|
||||
|
||||
if (!rpcError && transactions && transactions.length > 0) {
|
||||
console.log(`Found ${transactions.length} transactions for ${userUuid} via RPC`);
|
||||
return transactions;
|
||||
}
|
||||
|
||||
// Fallback to direct query
|
||||
const { data: directTransactions, error } = await supabaseAdmin
|
||||
.from('point_transactions')
|
||||
.select('*')
|
||||
.eq('user_id', userUuid)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (!error && directTransactions && directTransactions.length > 0) {
|
||||
console.log(`Found ${directTransactions.length} transactions for ${userUuid} via direct query`);
|
||||
return directTransactions;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error getting transactions for ID ${userUuid}:`, err);
|
||||
if (!rpcError && transactions && transactions.length > 0) {
|
||||
console.log(`Found ${transactions.length} transactions for ${userId} via RPC`);
|
||||
return transactions;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error getting transactions for ID ${userId} via RPC:`, err);
|
||||
}
|
||||
|
||||
// Fallback to direct query
|
||||
try {
|
||||
const { data: directTransactions, error } = await supabaseAdmin
|
||||
.from('point_transactions')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (!error && directTransactions && directTransactions.length > 0) {
|
||||
console.log(`Found ${directTransactions.length} transactions for ${userId} via direct query`);
|
||||
return directTransactions;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error getting transactions for ID ${userId} via direct query:`, err);
|
||||
}
|
||||
|
||||
// If no transactions found, return empty array
|
||||
@ -287,4 +376,49 @@ export async function getUserTransactions(userId: string) {
|
||||
console.log('Unexpected error in getUserTransactions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full user stats
|
||||
*/
|
||||
export async function getUserStats(userId: string) {
|
||||
try {
|
||||
if (!userId) {
|
||||
console.warn('Cannot get stats for undefined userId');
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the userId is not a UUID and looks like an email, try to find by email
|
||||
if (!validateUuid(userId) && userId.includes('@')) {
|
||||
const user = await findUserByEmail(userId);
|
||||
if (user) {
|
||||
userId = user.id;
|
||||
console.log('Found user by email, using ID:', userId);
|
||||
} else {
|
||||
console.log('No user found by email, returning null stats');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate final user ID format
|
||||
if (!validateUuid(userId)) {
|
||||
console.warn('Invalid UUID format for user:', userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get user stats using the RPC function
|
||||
const { data, error } = await supabaseAdmin.rpc('get_user_stats', {
|
||||
user_id_param: userId
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Error getting user stats:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in getUserStats:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,9 @@ DROP FUNCTION IF EXISTS ensure_user_exists;
|
||||
DROP FUNCTION IF EXISTS get_user_points_by_id;
|
||||
DROP FUNCTION IF EXISTS force_set_user_points;
|
||||
DROP FUNCTION IF EXISTS create_user_with_raw_sql;
|
||||
DROP FUNCTION IF EXISTS check_duplicate_upload;
|
||||
DROP FUNCTION IF EXISTS get_user_total_points;
|
||||
|
||||
-- The following command can be used to drop the user_points table after migrating all data
|
||||
-- The following commands can be used to drop the user_points table after migrating all data
|
||||
-- This is commented out as it should be run manually after ensuring successful data migration
|
||||
-- DROP TABLE IF EXISTS user_points;
|
||||
@ -1,25 +1,6 @@
|
||||
-- Additional SQL functions for the simplified points system
|
||||
-- Additional SQL functions for the points system
|
||||
-- This file is meant to be run after supabase-schema.sql
|
||||
|
||||
-- Function to check duplicate image uploads for a specific user
|
||||
CREATE OR REPLACE FUNCTION check_duplicate_upload(
|
||||
user_id_param TEXT,
|
||||
image_hash_param TEXT
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
exists_flag BOOLEAN;
|
||||
BEGIN
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM point_transactions
|
||||
WHERE user_id = user_id_param::UUID
|
||||
AND image_hash = image_hash_param
|
||||
) INTO exists_flag;
|
||||
|
||||
RETURN exists_flag;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Function to get transaction count for a user
|
||||
CREATE OR REPLACE FUNCTION get_user_transaction_count(
|
||||
user_id_param TEXT
|
||||
@ -27,10 +8,14 @@ CREATE OR REPLACE FUNCTION get_user_transaction_count(
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
count_result INTEGER;
|
||||
user_uuid UUID;
|
||||
BEGIN
|
||||
-- Convert to UUID
|
||||
user_uuid := user_id_param::UUID;
|
||||
|
||||
SELECT COUNT(*) INTO count_result
|
||||
FROM point_transactions
|
||||
WHERE user_id = user_id_param::UUID;
|
||||
WHERE user_id = user_uuid;
|
||||
|
||||
RETURN count_result;
|
||||
END;
|
||||
@ -43,10 +28,14 @@ CREATE OR REPLACE FUNCTION get_user_first_transaction_date(
|
||||
RETURNS TIMESTAMP WITH TIME ZONE AS $$
|
||||
DECLARE
|
||||
first_date TIMESTAMP WITH TIME ZONE;
|
||||
user_uuid UUID;
|
||||
BEGIN
|
||||
-- Convert to UUID
|
||||
user_uuid := user_id_param::UUID;
|
||||
|
||||
SELECT MIN(created_at) INTO first_date
|
||||
FROM point_transactions
|
||||
WHERE user_id = user_id_param::UUID;
|
||||
WHERE user_id = user_uuid;
|
||||
|
||||
RETURN first_date;
|
||||
END;
|
||||
@ -59,11 +48,121 @@ CREATE OR REPLACE FUNCTION get_user_latest_transaction_date(
|
||||
RETURNS TIMESTAMP WITH TIME ZONE AS $$
|
||||
DECLARE
|
||||
latest_date TIMESTAMP WITH TIME ZONE;
|
||||
user_uuid UUID;
|
||||
BEGIN
|
||||
-- Convert to UUID
|
||||
user_uuid := user_id_param::UUID;
|
||||
|
||||
SELECT MAX(created_at) INTO latest_date
|
||||
FROM point_transactions
|
||||
WHERE user_id = user_id_param::UUID;
|
||||
WHERE user_id = user_uuid;
|
||||
|
||||
RETURN latest_date;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Function to get user info by ID
|
||||
CREATE OR REPLACE FUNCTION get_user_info(
|
||||
user_id_param TEXT
|
||||
)
|
||||
RETURNS json AS $$
|
||||
DECLARE
|
||||
user_record users;
|
||||
user_uuid UUID;
|
||||
BEGIN
|
||||
-- Convert to UUID
|
||||
user_uuid := user_id_param::UUID;
|
||||
|
||||
SELECT * INTO user_record
|
||||
FROM users
|
||||
WHERE id = user_uuid;
|
||||
|
||||
IF FOUND THEN
|
||||
RETURN row_to_json(user_record);
|
||||
ELSE
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Function to get user stats (combines user info and point total)
|
||||
CREATE OR REPLACE FUNCTION get_user_stats(
|
||||
user_id_param TEXT
|
||||
)
|
||||
RETURNS json AS $$
|
||||
DECLARE
|
||||
user_info json;
|
||||
points_total INTEGER;
|
||||
transaction_count INTEGER;
|
||||
first_date TIMESTAMP WITH TIME ZONE;
|
||||
latest_date TIMESTAMP WITH TIME ZONE;
|
||||
user_uuid UUID;
|
||||
result json;
|
||||
BEGIN
|
||||
-- Convert to UUID
|
||||
user_uuid := user_id_param::UUID;
|
||||
|
||||
-- Get user info
|
||||
SELECT row_to_json(u) INTO user_info
|
||||
FROM users u
|
||||
WHERE id = user_uuid;
|
||||
|
||||
-- Get points total
|
||||
SELECT COALESCE(SUM(points), 0) INTO points_total
|
||||
FROM point_transactions
|
||||
WHERE user_id = user_uuid;
|
||||
|
||||
-- Get transaction count
|
||||
SELECT COUNT(*) INTO transaction_count
|
||||
FROM point_transactions
|
||||
WHERE user_id = user_uuid;
|
||||
|
||||
-- Get first and latest transaction dates
|
||||
SELECT MIN(created_at), MAX(created_at)
|
||||
INTO first_date, latest_date
|
||||
FROM point_transactions
|
||||
WHERE user_id = user_uuid;
|
||||
|
||||
-- Build result object
|
||||
SELECT json_build_object(
|
||||
'user', user_info,
|
||||
'points', points_total,
|
||||
'transaction_count', transaction_count,
|
||||
'first_transaction', first_date,
|
||||
'latest_transaction', latest_date
|
||||
) INTO result;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Migration helper function to transfer data from old schema to new schema
|
||||
-- This is only needed if you're migrating from the old schema
|
||||
CREATE OR REPLACE FUNCTION migrate_user_data(
|
||||
old_user_id_param TEXT,
|
||||
new_user_id_param TEXT,
|
||||
email_param TEXT,
|
||||
name_param TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
old_user_uuid UUID;
|
||||
new_user_uuid UUID;
|
||||
BEGIN
|
||||
-- Convert to UUIDs
|
||||
old_user_uuid := old_user_id_param::UUID;
|
||||
new_user_uuid := new_user_id_param::UUID;
|
||||
|
||||
-- Ensure new user exists
|
||||
INSERT INTO users (id, email, name, first_login, last_login)
|
||||
VALUES (new_user_uuid, email_param, name_param, now(), now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Update transactions to use new user ID
|
||||
UPDATE point_transactions
|
||||
SET user_id = new_user_uuid
|
||||
WHERE user_id = old_user_uuid;
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
@ -1,10 +1,19 @@
|
||||
-- Schema for Supabase to set up user points tracking
|
||||
-- This file is the main schema definition for the points system
|
||||
|
||||
-- Create users table to track user information
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
name TEXT,
|
||||
first_login TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
last_login TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create point_transactions table to track individual point awards
|
||||
CREATE TABLE IF NOT EXISTS point_transactions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id UUID NOT NULL,
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
points INTEGER NOT NULL,
|
||||
transaction_type TEXT NOT NULL,
|
||||
image_hash TEXT,
|
||||
@ -17,8 +26,43 @@ CREATE TABLE IF NOT EXISTS point_transactions (
|
||||
-- Create indexes for faster queries
|
||||
CREATE INDEX IF NOT EXISTS idx_point_transactions_user_id ON point_transactions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_point_transactions_image_hash ON point_transactions(image_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
|
||||
-- Create a function to create a point transaction
|
||||
-- Function to ensure a user exists in the database
|
||||
CREATE OR REPLACE FUNCTION ensure_user_exists(
|
||||
user_id_param TEXT,
|
||||
email_param TEXT,
|
||||
name_param TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
user_uuid UUID;
|
||||
exists_flag BOOLEAN;
|
||||
BEGIN
|
||||
-- Convert to UUID
|
||||
user_uuid := user_id_param::UUID;
|
||||
|
||||
-- Check if user exists
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM users WHERE id = user_uuid
|
||||
) INTO exists_flag;
|
||||
|
||||
-- If user doesn't exist, create them
|
||||
IF NOT exists_flag THEN
|
||||
INSERT INTO users (id, email, name, first_login, last_login)
|
||||
VALUES (user_uuid, email_param, name_param, now(), now());
|
||||
ELSE
|
||||
-- Update last_login
|
||||
UPDATE users
|
||||
SET last_login = now()
|
||||
WHERE id = user_uuid;
|
||||
END IF;
|
||||
|
||||
RETURN user_uuid;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Function to create a point transaction
|
||||
CREATE OR REPLACE FUNCTION create_point_transaction(
|
||||
user_id_param TEXT,
|
||||
points_param INTEGER,
|
||||
@ -31,7 +75,11 @@ CREATE OR REPLACE FUNCTION create_point_transaction(
|
||||
RETURNS json AS $$
|
||||
DECLARE
|
||||
new_transaction point_transactions;
|
||||
user_uuid UUID;
|
||||
BEGIN
|
||||
-- Convert to UUID
|
||||
user_uuid := user_id_param::UUID;
|
||||
|
||||
INSERT INTO point_transactions (
|
||||
user_id,
|
||||
points,
|
||||
@ -43,7 +91,7 @@ BEGIN
|
||||
created_at
|
||||
)
|
||||
VALUES (
|
||||
user_id_param::UUID,
|
||||
user_uuid,
|
||||
points_param,
|
||||
transaction_type_param,
|
||||
image_hash_param,
|
||||
@ -65,41 +113,54 @@ CREATE OR REPLACE FUNCTION get_user_total_points(
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
total INTEGER;
|
||||
user_uuid UUID;
|
||||
BEGIN
|
||||
-- Convert to UUID
|
||||
user_uuid := user_id_param::UUID;
|
||||
|
||||
SELECT COALESCE(SUM(points), 0) INTO total
|
||||
FROM point_transactions
|
||||
WHERE user_id = user_id_param::UUID;
|
||||
WHERE user_id = user_uuid;
|
||||
|
||||
RETURN total;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Create a function to get user transactions
|
||||
-- Function to get user transactions
|
||||
CREATE OR REPLACE FUNCTION get_user_transactions(
|
||||
user_id_param TEXT
|
||||
)
|
||||
RETURNS SETOF point_transactions AS $$
|
||||
DECLARE
|
||||
user_uuid UUID;
|
||||
BEGIN
|
||||
-- Convert to UUID
|
||||
user_uuid := user_id_param::UUID;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT *
|
||||
FROM point_transactions
|
||||
WHERE user_id = user_id_param::UUID
|
||||
WHERE user_id = user_uuid
|
||||
ORDER BY created_at DESC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Check if an image has already been processed for a user
|
||||
CREATE OR REPLACE FUNCTION check_image_exists(
|
||||
-- Function to check if an image has already been processed for a user
|
||||
CREATE OR REPLACE FUNCTION check_duplicate_upload(
|
||||
user_id_param TEXT,
|
||||
image_hash_param TEXT
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
exists_flag BOOLEAN;
|
||||
user_uuid UUID;
|
||||
BEGIN
|
||||
-- Convert to UUID
|
||||
user_uuid := user_id_param::UUID;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM point_transactions
|
||||
WHERE user_id = user_id_param::UUID
|
||||
WHERE user_id = user_uuid
|
||||
AND image_hash = image_hash_param
|
||||
) INTO exists_flag;
|
||||
|
||||
@ -107,11 +168,41 @@ BEGIN
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Function to find a user by email
|
||||
CREATE OR REPLACE FUNCTION find_user_by_email(
|
||||
email_param TEXT
|
||||
)
|
||||
RETURNS json AS $$
|
||||
DECLARE
|
||||
user_record users;
|
||||
BEGIN
|
||||
SELECT * INTO user_record
|
||||
FROM users
|
||||
WHERE email = email_param;
|
||||
|
||||
IF FOUND THEN
|
||||
RETURN row_to_json(user_record);
|
||||
ELSE
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Enable Row Level Security
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE point_transactions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create policies section - uncomment if you need to recreate policies
|
||||
/*
|
||||
-- Users policies
|
||||
CREATE POLICY "Users can view their own profile"
|
||||
ON users FOR SELECT
|
||||
USING (auth.uid() = id);
|
||||
|
||||
CREATE POLICY "Service role can manage all users"
|
||||
ON users FOR ALL
|
||||
USING (auth.role() = 'service_role');
|
||||
|
||||
-- Transaction policies
|
||||
CREATE POLICY "Users can view their own transactions"
|
||||
ON point_transactions FOR SELECT
|
||||
|
||||
Loading…
Reference in New Issue
Block a user