- Replace legacy directory structure (api/, db/, functions/, logs/, pooler/) with single docker-compose.yml based self-hosted setup - Add selfhosted-supabase-mcp TypeScript MCP server for database management - Add .dockerignore for Docker build context - Update .gitignore to exclude .env files, volumes/, backups, logs
142 lines
7.1 KiB
TypeScript
142 lines
7.1 KiB
TypeScript
import { z } from 'zod';
|
|
import type { ToolContext, ToolPrivilegeLevel } from './types.js';
|
|
import { handleSqlResponse } from './utils.js';
|
|
import type { PoolClient } from 'pg';
|
|
import type { SqlSuccessResponse, AuthUser } from '../types/index.js'; // Import AuthUser
|
|
|
|
// Input schema
|
|
const CreateAuthUserInputSchema = z.object({
|
|
email: z.string().email('Invalid email address').describe('The email address for the new user.'),
|
|
password: z.string().min(6, 'Password must be at least 6 characters').describe('Plain text password (min 6 chars). WARNING: Insecure.'),
|
|
role: z.optional(z.string()).describe('User role.'),
|
|
app_metadata: z.optional(z.record(z.string(), z.unknown())).describe('Optional app metadata.'),
|
|
user_metadata: z.optional(z.record(z.string(), z.unknown())).describe('Optional user metadata.'),
|
|
});
|
|
type CreateAuthUserInput = z.infer<typeof CreateAuthUserInputSchema>;
|
|
|
|
// Output schema - Zod validation for the created user (should match AuthUser structure)
|
|
const CreatedAuthUserZodSchema = z.object({
|
|
id: z.string().uuid(),
|
|
email: z.string().email('Invalid email').nullable(),
|
|
role: z.string().nullable(),
|
|
created_at: z.string().nullable(),
|
|
last_sign_in_at: z.string().nullable(), // Will likely be null on creation
|
|
raw_app_meta_data: z.record(z.string(), z.unknown()).nullable(),
|
|
raw_user_meta_data: z.record(z.string(), z.unknown()).nullable(),
|
|
// Add other fields returned by the INSERT if necessary
|
|
});
|
|
// Use AuthUser for the output type hint
|
|
type CreateAuthUserOutput = AuthUser;
|
|
|
|
// Static JSON Schema for MCP capabilities
|
|
const mcpInputSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
email: { type: 'string', format: 'email', description: 'The email address for the new user.' },
|
|
password: { type: 'string', minLength: 6, description: 'Plain text password (min 6 chars). WARNING: Insecure.' },
|
|
role: { type: 'string', default: 'authenticated', description: 'User role.' },
|
|
user_metadata: { type: 'object', description: 'Optional user metadata.' },
|
|
app_metadata: { type: 'object', description: 'Optional app metadata.' },
|
|
},
|
|
required: ['email', 'password'],
|
|
};
|
|
|
|
// Tool definition
|
|
export const createAuthUserTool = {
|
|
name: 'create_auth_user',
|
|
description: 'Creates a new user directly in auth.users. WARNING: Requires plain password, insecure. Use with extreme caution.',
|
|
privilegeLevel: 'privileged' as ToolPrivilegeLevel,
|
|
inputSchema: CreateAuthUserInputSchema,
|
|
mcpInputSchema: mcpInputSchema, // Ensure defined above
|
|
outputSchema: CreatedAuthUserZodSchema,
|
|
|
|
execute: async (input: CreateAuthUserInput, context: ToolContext): Promise<CreateAuthUserOutput> => { // Use CreateAuthUserOutput
|
|
const client = context.selfhostedClient;
|
|
const { email, password, role, app_metadata, user_metadata } = input;
|
|
|
|
// Direct DB connection is absolutely required for this direct insert
|
|
if (!client.isPgAvailable()) {
|
|
context.log('Direct database connection (DATABASE_URL) is required to create an auth user directly.', 'error');
|
|
throw new Error('Direct database connection (DATABASE_URL) is required to create an auth user directly.');
|
|
}
|
|
|
|
context.log(`Creating user ${email}...`, 'info');
|
|
|
|
// Use transaction to ensure atomicity and get pg client
|
|
const createdUser = await client.executeTransactionWithPg(async (pgClient: PoolClient) => {
|
|
// Check if pgcrypto extension is available (needed for crypt)
|
|
try {
|
|
await pgClient.query("SELECT crypt('test', gen_salt('bf'))");
|
|
} catch (err) {
|
|
throw new Error('Failed to execute crypt function. Ensure pgcrypto extension is enabled in the database.');
|
|
}
|
|
|
|
// Construct the INSERT statement with parameterization
|
|
const sql = `
|
|
INSERT INTO auth.users (
|
|
instance_id, email, encrypted_password, role,
|
|
raw_app_meta_data, raw_user_meta_data,
|
|
aud, email_confirmed_at, confirmation_sent_at -- Set required defaults
|
|
)
|
|
VALUES (
|
|
COALESCE(current_setting('app.instance_id', TRUE), '00000000-0000-0000-0000-000000000000')::uuid,
|
|
$1, crypt($2, gen_salt('bf')),
|
|
$3,
|
|
$4::jsonb,
|
|
$5::jsonb,
|
|
'authenticated', now(), now()
|
|
)
|
|
RETURNING id, email, role, raw_app_meta_data, raw_user_meta_data, created_at::text, last_sign_in_at::text;
|
|
`;
|
|
|
|
const params = [
|
|
email,
|
|
password,
|
|
role || 'authenticated', // Default role
|
|
JSON.stringify(app_metadata || {}),
|
|
JSON.stringify(user_metadata || {})
|
|
];
|
|
|
|
try {
|
|
const result = await pgClient.query(sql, params);
|
|
if (result.rows.length === 0) {
|
|
throw new Error('User creation failed, no user returned after insert.');
|
|
}
|
|
return CreatedAuthUserZodSchema.parse(result.rows[0]);
|
|
} catch (dbError: unknown) {
|
|
let errorMessage = 'Unknown database error during user creation';
|
|
|
|
if (typeof dbError === 'object' && dbError !== null && 'code' in dbError) {
|
|
// Safely extract code and message with proper type narrowing
|
|
const errorCode = String((dbError as { code: unknown }).code);
|
|
const errorMsg = 'message' in dbError && typeof (dbError as { message: unknown }).message === 'string'
|
|
? (dbError as { message: string }).message
|
|
: undefined;
|
|
|
|
// Check PG error code for unique violation
|
|
if (errorCode === '23505') {
|
|
errorMessage = `User creation failed: Email '${email}' likely already exists.`;
|
|
} else if (errorMsg) {
|
|
errorMessage = `Database error (${errorCode}): ${errorMsg}`;
|
|
} else {
|
|
errorMessage = `Database error code: ${errorCode}`;
|
|
}
|
|
} else if (dbError instanceof Error) {
|
|
errorMessage = `Database error during user creation: ${dbError.message}`;
|
|
} else {
|
|
errorMessage = `Database error during user creation: ${String(dbError)}`;
|
|
}
|
|
|
|
// Log sanitized error (not full object to avoid leaking sensitive info)
|
|
console.error('Error creating user in DB:', errorMessage);
|
|
|
|
// Throw a specific error message
|
|
throw new Error(errorMessage);
|
|
}
|
|
});
|
|
|
|
console.error(`Successfully created user ${email} with ID ${createdUser.id}.`);
|
|
context.log(`Successfully created user ${email} with ID ${createdUser.id}.`);
|
|
return createdUser; // Matches CreateAuthUserOutput (AuthUser)
|
|
},
|
|
};
|