- DB: audit_log 테이블(diff 포함) + subscriptions.admin_note + super_admin role - Edge Functions 4개: admin-users, admin-subscriptions, admin-payments, admin-audit-log - 공유 유틸: admin-auth.ts(권한 검증), audit.ts(감사로그 기록) - Swagger UI: 독립 정적 페이지 + OpenAPI 3.0 spec - CRUD 페이지: 구독 생성/수정/삭제, role 변경, 감사로그 목록/상세 - recharts: feature별 StackedBar + DAU Line + Top Users HorizontalBar - 결제 이력: DB + Payple API 병행 조회 - 권한: super_admin만 위험 작업, admin은 조회 전용 - RLS: admin/super_admin IN 정책 + super_admin 쓰기 정책 - SQL RPC: admin_usage_by_feature, admin_top_users, admin_dau
138 lines
5.9 KiB
PL/PgSQL
138 lines
5.9 KiB
PL/PgSQL
-- ============================================================================
|
|
-- Phase V2-6: Admin CRM 고도화 — 권한 확장 + 감사로그 + admin_note
|
|
-- ============================================================================
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- 1. profiles.role CHECK 확장: super_admin 추가
|
|
-- ----------------------------------------------------------------------------
|
|
ALTER TABLE public.profiles DROP CONSTRAINT IF EXISTS profiles_role_check;
|
|
ALTER TABLE public.profiles
|
|
ADD CONSTRAINT profiles_role_check
|
|
CHECK (role IN ('user', 'admin', 'super_admin'));
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- 2. audit_log 테이블 — 관리자 작업 감사 기록 (before/after diff 포함)
|
|
-- ----------------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS public.audit_log (
|
|
id bigserial PRIMARY KEY,
|
|
admin_id uuid NOT NULL REFERENCES auth.users(id),
|
|
action text NOT NULL,
|
|
target_type text NOT NULL,
|
|
target_id uuid NOT NULL,
|
|
before_data jsonb,
|
|
after_data jsonb,
|
|
memo text NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_target ON public.audit_log(target_type, target_id);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_admin ON public.audit_log(admin_id);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_date ON public.audit_log(created_at DESC);
|
|
|
|
-- audit_log RLS
|
|
ALTER TABLE public.audit_log ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "admin_read_audit_log" ON public.audit_log
|
|
FOR SELECT TO authenticated
|
|
USING ((auth.jwt() -> 'app_metadata' ->> 'role') IN ('admin', 'super_admin'));
|
|
|
|
-- INSERT는 service_role만 (Edge Function에서 기록)
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- 3. subscriptions.admin_note 컬럼
|
|
-- ----------------------------------------------------------------------------
|
|
ALTER TABLE public.subscriptions
|
|
ADD COLUMN IF NOT EXISTS admin_note text;
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- 4. 기존 RLS 정책 업데이트 — admin OR super_admin
|
|
-- ----------------------------------------------------------------------------
|
|
DROP POLICY IF EXISTS "admin_read_all_profiles" ON public.profiles;
|
|
CREATE POLICY "admin_read_all_profiles" ON public.profiles
|
|
FOR SELECT TO authenticated
|
|
USING ((auth.jwt() -> 'app_metadata' ->> 'role') IN ('admin', 'super_admin'));
|
|
|
|
DROP POLICY IF EXISTS "admin_read_all_subscriptions" ON public.subscriptions;
|
|
CREATE POLICY "admin_read_all_subscriptions" ON public.subscriptions
|
|
FOR SELECT TO authenticated
|
|
USING ((auth.jwt() -> 'app_metadata' ->> 'role') IN ('admin', 'super_admin'));
|
|
|
|
DROP POLICY IF EXISTS "admin_read_all_daily_usage" ON public.daily_usage;
|
|
CREATE POLICY "admin_read_all_daily_usage" ON public.daily_usage
|
|
FOR SELECT TO authenticated
|
|
USING ((auth.jwt() -> 'app_metadata' ->> 'role') IN ('admin', 'super_admin'));
|
|
|
|
-- subscriptions: super_admin 쓰기 정책
|
|
DROP POLICY IF EXISTS "super_admin_write_subscriptions" ON public.subscriptions;
|
|
CREATE POLICY "super_admin_write_subscriptions" ON public.subscriptions
|
|
FOR ALL TO authenticated
|
|
USING ((auth.jwt() -> 'app_metadata' ->> 'role') = 'super_admin')
|
|
WITH CHECK ((auth.jwt() -> 'app_metadata' ->> 'role') = 'super_admin');
|
|
|
|
-- profiles: super_admin이 role 컬럼 수정 가능
|
|
DROP POLICY IF EXISTS "super_admin_update_profiles" ON public.profiles;
|
|
CREATE POLICY "super_admin_update_profiles" ON public.profiles
|
|
FOR UPDATE TO authenticated
|
|
USING ((auth.jwt() -> 'app_metadata' ->> 'role') = 'super_admin')
|
|
WITH CHECK ((auth.jwt() -> 'app_metadata' ->> 'role') = 'super_admin');
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- 5. 통계 RPC 함수 — admin 전용
|
|
-- ----------------------------------------------------------------------------
|
|
|
|
-- 5.1 일별 feature 집계
|
|
CREATE OR REPLACE FUNCTION public.admin_usage_by_feature(
|
|
p_from date, p_to date
|
|
) RETURNS TABLE(date date, feature text, total_count bigint, unique_users bigint)
|
|
LANGUAGE sql SECURITY DEFINER STABLE
|
|
SET search_path = public
|
|
AS $$
|
|
SELECT du.date, du.feature,
|
|
SUM(du.count)::bigint AS total_count,
|
|
COUNT(DISTINCT du.user_id)::bigint AS unique_users
|
|
FROM public.daily_usage du
|
|
WHERE du.date BETWEEN p_from AND p_to
|
|
GROUP BY du.date, du.feature
|
|
ORDER BY du.date, du.feature;
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.admin_usage_by_feature(date, date) FROM public;
|
|
GRANT EXECUTE ON FUNCTION public.admin_usage_by_feature(date, date) TO authenticated;
|
|
|
|
-- 5.2 유저별 사용량 랭킹
|
|
CREATE OR REPLACE FUNCTION public.admin_top_users(
|
|
p_from date, p_to date, p_limit integer DEFAULT 20
|
|
) RETURNS TABLE(user_id uuid, name text, total_count bigint, feature_count bigint)
|
|
LANGUAGE sql SECURITY DEFINER STABLE
|
|
SET search_path = public
|
|
AS $$
|
|
SELECT du.user_id, p.name,
|
|
SUM(du.count)::bigint AS total_count,
|
|
COUNT(DISTINCT du.feature)::bigint AS feature_count
|
|
FROM public.daily_usage du
|
|
JOIN public.profiles p ON p.id = du.user_id
|
|
WHERE du.date BETWEEN p_from AND p_to
|
|
GROUP BY du.user_id, p.name
|
|
ORDER BY total_count DESC
|
|
LIMIT p_limit;
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.admin_top_users(date, date, integer) FROM public;
|
|
GRANT EXECUTE ON FUNCTION public.admin_top_users(date, date, integer) TO authenticated;
|
|
|
|
-- 5.3 DAU 추이
|
|
CREATE OR REPLACE FUNCTION public.admin_dau(
|
|
p_from date, p_to date
|
|
) RETURNS TABLE(date date, active_users bigint)
|
|
LANGUAGE sql SECURITY DEFINER STABLE
|
|
SET search_path = public
|
|
AS $$
|
|
SELECT du.date, COUNT(DISTINCT du.user_id)::bigint AS active_users
|
|
FROM public.daily_usage du
|
|
WHERE du.date BETWEEN p_from AND p_to
|
|
GROUP BY du.date
|
|
ORDER BY du.date;
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.admin_dau(date, date) FROM public;
|
|
GRANT EXECUTE ON FUNCTION public.admin_dau(date, date) TO authenticated;
|