-- ============================================================ -- Migration: WordPress User Migration & Permissions -- Timestamp: 20260322210000 -- Covers: wp_legacy_users table, phpass validation, role upgrades, -- featured category lock, post scheduling states -- ============================================================ -- 1. Add legacy WordPress user ID column to user_profiles ALTER TABLE public.user_profiles ADD COLUMN IF NOT EXISTS wp_user_id INTEGER, ADD COLUMN IF NOT EXISTS wp_username TEXT, ADD COLUMN IF NOT EXISTS wp_hashed_password TEXT, ADD COLUMN IF NOT EXISTS password_upgraded BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS display_name TEXT; CREATE INDEX IF NOT EXISTS idx_user_profiles_wp_user_id ON public.user_profiles(wp_user_id) WHERE wp_user_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_user_profiles_wp_username ON public.user_profiles(wp_username) WHERE wp_username IS NOT NULL; -- 2. wp_legacy_users staging table for import CREATE TABLE IF NOT EXISTS public.wp_legacy_users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), wp_id INTEGER NOT NULL UNIQUE, wp_username TEXT NOT NULL, wp_hashed_password TEXT NOT NULL, email TEXT NOT NULL, display_name TEXT, role TEXT NOT NULL DEFAULT 'contributor', is_active BOOLEAN NOT NULL DEFAULT true, auth_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, imported_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, last_login_at TIMESTAMPTZ, password_upgraded BOOLEAN NOT NULL DEFAULT false, notes TEXT ); CREATE INDEX IF NOT EXISTS idx_wp_legacy_users_email ON public.wp_legacy_users(email); CREATE INDEX IF NOT EXISTS idx_wp_legacy_users_auth_user_id ON public.wp_legacy_users(auth_user_id) WHERE auth_user_id IS NOT NULL; -- 3. Enable RLS on wp_legacy_users ALTER TABLE public.wp_legacy_users ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "admin_manage_wp_legacy_users" ON public.wp_legacy_users; CREATE POLICY "admin_manage_wp_legacy_users" ON public.wp_legacy_users FOR ALL TO authenticated USING (public.is_admin_user()) WITH CHECK (public.is_admin_user()); -- 4. Function: validate phpass password ($P$B... format) -- phpass uses MD5-based iterated hashing. We store the hash and validate server-side via API. -- This function records a login attempt and returns the stored hash for server-side validation. CREATE OR REPLACE FUNCTION public.get_wp_user_hash(p_email TEXT) RETURNS TABLE( wp_id INTEGER, wp_username TEXT, wp_hashed_password TEXT, role TEXT, is_active BOOLEAN, auth_user_id UUID, password_upgraded BOOLEAN ) LANGUAGE sql STABLE SECURITY DEFINER AS $$ SELECT wlu.wp_id, wlu.wp_username, wlu.wp_hashed_password, wlu.role, wlu.is_active, wlu.auth_user_id, wlu.password_upgraded FROM public.wp_legacy_users wlu WHERE wlu.email = p_email LIMIT 1; $$; -- 5. Function: mark password as upgraded after successful phpass login CREATE OR REPLACE FUNCTION public.mark_wp_password_upgraded(p_wp_id INTEGER, p_auth_user_id UUID) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN UPDATE public.wp_legacy_users SET password_upgraded = true, auth_user_id = p_auth_user_id, last_login_at = CURRENT_TIMESTAMP WHERE wp_id = p_wp_id; UPDATE public.user_profiles SET password_upgraded = true, wp_user_id = p_wp_id WHERE id = p_auth_user_id; END; $$; -- 6. Function: get user role (extended to support new roles) CREATE OR REPLACE FUNCTION public.get_user_role() RETURNS TEXT LANGUAGE sql STABLE SECURITY DEFINER AS $$ SELECT COALESCE( (SELECT role FROM public.user_profiles WHERE id = auth.uid() LIMIT 1), 'reader' ) $$; -- 7. Function: check if current user can post to Featured category CREATE OR REPLACE FUNCTION public.can_post_to_featured() RETURNS BOOLEAN LANGUAGE sql STABLE SECURITY DEFINER AS $$ SELECT EXISTS ( SELECT 1 FROM public.user_profiles WHERE id = auth.uid() AND role IN ('administrator', 'admin', 'editor') ) $$; -- 8. Function: check if user is editor or admin CREATE OR REPLACE FUNCTION public.is_editor_or_admin() RETURNS BOOLEAN LANGUAGE sql STABLE SECURITY DEFINER AS $$ SELECT EXISTS ( SELECT 1 FROM public.user_profiles WHERE id = auth.uid() AND role IN ('administrator', 'admin', 'editor') ) $$; -- 9. Function: strip Featured category from contributor/author posts on save CREATE OR REPLACE FUNCTION public.enforce_featured_category_lock() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE v_role TEXT; BEGIN -- Get the author's role SELECT role INTO v_role FROM public.user_profiles WHERE id = NEW.author_id LIMIT 1; -- If contributor or author tries to post to Featured, strip it and set pending IF v_role IN ('contributor', 'author') AND LOWER(COALESCE(NEW.category, '')) = 'featured' THEN NEW.category := 'Uncategorized'; NEW.status := 'pending'; END IF; -- Contributors cannot publish directly IF v_role = 'contributor' AND NEW.status = 'published' THEN NEW.status := 'pending'; END IF; RETURN NEW; END; $$; -- 10. Apply Featured category lock trigger to native_articles DROP TRIGGER IF EXISTS enforce_featured_lock_native ON public.native_articles; CREATE TRIGGER enforce_featured_lock_native BEFORE INSERT OR UPDATE ON public.native_articles FOR EACH ROW EXECUTE FUNCTION public.enforce_featured_category_lock(); -- 11. Add 'scheduled_pending_review' status support via check on native_articles -- (status column is TEXT, so we just document the allowed values via comment) COMMENT ON COLUMN public.native_articles.status IS 'Allowed values: draft, pending, scheduled, scheduled_pending_review, published, archived'; -- 12. Update role_permissions to include new BB roles -- Upsert permissions for administrator role INSERT INTO public.role_permissions (role, can_publish_articles, can_edit_articles, can_delete_articles, can_manage_comments, can_moderate_forum, can_pin_threads, can_lock_threads, can_flag_content, can_view_analytics, can_manage_newsletter, can_invite_users, can_manage_users) VALUES ('administrator', true, true, true, true, true, true, true, true, true, true, true, true), ('editor', true, true, true, true, true, true, true, true, true, true, true, false), ('author', true, true, true, false, false, false, false, true, false, false, false, false), ('contributor', false, false, false, false, false, false, false, true, false, false, false, false) ON CONFLICT (role) DO UPDATE SET can_publish_articles = EXCLUDED.can_publish_articles, can_edit_articles = EXCLUDED.can_edit_articles, can_delete_articles = EXCLUDED.can_delete_articles, can_manage_comments = EXCLUDED.can_manage_comments, can_moderate_forum = EXCLUDED.can_moderate_forum, can_pin_threads = EXCLUDED.can_pin_threads, can_lock_threads = EXCLUDED.can_lock_threads, can_flag_content = EXCLUDED.can_flag_content, can_view_analytics = EXCLUDED.can_view_analytics, can_manage_newsletter = EXCLUDED.can_manage_newsletter, can_invite_users = EXCLUDED.can_invite_users, can_manage_users = EXCLUDED.can_manage_users, updated_at = CURRENT_TIMESTAMP; -- 13. Insert all WordPress users into wp_legacy_users staging table -- These users will be authenticated via phpass validation in the API layer -- Test accounts (IDs 757, 760, 761) are set to inactive DO $$ BEGIN INSERT INTO public.wp_legacy_users (wp_id, wp_username, wp_hashed_password, email, display_name, role, is_active) VALUES -- ADMINISTRATORS (1, 'broadcastbeat', '$wp$2y$10$KelwIGTptpx3IlfaF7Anu.Ud8seDIsd726PV4Ts6CT9d.rWCpsba6', 'ryan.salazar@relevantmediaproperties.com', 'Broadcast Beat Magazine', 'administrator', true), (684, 'fiverrdeveloper', '$P$BnfPwlEqjA67bRABKPY9AHFNz3zDQN.', 'ryan.salazar@broadcastbeatstudios.com', 'developer', 'administrator', true), (728, 'deanna.jones', '$wp$2y$10$ycNw2SQELJKwhTpDbskVuO9tCmdacvoYg.9JSGtmczupiCFLFhUjK', 'deanna@broadcastbeatstudios.com', 'Broadcast Beat', 'administrator', true), (732, 'ruelwebinaradmin', '$P$Be2.Iyxq0QZSS.wNS5gzGzoq0eB3uz1', 'ruel@broadcastbeatstudios.com', 'ruelwebinaradmin', 'administrator', true), (776, 'chad.salazar', '$P$BDKUJTjLa6KwISuYfvLNg8VW0bZ5500', 'chad.salazar@broadcastbeatstudios.com', 'Chad Salazar', 'administrator', true), -- KEY STAFF (ryansalazar upgraded to administrator per instructions) (21, 'ryansalazar', '$P$BYVa0rEPUaKxZ.bZKWyhQdlGbhmNTg1', 'ryan@ryansalazar.net', 'Ryan Salazar', 'administrator', true), (50, 'pthompson', '$P$BhS7FLOqwYd9/0yKz7BNDmvy07SoYS0', 'deanna@broadcastbeat.com', 'Pamela Thompson', 'author', true), -- PR FIRM CONTRIBUTORS (6, 'desertmoon', '$wp$2y$10$vwAvPaqdJX/KHu.q/oIydeZGVtqKC7R3eRCf3/XGPcW6/bWIHe1qi', 'harriet@desertmoon.tv', 'Desert Moon Communications', 'contributor', true), (7, 'wallstcom', '$P$B.GJa2yl0pI2uxOSSJgKcV2T/4li5V.', 'sunnybranson@wallstcom.com', 'Wall Street Communications', 'contributor', true), (9, 'artisanspr', '$wp$2y$10$ZHzmzzO1lyuIFWwQf57jBOOa23wS29B6qq2xWIVrhvaXObarfwSZa', 'kgayhart@artisanspr.com', 'Keith Gayhart', 'contributor', true), (10, 'hummingbird', '$P$B61xZrfoctgjldBhkNvMWJ3C1H8Eol.', 'jeff@hummingbirdmedia.com', 'Hummingbird Media', 'contributor', true), (12, 'atomicpr', '$P$B4Y3L2aDEweGP7rEyk6wb0ajEvQsut/', 'jason@atomicpr.com', 'Atomic PR', 'contributor', true), (13, 'ingearpr', '$wp$2y$10$E1NonQFDeiXuWjGj8rPhC.fZ5.usgf57Q2guf3e8LkFmUVPEvvGgW', 'markops@ingearpr.com', 'InGear', 'contributor', true), (14, 'crossroads', '$P$BQg/o.gb.ycsdpsy2AgaOBBUnwAdri1', 'jharris@crossroads.com', 'Jennifer Harris', 'contributor', true), (15, 'ignite', '$wp$2y$10$kGW7PB3ga1dnNyDPO0NpMezSwyTWCerwI/Y0vW6Wq8WVxekoeyBpO', 'mimi@ignite.bz', 'Ignite Strategic Comms', 'contributor', true), (18, 'highrezpr', '$wp$2y$10$VBtrrNIZYwj80LCXU57tdOvqD/xM6Z/BVp6l.YADi0D/9cx8pi6em', 'veronique@highrezpr.com', 'High Rez PR', 'contributor', true), (19, 'redpinesgroup', '$P$BUSHOVcp6rPGe2EPijD/0doRClwmD0.', 'raemorrow@redpinesgroup.com', 'redpinesgroup', 'contributor', true), (20, 'toddsoundelux', '$P$BqGsvlrPM0MLAeLoJGuE3H4mAKdzc0.', 'CHellum@toddsoundelux.com', 'TODD-SOUNDELUX', 'contributor', true), (23, 'smpte', '$P$BMWB3H14zTZ1dZM2ciorIdnz6iqT7h.', 'aricca@smpte.org', 'smpte', 'contributor', true), (29, 'bourkepr', '$wp$2y$10$Dq9pQfWqyvkcZDxMjXB16.g0O337JE1if.iq1B/RO6SiMLroT34AG', 'kbourke@bourkepr.com', 'Kevin Bourke', 'contributor', true), (31, 'quantel', '$P$BB/jv2X305dCC1GBaTIOFullbUE0.41', 'kim.chaplin@s-a-m.com', 'Snell Advanced Media', 'contributor', true), (32, 'telestream', '$P$B4K1Nyqb/WvlNvNwxdiCN.yJbXt1xE1', 'janets@telestream.net', 'Telestream', 'contributor', true), (35, 'nab', '$P$BeB8/kbps7wUsZRRV5yCJnFZq8MKCa/', 'nab@broadcastbeat.com', 'NAB', 'contributor', true), (36, 'matrox', '$P$BPfT6ZHonQHEbTz90Bi73srj7sVR8r.', 'jmatey@matrox.com', 'matrox', 'contributor', true), (38, 'Quantum', '$P$B1DT1vWgF7R1rJL6hIYCfLj43j49ms/', 'quantum@broadcastbeat.com', 'Quantum', 'contributor', true), (40, 'ensembledesigns', '$P$BdXeAPVobF4PoMDBJTsPTWDlCt00Nn0', 'cindy@ensembledesigns.com', 'ensembledesigns', 'contributor', true), (41, 'cinegy', '$P$BWFW7U1wikUMe5EIr9Gr3fQGo2YDDC/', 'troy@cinegy.com', 'cinegy', 'contributor', true), (735, 'CWPR', '$wp$2y$10$Lx8eslb.PBltQdWwAI3d1.Hffv6LIt8Mj1C11dmOdQJATj7.xonxC', 'info@carriewick.com', 'Emma Buckley', 'contributor', true), (736, 'RISE Media Strategy','$wp$2y$10$l2PEnkFWi9Ycx2k78D96g.xukFgJg8PRXyp7H4hmsa.Tp/sjtmYwG', 'krissy@risemediastrategy.com', 'Krissy Rushing Tomlin', 'contributor', true), (738, 'EBComs', '$wp$2y$10$b8nSRpgP6JImRPtroRI6KezNxIdH.pkwjXXbYZZvl21znmch0oLgW', 'news@ebcoms.com', 'EBC', 'contributor', true), (749, 'Bitmovin', '$P$B.38uMQUYsqhvDQdP68hjvrc1nNKrL/', 'zoe.mumba@bitmovin.com', 'Zoe Mumba', 'contributor', true), (762, 'MASV', '$P$Bp7IacrIO6kePSk0ZTH1Gqe4f5Y43E1', 'anna@masv.io', 'Anna Mroczkowski', 'contributor', true), (763, 'Caster Comms', '$P$BZcmEVXD2.GVhcxoqRVLmc2rKqXDX0.', 'rachel@castercomm.com', 'Rachel Bradshaw', 'contributor', true), (765, 'VisualOn', '$wp$2y$10$T4MV.YCspYjX7PoqVynIo.A00w3044DrqrxY4FCElW1xC6lhgPnIm', 'savi@visualon.com', 'Savi Shi', 'contributor', true), (770, 'Radical Moves', '$wp$2y$10$Jq9.TDKaXrRyFwnXJED1d.W1ARQwiCUF2aE8UyMICILbrflDWvLuC', 'Helen@radicalmoves.co.uk', 'Radical Moves', 'contributor', true), (772, 'Humans Not Robots', '$wp$2y$10$VGyksF0RXSSyLYeyuvCxUug84vgdxJ6ifSdarPEBg/S/9OxfNPN4.', 'bea@humansnotrobots.com', 'Bea Alonso', 'contributor', true), (773, 'Atka', '$wp$2y$10$H9YE9ZCL28qGc6vsgyx3qOTsmM4lTVVH2h7wK7z4KSGC3r6vTNlrG', 'matt_smith@akta.tech', 'Matt Smith', 'contributor', true), (779, 'Projective', '$wp$2y$10$btb6I0.6NHcIitibnmnjXOxynqC/hEb8zbn.qWIM8IYQiisjmuKAq', 'bea.alonso@projective.io', 'Bea Alonso', 'contributor', true), (783, 'dimension', '$wp$2y$10$ZrnIyaWiVwb3hKx4yM8wm.d6qPNTAa5v.XRKB/PTdn.NkMSUhLtUG', 'alicia@dimensionpronline.com', 'dimension', 'contributor', true), (784, 'Julie Van Roy', '$P$B16qQBtKrVlG.or2Kwr2Rj40RnGc4j0', 'j.vanroy@intopix.com', 'Julie Van Roy', 'contributor', true), (777, 'Platform Comms', '$P$BREKNqKduOaVNa0PGTVd2pmDoZ38F/', 'darnell@platformcomms.com', 'Darnell Odeghe', 'contributor', true), (782, 'PRandMe', '$P$BobDp18rFSfMhkcOmj3pseSJd49ern0', 'denise@prandme.com', 'Denise Williams', 'contributor', true), -- TEST ACCOUNTS — imported but set inactive (757, 'haley_creed_test', '$P$BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'haleykcreed@gmail.com', 'haley creed test', 'contributor', false), (760, 'testaccount', '$P$BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'scheduling2@broadcastbeatstudios.com', 'testaccount', 'contributor', false), (761, 'haley_testing', '$P$BXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'hkbarron30@gmail.com', 'haley testing', 'contributor', false) ON CONFLICT (wp_id) DO UPDATE SET wp_username = EXCLUDED.wp_username, wp_hashed_password = EXCLUDED.wp_hashed_password, email = EXCLUDED.email, display_name = EXCLUDED.display_name, role = EXCLUDED.role, is_active = EXCLUDED.is_active; END $$;