snapshot of https://github.com/marmelab/atomic-crm.git (main, 2026-09-18; blobs > 131072B omitted)
Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
This commit is contained in:
80
supabase/functions/_shared/authentication.ts
Normal file
80
supabase/functions/_shared/authentication.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// Based on https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/_shared/jwt/default.ts
|
||||
import * as jose from "jsr:@panva/jose@6";
|
||||
import { createClient, type User } from "jsr:@supabase/supabase-js@2";
|
||||
import { createErrorResponse } from "./utils.ts";
|
||||
|
||||
const SUPABASE_JWT_ISSUER =
|
||||
Deno.env.get("SB_JWT_ISSUER") ?? Deno.env.get("SUPABASE_URL") + "/auth/v1";
|
||||
|
||||
const SUPABASE_JWT_KEYS = jose.createRemoteJWKSet(
|
||||
new URL(Deno.env.get("SUPABASE_URL")! + "/auth/v1/.well-known/jwks.json"),
|
||||
);
|
||||
|
||||
function getAuthToken(req: Request) {
|
||||
const authHeader = req.headers.get("authorization");
|
||||
if (!authHeader) {
|
||||
throw new Error("Missing authorization header");
|
||||
}
|
||||
const [bearer, token] = authHeader.split(" ");
|
||||
if (bearer !== "Bearer") {
|
||||
throw new Error(`Auth header is not 'Bearer {token}'`);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
function verifySupabaseJWT(jwt: string) {
|
||||
return jose.jwtVerify(jwt, SUPABASE_JWT_KEYS, {
|
||||
issuer: SUPABASE_JWT_ISSUER,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the Authorization header to ensure that a user is authenticated.
|
||||
*/
|
||||
export const AuthMiddleware = async (
|
||||
req: Request,
|
||||
next: (req: Request) => Promise<Response>,
|
||||
) => {
|
||||
if (req.method === "OPTIONS") return await next(req);
|
||||
|
||||
try {
|
||||
const token = getAuthToken(req);
|
||||
const isValidJWT = await verifySupabaseJWT(token);
|
||||
|
||||
if (isValidJWT) return await next(req);
|
||||
|
||||
return createErrorResponse(401, "Invalid authentication");
|
||||
} catch (e) {
|
||||
return createErrorResponse(401, e?.toString() || "Unauthorized");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the authenticated user using the authorization header.
|
||||
* User will be undefined for OPTIONS requests.
|
||||
*/
|
||||
export const UserMiddleware = async (
|
||||
req: Request,
|
||||
next: (req: Request, user?: User) => Promise<Response>,
|
||||
) => {
|
||||
if (req.method === "OPTIONS") return await next(req);
|
||||
|
||||
try {
|
||||
const authHeader = req.headers.get("Authorization")!;
|
||||
const localClient = createClient(
|
||||
Deno.env.get("SUPABASE_URL") ?? "",
|
||||
Deno.env.get("SB_PUBLISHABLE_KEY") ?? "",
|
||||
{ global: { headers: { Authorization: authHeader } } },
|
||||
);
|
||||
|
||||
const { data, error: authError } = await localClient.auth.getUser();
|
||||
if (!data?.user || authError) {
|
||||
return createErrorResponse(401, "Unauthorized");
|
||||
}
|
||||
|
||||
return next(req, data.user);
|
||||
} catch (err) {
|
||||
return createErrorResponse(401, err?.toString() || "Unauthorized");
|
||||
}
|
||||
};
|
||||
20
supabase/functions/_shared/cors.ts
Normal file
20
supabase/functions/_shared/cors.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers":
|
||||
"authorization, x-client-info, apikey, content-type",
|
||||
"Access-Control-Allow-Methods": "POST, PATCH, DELETE",
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle OPTIONS requests for CORS preflight.
|
||||
*/
|
||||
export function OptionsMiddleware(
|
||||
req: Request,
|
||||
next: (req: Request) => Promise<Response>,
|
||||
) {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { status: 204, headers: corsHeaders });
|
||||
}
|
||||
|
||||
return next(req);
|
||||
}
|
||||
187
supabase/functions/_shared/db.ts
Normal file
187
supabase/functions/_shared/db.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type {
|
||||
DatabaseConnection,
|
||||
Driver,
|
||||
QueryResult,
|
||||
} from "https://esm.sh/kysely@0.27.2";
|
||||
import {
|
||||
CompiledQuery,
|
||||
Kysely,
|
||||
PostgresAdapter,
|
||||
PostgresIntrospector,
|
||||
PostgresQueryCompiler,
|
||||
type Generated,
|
||||
} from "https://esm.sh/kysely@0.27.2";
|
||||
|
||||
export { CompiledQuery };
|
||||
import type { PoolClient } from "https://deno.land/x/postgres@v0.17.0/mod.ts";
|
||||
import { Pool } from "https://deno.land/x/postgres@v0.17.0/mod.ts";
|
||||
|
||||
// Database schema types
|
||||
export interface ContactsTable {
|
||||
id: Generated<number>;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
gender: string | null;
|
||||
title: string | null;
|
||||
email_jsonb: unknown | null; // JSONB array
|
||||
phone_jsonb: unknown | null; // JSONB array
|
||||
background: string | null;
|
||||
avatar: unknown | null; // JSONB
|
||||
first_seen: Date | null;
|
||||
last_seen: Date | null;
|
||||
has_newsletter: boolean | null;
|
||||
status: string | null;
|
||||
tags: number[] | null;
|
||||
company_id: number | null;
|
||||
sales_id: number | null;
|
||||
linkedin_url: string | null;
|
||||
}
|
||||
|
||||
interface TasksTable {
|
||||
id: Generated<number>;
|
||||
contact_id: number;
|
||||
type: string | null;
|
||||
text: string | null;
|
||||
due_date: Date;
|
||||
done_date: Date | null;
|
||||
sales_id: number | null;
|
||||
}
|
||||
|
||||
interface ContactNotesTable {
|
||||
id: Generated<number>;
|
||||
contact_id: number;
|
||||
text: string | null;
|
||||
date: Date | null;
|
||||
sales_id: number | null;
|
||||
status: string | null;
|
||||
attachments: unknown[] | null; // JSONB array
|
||||
}
|
||||
|
||||
interface DealsTable {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
company_id: number | null;
|
||||
contact_ids: number[];
|
||||
category: string | null;
|
||||
stage: string;
|
||||
description: string | null;
|
||||
amount: number | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
archived_at: Date | null;
|
||||
expected_closing_date: Date | null;
|
||||
sales_id: number | null;
|
||||
index: number | null;
|
||||
}
|
||||
|
||||
interface Database {
|
||||
contacts: ContactsTable;
|
||||
tasks: TasksTable;
|
||||
contact_notes: ContactNotesTable;
|
||||
deals: DealsTable;
|
||||
}
|
||||
|
||||
// Deno Postgres Driver for Kysely
|
||||
class DenoPostgresDriver implements Driver {
|
||||
private pool: Pool;
|
||||
private connections = new WeakMap<PoolClient, DatabaseConnection>();
|
||||
|
||||
constructor(pool: Pool) {
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
// Connection pool is already initialized
|
||||
}
|
||||
|
||||
async acquireConnection(): Promise<DatabaseConnection> {
|
||||
const client = await this.pool.connect();
|
||||
let connection = this.connections.get(client);
|
||||
|
||||
if (!connection) {
|
||||
connection = new DenoPostgresConnection(client);
|
||||
this.connections.set(client, connection);
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("begin"));
|
||||
}
|
||||
|
||||
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("commit"));
|
||||
}
|
||||
|
||||
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("rollback"));
|
||||
}
|
||||
|
||||
async releaseConnection(connection: DatabaseConnection): Promise<void> {
|
||||
(connection as DenoPostgresConnection).release();
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
await this.pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
class DenoPostgresConnection implements DatabaseConnection {
|
||||
private client: PoolClient;
|
||||
|
||||
constructor(client: PoolClient) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
|
||||
const result = await this.client.queryObject<O>({
|
||||
text: compiledQuery.sql,
|
||||
args: compiledQuery.parameters as unknown[],
|
||||
});
|
||||
|
||||
if (
|
||||
result.command === "INSERT" ||
|
||||
result.command === "UPDATE" ||
|
||||
result.command === "DELETE"
|
||||
) {
|
||||
return {
|
||||
numAffectedRows: BigInt(result.rowCount ?? 0),
|
||||
rows: result.rows ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rows: result.rows ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
streamQuery<O>(
|
||||
_compiledQuery: CompiledQuery,
|
||||
_chunkSize?: number,
|
||||
): AsyncIterableIterator<QueryResult<O>> {
|
||||
throw new Error("Deno Postgres driver does not support streaming");
|
||||
}
|
||||
|
||||
release() {
|
||||
this.client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Create connection pool
|
||||
// Use SUPABASE_DB_URL if available (production), otherwise fall back to local dev connection string
|
||||
const connectionString =
|
||||
Deno.env.get("SUPABASE_DB_URL") ||
|
||||
"postgresql://postgres:postgres@db:5432/postgres";
|
||||
|
||||
const pool = new Pool(connectionString, 1); // Single connection for edge functions
|
||||
|
||||
// Create and export Kysely instance
|
||||
export const db = new Kysely<Database>({
|
||||
dialect: {
|
||||
createAdapter: () => new PostgresAdapter(),
|
||||
createDriver: () => new DenoPostgresDriver(pool),
|
||||
createIntrospector: (db: Kysely<any>) => new PostgresIntrospector(db),
|
||||
createQueryCompiler: () => new PostgresQueryCompiler(),
|
||||
},
|
||||
});
|
||||
15
supabase/functions/_shared/getUserSale.ts
Normal file
15
supabase/functions/_shared/getUserSale.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { type User } from "jsr:@supabase/supabase-js@2";
|
||||
import { supabaseAdmin } from "./supabaseAdmin.ts";
|
||||
|
||||
/**
|
||||
* Get the sale associated to the provided user.
|
||||
*/
|
||||
export const getUserSale = async (user: User) => {
|
||||
return (
|
||||
await supabaseAdmin
|
||||
.from("sales")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.single()
|
||||
)?.data;
|
||||
};
|
||||
15
supabase/functions/_shared/supabaseAdmin.ts
Normal file
15
supabase/functions/_shared/supabaseAdmin.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { SupabaseClient } from "jsr:@supabase/supabase-js@2";
|
||||
import { createClient } from "jsr:@supabase/supabase-js@2";
|
||||
|
||||
export const supabaseAdmin: SupabaseClient = createClient(
|
||||
Deno.env.get("SUPABASE_URL") ?? "",
|
||||
// TODO - When Supabase finally provides the project's secret key to the functions, we'll need to use it here instead of the service role key.
|
||||
// See https://supabase.com/docs/guides/functions/auth#get-api-details
|
||||
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
|
||||
{
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
12
supabase/functions/_shared/utils.ts
Normal file
12
supabase/functions/_shared/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { corsHeaders } from "./cors.ts";
|
||||
|
||||
export function createErrorResponse(
|
||||
status: number,
|
||||
message: string,
|
||||
custom: Record<string, unknown> = {},
|
||||
) {
|
||||
return new Response(JSON.stringify({ status, message, ...custom }), {
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders },
|
||||
status,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user