Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent channel deletion when unsetting device channel #1012

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Binary file modified bun.lockb
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
"husky": "^9.1.7",
"miniflare": "^3.20250124.0",
"sass": "1.83.4",
"supabase": "1.127.3",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do no add this

"tailwindcss": "^4.0.0",
"typescript": "5.7.3",
"unplugin-icons": "22.0.0",
Expand Down
14 changes: 7 additions & 7 deletions supabase/functions/_backend/plugins/channel_self.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not modify this file

Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { Context } from '@hono/hono'
import type { DeviceWithoutCreatedAt } from '../utils/stats.ts'
import type { Database } from '../utils/supabase.types.ts'
import type { AppInfos } from '../utils/types.ts'
import { Hono } from '@hono/hono'
import { format, tryParse } from '@std/semver'
import { Hono } from 'hono/tiny'
import { z } from 'zod'
import { BRES, getBody } from '../utils/hono.ts'
import { sendStatsAndDevice } from '../utils/stats.ts'
Expand Down Expand Up @@ -49,7 +49,7 @@ export const jsonRequestSchema = z.object({
return val
})

async function post(c: Context, body: DeviceLink): Promise<Response> {
async function post(c: Context<any, any, unknown>, body: DeviceLink): Promise<Response> {
console.log({ requestId: c.get('requestId'), context: 'post channel self body', body })
const parseResult = jsonRequestSchema.safeParse(body)
if (!parseResult.success) {
Expand Down Expand Up @@ -265,7 +265,7 @@ async function post(c: Context, body: DeviceLink): Promise<Response> {
return c.json(BRES)
}

async function put(c: Context, body: DeviceLink): Promise<Response> {
async function put(c: Context<any, any, unknown>, body: DeviceLink): Promise<Response> {
console.log({ requestId: c.get('requestId'), context: 'put channel self body', body })
let {
version_name,
Expand Down Expand Up @@ -408,7 +408,7 @@ async function put(c: Context, body: DeviceLink): Promise<Response> {
}, 400)
}

async function deleteOverride(c: Context, body: DeviceLink): Promise<Response> {
async function deleteOverride(c: Context<any, any, unknown>, body: DeviceLink): Promise<Response> {
console.log({ requestId: c.get('requestId'), context: 'delete channel self body', body })
let {
version_build,
Expand Down Expand Up @@ -473,7 +473,7 @@ export const app = new Hono()

app.post('/', async (c: Context) => {
try {
const body = await c.req.json<DeviceLink>()
const body = await c.req.json() as DeviceLink
console.log({ requestId: c.get('requestId'), context: 'post body', body })
return post(c, body)
}
Expand All @@ -485,7 +485,7 @@ app.post('/', async (c: Context) => {
app.put('/', async (c: Context) => {
// Used as get, should be refactor with query param instead
try {
const body = await c.req.json<DeviceLink>()
const body = await c.req.json() as DeviceLink
console.log({ requestId: c.get('requestId'), context: 'put body', body })
return put(c, body)
}
Expand All @@ -496,7 +496,7 @@ app.put('/', async (c: Context) => {

app.delete('/', async (c: Context) => {
try {
const body = await getBody<DeviceLink>(c)
const body = await getBody(c) as DeviceLink
// const body = await c.req.json<DeviceLink>()
console.log({ requestId: c.get('requestId'), context: 'delete body', body })
return deleteOverride(c, body)
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do no rename

File renamed without changes.
37 changes: 37 additions & 0 deletions supabase/migrations/20240101000002_fix_channel_deletion.sql
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain your changes

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- Drop existing constraint
ALTER TABLE "public"."channel_devices"
DROP CONSTRAINT IF EXISTS "channel_devices_channel_id_fkey";

-- Re-add constraint with ON DELETE SET NULL
ALTER TABLE "public"."channel_devices"
ADD CONSTRAINT "channel_devices_channel_id_fkey"
FOREIGN KEY ("channel_id")
REFERENCES "public"."channels"("id")
ON DELETE SET NULL;

-- Add NOT NULL constraint to prevent accidental deletions
ALTER TABLE "public"."channels"
ALTER COLUMN "name" SET NOT NULL,
ALTER COLUMN "app_id" SET NOT NULL;

-- Add explicit deletion protection
CREATE OR REPLACE FUNCTION prevent_channel_deletion()
RETURNS TRIGGER AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM public.channel_devices
WHERE channel_id = OLD.id
LIMIT 1
) THEN
RAISE EXCEPTION 'Cannot delete channel while devices are associated with it';
END IF;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS prevent_channel_deletion_trigger ON public.channels;
CREATE TRIGGER prevent_channel_deletion_trigger
BEFORE DELETE ON public.channels
FOR EACH ROW
EXECUTE FUNCTION prevent_channel_deletion();
72 changes: 72 additions & 0 deletions supabase/migrations/20240101000003_add_seed_function.sql
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain your changes

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
CREATE OR REPLACE FUNCTION public.reset_and_seed_app_data(p_app_id character varying) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
org_id uuid := '046a36ac-e03c-4590-9257-bd6c9dba9ee8';
user_id uuid := '6aa76066-55ef-4238-ade6-0b32334a4097';
max_version_id bigint;
max_channel_id bigint;
BEGIN
-- Lock the tables to prevent concurrent inserts
LOCK TABLE app_versions, channels IN EXCLUSIVE MODE;

-- Delete existing data for the specified app_id
DELETE FROM channels WHERE app_id = p_app_id;
DELETE FROM app_versions WHERE app_id = p_app_id;
DELETE FROM apps WHERE app_id = p_app_id;

-- Get the current max ids and reset the sequences
SELECT COALESCE(MAX(id), 0) + 1 INTO max_version_id FROM app_versions;
SELECT COALESCE(MAX(id), 0) + 1 INTO max_channel_id FROM channels;

-- Reset both sequences
PERFORM setval('app_versions_id_seq', max_version_id, false);
PERFORM setval('channel_id_seq', max_channel_id, false);

-- Insert new app data
INSERT INTO apps (created_at, app_id, icon_url, name, last_version, updated_at, owner_org, user_id)
VALUES (now(), p_app_id, '', 'Seeded App', '1.0.0', now(), org_id, user_id);

-- Insert app versions in a single statement
WITH inserted_versions AS (
INSERT INTO app_versions (created_at, app_id, name, r2_path, updated_at, deleted, external_url, checksum, storage_provider, owner_org)
VALUES
(now(), p_app_id, 'builtin', NULL, now(), 't', NULL, NULL, 'supabase', org_id),
(now(), p_app_id, 'unknown', NULL, now(), 't', NULL, NULL, 'supabase', org_id),
(now(), p_app_id, '1.0.1', 'orgs/'||org_id||'/apps/'||p_app_id||'/1.0.1.zip', now(), 'f', NULL, '', 'r2-direct', org_id),
(now(), p_app_id, '1.0.0', 'orgs/'||org_id||'/apps/'||p_app_id||'/1.0.0.zip', now(), 'f', NULL, '3885ee49', 'r2', org_id),
(now(), p_app_id, '1.361.0', 'orgs/'||org_id||'/apps/'||p_app_id||'/1.361.0.zip', now(), 'f', NULL, '9d4f798a', 'r2', org_id),
(now(), p_app_id, '1.360.0', 'orgs/'||org_id||'/apps/'||p_app_id||'/1.360.0.zip', now(), 'f', NULL, '44913a9f', 'r2', org_id),
(now(), p_app_id, '1.359.0', 'orgs/'||org_id||'/apps/'||p_app_id||'/1.359.0.zip', now(), 'f', NULL, '9f74e70a', 'r2', org_id)
RETURNING id, name
)
-- Insert channels using the version IDs from the CTE
INSERT INTO channels (created_at, name, app_id, version, updated_at, public, disable_auto_update_under_native, disable_auto_update, ios, android, allow_device_self_set, allow_emulator, allow_dev, owner_org)
SELECT
now(),
c.name,
p_app_id,
v.id,
now(),
c.is_public,
't',
'major',
c.ios,
c.android,
't',
't',
't',
org_id
FROM (
VALUES
('production', '1.0.0', true, false, true),
('no_access', '1.361.0', false, true, true),
('two_default', '1.0.0', true, true, false)
) as c(name, version_name, is_public, ios, android)
JOIN inserted_versions v ON v.name = c.version_name;

END;
$$;

REVOKE ALL ON FUNCTION public.reset_and_seed_app_data(p_app_id character varying) FROM PUBLIC;
GRANT ALL ON FUNCTION public.reset_and_seed_app_data(p_app_id character varying) TO service_role;
Loading
Loading