+
+
+ {/*
*/}
+ {/*
*/}
+
+
+
+
+ Name
+ Project Status
+ Created on
+
+
+
+ {projects.map((project) => (
+
+
+
+ {project.name}
+
+
+
+
+ {/* Add your project status rendering logic here */}
+ {project.project_status === 'completed' ? (
+
+
+ Completed
+
+ ) : project.project_status === 'pending_approval' ? (
+
+
+ Pending Approval
+
+ ) : project.project_status === 'approved' ? (
+
+
+ Approved
+
+ ) : project.project_status === 'draft' ? (
+
+
+ Draft
+
+ ) : (
+
+
+
+ {String(project.project_status).replace('_', ' ')}
+
+
+ )}
+
+
+ {moment(project.created_at).format('LLL')}
+
+
+ ))}
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/Teams/TeamsCardList.tsx b/src/components/Teams/TeamsCardList.tsx
new file mode 100644
index 00000000..c5083388
--- /dev/null
+++ b/src/components/Teams/TeamsCardList.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { Card, CardContent } from "@/components/ui/card";
+import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
+import { Table } from "@/types";
+import { format } from "date-fns";
+import { motion } from "framer-motion";
+import { CalendarDays, Link as LinkIcon } from "lucide-react";
+import Link from "next/link";
+
+const MotionCard = motion(Card);
+const MotionCardContent = motion(CardContent);
+
+const cardVariants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: { opacity: 1, y: 0 },
+};
+
+const contentVariants = {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
+};
+
+const itemVariants = {
+ hidden: { opacity: 0, y: 10 },
+ visible: { opacity: 1, y: 0 },
+};
+
+export const TeamsCardList = ({
+ teams,
+}: {
+ teams: Table<"teams">[];
+}) => {
+ if (teams.length === 0) {
+ return (
+
+ 🔍 No matching teams found.
+
+ );
+ }
+
+ return (
+
+
+ {teams.slice(0, 5).map((team, index) => (
+
+
+
+
+ ID: {team.id}
+
+ {team.name}
+
+
+ Created: {format(new Date(team.created_at ?? Date.now()), "dd MMM yyyy")}
+
+
+
+ /{team.id}
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+function capitalizeFirstLetter(string: string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
\ No newline at end of file
diff --git a/src/data/user/projects.tsx b/src/data/user/projects.tsx
index 6f59c3f1..ce433ee4 100644
--- a/src/data/user/projects.tsx
+++ b/src/data/user/projects.tsx
@@ -70,6 +70,7 @@ export const createProjectAction = async ({
terraformWorkingDir,
managedState,
labels,
+ teamId,
}: {
organizationId: string;
name: string;
@@ -78,6 +79,7 @@ export const createProjectAction = async ({
terraformWorkingDir: string;
managedState: boolean;
labels: string[];
+ teamId: number | null;
}): Promise
>> => {
"use server";
const supabaseClient = createSupabaseUserServerActionClient();
@@ -87,6 +89,7 @@ export const createProjectAction = async ({
organization_id: organizationId,
name,
slug,
+ team_id: teamId,
repo_id: repoId,
terraform_working_dir: terraformWorkingDir,
is_managing_state: managedState,
@@ -100,20 +103,23 @@ export const createProjectAction = async ({
.select("*")
.single();
- console.log('createProjectAction', project);
-
if (error) {
- console.log('createProjectAction', error);
return {
status: 'error',
message: error.message,
};
}
+ if (teamId) {
+ revalidatePath(`/org/[organizationId]/team/[teamId]`, "layout");
+ } else {
+ revalidatePath(`/org/[organizationId]`, "layout");
+ revalidatePath(`/org/[organizationId]/projects/`, "layout");
+ }
+
+
- revalidatePath(`/org/[organizationId]`, "layout");
- revalidatePath(`/org/[organizationId]/projects/`, "layout");
return {
status: 'success',
@@ -238,6 +244,7 @@ export const markProjectAsCompletedAction = async (projectId: string): Promise {
const zeroIndexedPage = page - 1;
@@ -255,6 +263,13 @@ export const getProjects = async ({
.eq("organization_id", organizationId)
.range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1);
+ // Add team filter
+ if (teamId !== null) {
+ supabaseQuery = supabaseQuery.eq('team_id', teamId);
+ } else {
+ supabaseQuery = supabaseQuery.is('team_id', null);
+ }
+
if (query) {
supabaseQuery = supabaseQuery.ilike("name", `%${query}%`);
}
diff --git a/src/data/user/teams.ts b/src/data/user/teams.ts
new file mode 100644
index 00000000..3e939e9b
--- /dev/null
+++ b/src/data/user/teams.ts
@@ -0,0 +1,400 @@
+'use server';
+import { supabaseAdminClient } from '@/supabase-clients/admin/supabaseAdminClient';
+import { createSupabaseUserServerActionClient } from '@/supabase-clients/user/createSupabaseUserServerActionClient';
+import { createSupabaseUserServerComponentClient } from '@/supabase-clients/user/createSupabaseUserServerComponentClient';
+import { Enum, SAPayload, Table } from '@/types';
+import { serverGetLoggedInUser } from '@/utils/server/serverGetLoggedInUser';
+import { revalidatePath } from 'next/cache';
+import {
+ getLoggedInUserOrganizationRole,
+ getOrganizationAdmins,
+ getTeamMembersInOrganization,
+} from './organizations';
+
+export async function getSlimTeamById(teamId: number) {
+ const supabaseClient = createSupabaseUserServerComponentClient();
+ const { data, error } = await supabaseClient
+ .from('teams')
+ .select('id,name,organization_id')
+ .eq('id', teamId)
+ .single();
+ if (error) {
+ throw error;
+ }
+ return data;
+}
+
+export const getTeamsInOrganization = async (
+ organizationId: string,
+): Promise[]> => {
+ const supabaseClient = createSupabaseUserServerComponentClient();
+
+ const { data, error } = await supabaseClient
+ .from('teams')
+ .select('*')
+ .eq('organization_id', organizationId)
+ .order('created_at', { ascending: true });
+
+ if (error) {
+ throw error;
+ }
+
+ if (!data) {
+ throw new Error('No teams found for organization');
+ }
+ return data;
+};
+
+export const getTeams = async ({
+ organizationId,
+ query = '',
+ page = 1,
+ limit = 5,
+}: {
+ query?: string;
+ page?: number;
+ organizationId: string;
+ limit?: number;
+}) => {
+ const zeroIndexedPage = page - 1;
+ const supabase = createSupabaseUserServerComponentClient();
+ let supabaseQuery = supabase
+ .from('teams')
+ .select('*')
+ .eq('organization_id', organizationId)
+ .range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1);
+
+ if (query) {
+ supabaseQuery = supabaseQuery.ilike('name', `%${query}%`);
+ }
+
+ const { data, error } = await supabaseQuery.order('created_at', {
+ ascending: false,
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return data;
+};
+
+export const getTeamsTotalCount = async ({
+ organizationId,
+ query = '',
+ page = 1,
+ limit = 5,
+}: {
+ organizationId: string;
+ query?: string;
+ page?: number;
+ limit?: number;
+}) => {
+ const zeroIndexedPage = page - 1;
+ let supabaseQuery = supabaseAdminClient
+ .from('teams')
+ .select('id', {
+ count: 'exact',
+ head: true,
+ })
+ .eq('organization_id', organizationId)
+ .range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1);
+
+ if (query) {
+ supabaseQuery = supabaseQuery.ilike('name', `%${query}%`);
+ }
+
+ const { count, error } = await supabaseQuery.order('created_at', {
+ ascending: false,
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ if (!count) {
+ return 0;
+ }
+
+ return Math.ceil(count / limit) ?? 0;
+};
+
+export const createTeamAction = async (
+ organizationId: string,
+ name: string,
+): Promise<
+ SAPayload<{
+ created_at: string | null;
+ id: number;
+ name: string;
+ organization_id: string;
+ }>
+> => {
+ const supabaseClient = createSupabaseUserServerComponentClient();
+ const user = await serverGetLoggedInUser();
+
+ const { data, error } = await supabaseClient
+ .from('teams')
+ .insert({
+ name,
+ organization_id: organizationId,
+ })
+ .select('*')
+ .single();
+
+ if (error) {
+ throw error;
+ }
+
+ await addUserToTeamAction({
+ userId: user.id,
+ teamId: data.id,
+ role: 'admin',
+ organizationId,
+ });
+
+ revalidatePath(`/org/${organizationId}`);
+
+ return {
+ status: 'success',
+ data,
+ };
+};
+
+export async function getOrganizationOfTeam(teamId: number) {
+ const supabaseClient = createSupabaseUserServerComponentClient();
+ const { data, error } = await supabaseClient
+ .from('teams')
+ .select('organization_id')
+ .eq('id', teamId)
+ .single();
+ if (error) {
+ throw error;
+ }
+ return data.organization_id;
+}
+
+export const getUserTeamRole = async (
+ userId: string,
+ teamId: number,
+): Promise | null> => {
+ const supabase = createSupabaseUserServerComponentClient();
+ const { data, error } = await supabase
+ .from('team_members')
+ .select('*')
+ .eq('user_id', userId)
+ .eq('team_id', teamId);
+
+ const row = data?.[0];
+
+ if (error) {
+ throw error;
+ }
+
+ return row?.role ?? null;
+};
+
+export const getLoggedInUserTeamRole = async (teamId: number) => {
+ const user = await serverGetLoggedInUser();
+ return getUserTeamRole(user.id, teamId);
+};
+
+export const getTeamById = async (teamId: number) => {
+ const supabase = createSupabaseUserServerComponentClient();
+ const { data, error } = await supabase
+ .from('teams')
+ .select('*')
+ .eq('id', teamId)
+ .single();
+
+ if (error) {
+ throw error;
+ }
+
+ return data;
+};
+
+export const getTeamNameById = async (teamId: number) => {
+ const supabase = createSupabaseUserServerComponentClient();
+ const { data, error } = await supabase
+ .from('teams')
+ .select('name')
+ .eq('id', teamId)
+ .single();
+
+ if (error) {
+ throw error;
+ }
+
+ return data.name;
+};
+
+export const getTeamMembersByTeamId = async (teamId: number) => {
+ const supabase = createSupabaseUserServerComponentClient();
+ const { data, error } = await supabase
+ .from('team_members')
+ .select('*, user_profiles(*)')
+ .eq('team_id', teamId);
+
+ if (error) {
+ throw error;
+ }
+
+ return data.map((member) => {
+ const { user_profiles, ...rest } = member;
+ if (!user_profiles) {
+ throw new Error('No user profile found for member');
+ }
+ return {
+ ...rest,
+ user_profiles: user_profiles,
+ };
+ });
+};
+
+export const getCanLoggedInUserManageTeam = async (
+ organizationId: string,
+ teamId: number,
+) => {
+ const [teamRole, orgRole] = await Promise.all([
+ getLoggedInUserTeamRole(teamId),
+ getLoggedInUserOrganizationRole(organizationId),
+ ]);
+
+ let canUserManageTeam = false;
+
+ if (teamRole === 'admin' || orgRole === 'owner' || orgRole === 'admin') {
+ canUserManageTeam = true;
+ }
+ return {
+ canUserManageTeam,
+ teamRole,
+ orgRole,
+ };
+};
+
+export const updateTeamRole = async ({
+ userId,
+ teamId,
+ role,
+}: {
+ userId: string;
+ teamId: number;
+ role: Enum<'project_team_member_role'>;
+}): Promise => {
+ const supabase = createSupabaseUserServerActionClient();
+ const organizationId = await getOrganizationOfTeam(teamId);
+ const { data, error } = await supabase
+ .from('team_members')
+ .update({ role: role })
+ .eq('user_id', userId)
+ .eq('team_id', teamId);
+
+ if (error) {
+ throw error;
+ }
+
+ revalidatePath(`/org/${organizationId}/team/${teamId}`);
+
+ return {
+ status: 'success',
+ };
+};
+
+export const removeUserFromTeam = async ({
+ userId,
+ teamId,
+}: {
+ userId: string;
+ teamId: number;
+}): Promise => {
+ const supabase = createSupabaseUserServerActionClient();
+ const organizationId = await getOrganizationOfTeam(teamId);
+
+ const { data, error } = await supabase
+ .from('team_members')
+ .delete()
+ .eq('user_id', userId)
+ .eq('team_id', teamId);
+
+ if (error) {
+ throw error;
+ }
+ revalidatePath(`/org/${organizationId}/team/${teamId}`);
+ return {
+ status: 'success',
+ };
+};
+
+export const getAddableMembers = async ({
+ organizationId,
+ teamId,
+}: {
+ organizationId: string;
+ teamId: number;
+}) => {
+ const [orgMembers, teamMembers, admins] = await Promise.all([
+ getTeamMembersInOrganization(organizationId),
+ getTeamMembersByTeamId(teamId),
+ getOrganizationAdmins(organizationId),
+ ]);
+
+ return orgMembers.filter((member) => {
+ const isMember = teamMembers.find(
+ (teamMember) => teamMember.user_profiles.id === member.user_profiles.id,
+ );
+ const isAdmin = admins.find(
+ (admin) => admin.user_profiles.id === member.user_profiles.id,
+ );
+ return !isMember && !isAdmin;
+ });
+};
+
+export const addUserToTeamAction = async ({
+ userId,
+ teamId,
+ role,
+ organizationId,
+}: {
+ userId: string;
+ organizationId: string;
+ teamId: number;
+ role: Enum<'project_team_member_role'>;
+}): Promise<
+ SAPayload<{
+ id: number;
+ user_id: string;
+ role: Enum<'project_team_member_role'>;
+ team_id: number;
+ }>
+> => {
+ const supabase = createSupabaseUserServerComponentClient();
+ const rowCount = await supabase
+ .from('team_members')
+ .select('id')
+ .eq('user_id', userId)
+ .eq('team_id', teamId);
+
+ if (rowCount.error || rowCount.data?.length > 0) {
+ throw new Error('User already in team');
+ }
+
+ const { data, error } = await supabase
+ .from('team_members')
+ .insert({
+ user_id: userId,
+ role: role,
+ team_id: teamId,
+ })
+ .select('*')
+ .single();
+
+ if (error) {
+ throw error;
+ }
+ revalidatePath(`/organorgization/${organizationId}/team/${teamId}`);
+ return {
+ status: 'success',
+ data,
+ };
+};
diff --git a/src/lib/database.types.ts b/src/lib/database.types.ts
index fbf4a5f0..577d0477 100644
--- a/src/lib/database.types.ts
+++ b/src/lib/database.types.ts
@@ -1458,6 +1458,59 @@ export type Database = {
},
]
}
+ team_members: {
+ Row: {
+ created_at: string | null
+ id: number
+ role: Database["public"]["Enums"]["project_team_member_role"]
+ team_id: number
+ user_id: string
+ }
+ Insert: {
+ created_at?: string | null
+ id?: number
+ role?: Database["public"]["Enums"]["project_team_member_role"]
+ team_id: number
+ user_id: string
+ }
+ Update: {
+ created_at?: string | null
+ id?: number
+ role?: Database["public"]["Enums"]["project_team_member_role"]
+ team_id?: number
+ user_id?: string
+ }
+ Relationships: [
+ {
+ foreignKeyName: "team_members_user_id_fkey"
+ columns: ["user_id"]
+ isOneToOne: false
+ referencedRelation: "user_profiles"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
+ teams: {
+ Row: {
+ created_at: string | null
+ id: number
+ name: string
+ organization_id: string
+ }
+ Insert: {
+ created_at?: string | null
+ id?: number
+ name: string
+ organization_id: string
+ }
+ Update: {
+ created_at?: string | null
+ id?: number
+ name?: string
+ organization_id?: string
+ }
+ Relationships: []
+ }
user_api_keys: {
Row: {
created_at: string
@@ -1780,12 +1833,19 @@ export type Database = {
member_id: string
}[]
}
- get_organization_id_by_team_id: {
- Args: {
- p_id: number
- }
- Returns: string
- }
+ get_organization_id_by_team_id:
+ | {
+ Args: {
+ p_id: number
+ }
+ Returns: string
+ }
+ | {
+ Args: {
+ p_id: number
+ }
+ Returns: string
+ }
get_organization_id_for_project_id: {
Args: {
project_id: string
@@ -1808,12 +1868,28 @@ export type Database = {
organization_id: string
}[]
}
+ get_team_admins_by_team_id: {
+ Args: {
+ team_id: number
+ }
+ Returns: {
+ user_id: string
+ }[]
+ }
get_team_id_for_project_id: {
Args: {
project_id: string
}
Returns: number
}
+ get_team_members_team_id: {
+ Args: {
+ team_id: number
+ }
+ Returns: {
+ user_id: string
+ }[]
+ }
increment_credits: {
Args: {
org_id: string
diff --git a/src/utils/parseNotification.ts b/src/utils/parseNotification.ts
index 2b0563ef..9946284e 100644
--- a/src/utils/parseNotification.ts
+++ b/src/utils/parseNotification.ts
@@ -76,7 +76,7 @@ export const parseNotification = (
return {
title: 'Accepted invitation to join organization',
description: `${notification.userFullName} has accepted your invitation to join your organization`,
- href: `/organization/${notification.organizationId}/settings/members`,
+ href: `/org/${notification.organizationId}/settings/members`,
image: 'UserCheck',
type: notification.type,
actionType: 'link',
diff --git a/src/utils/zod-schemas/params.ts b/src/utils/zod-schemas/params.ts
index d8956607..930e6088 100644
--- a/src/utils/zod-schemas/params.ts
+++ b/src/utils/zod-schemas/params.ts
@@ -4,6 +4,11 @@ export const organizationParamSchema = z.object({
organizationId: z.string().uuid(),
});
+export const teamParamSchema = z.object({
+ teamId: z.coerce.number().optional(),
+ organizationId: z.string().uuid(),
+});
+
export const organizationSlugParamSchema = z.object({
organizationSlug: z.string(),
});
@@ -13,6 +18,11 @@ export const projectsfilterSchema = z.object({
query: z.string().optional(),
});
+export const teamFilterSchema = z.object({
+ page: z.coerce.number().optional(),
+ query: z.string().optional(),
+});
+
export const projectParamSchema = z.object({
projectId: z.string().uuid(),
});
diff --git a/supabase/migrations/20240731075720_add_teams.sql b/supabase/migrations/20240731075720_add_teams.sql
new file mode 100644
index 00000000..d97aa8fe
--- /dev/null
+++ b/supabase/migrations/20240731075720_add_teams.sql
@@ -0,0 +1,16 @@
+CREATE TABLE "public"."team_members" (
+ "id" bigint generated by DEFAULT AS identity NOT NULL,
+ "created_at" timestamp WITH time zone DEFAULT NOW(),
+ "user_id" uuid NOT NULL REFERENCES "public"."user_profiles"("id") ON DELETE CASCADE,
+ "role" project_team_member_role NOT NULL DEFAULT 'member'::project_team_member_role,
+ "team_id" bigint NOT NULL
+);
+
+CREATE TABLE "public"."teams" (
+ "id" bigint generated by DEFAULT AS identity NOT NULL,
+ "created_at" timestamp WITH time zone DEFAULT NOW(),
+ "organization_id" uuid NOT NULL,
+ "name" text NOT NULL
+);
+
+ALTER TABLE "public"."teams" enable ROW LEVEL SECURITY;
\ No newline at end of file
diff --git a/supabase/migrations/20240731081047_add_team_database_functions.sql b/supabase/migrations/20240731081047_add_team_database_functions.sql
new file mode 100644
index 00000000..51169860
--- /dev/null
+++ b/supabase/migrations/20240731081047_add_team_database_functions.sql
@@ -0,0 +1,90 @@
+
+-- Get organization of a team
+-- This function is used to get an organization of a team
+CREATE OR REPLACE FUNCTION public.get_organization_id_by_team_id(p_id integer) RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER AS $function$
+DECLARE v_organization_id UUID;
+BEGIN
+SELECT organization_id INTO v_organization_id
+FROM teams
+WHERE id = p_id;
+RETURN v_organization_id;
+EXCEPTION
+WHEN NO_DATA_FOUND THEN RAISE EXCEPTION 'No organization found for the provided id: %',
+p_id;
+END;
+$function$;
+REVOKE ALL ON FUNCTION public.get_organization_id_by_team_id(p_id integer)
+FROM PUBLIC;
+REVOKE ALL ON FUNCTION public.get_organization_id_by_team_id(p_id integer)
+FROM ANON;
+
+
+CREATE OR REPLACE FUNCTION public.get_organization_id_by_team_id(p_id bigint) RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER AS $function$
+DECLARE v_organization_id UUID;
+BEGIN
+SELECT organization_id INTO v_organization_id
+FROM teams
+WHERE id = p_id;
+RETURN v_organization_id;
+EXCEPTION
+WHEN NO_DATA_FOUND THEN RAISE EXCEPTION 'No organization found for the provided id: %',
+p_id;
+END;
+$function$;
+REVOKE ALL ON FUNCTION public.get_organization_id_by_team_id(p_id bigint)
+FROM PUBLIC;
+REVOKE ALL ON FUNCTION public.get_organization_id_by_team_id(p_id bigint)
+FROM ANON;
+
+-- Get organizations for user
+-- This function is used to get all organizations that a user is a member of
+CREATE OR REPLACE FUNCTION public.get_organizations_for_user(user_id uuid) RETURNS TABLE(organization_id uuid) LANGUAGE plpgsql SECURITY DEFINER AS $function$ BEGIN RETURN QUERY
+SELECT o.id AS organization_id
+FROM organizations o
+ JOIN organization_members ot ON o.id = ot.organization_id
+WHERE ot.member_id = user_id;
+END;
+$function$;
+REVOKE ALL ON FUNCTION public.get_organizations_for_user
+FROM PUBLIC;
+REVOKE ALL ON FUNCTION public.get_organizations_for_user
+FROM ANON;
+-- Get project admins of a team
+-- This function is used to get all admins of a team
+CREATE OR REPLACE FUNCTION public.get_team_admins_by_team_id(team_id bigint) RETURNS TABLE(user_id uuid) LANGUAGE plpgsql SECURITY DEFINER AS $function$ BEGIN RETURN QUERY
+SELECT team_members.user_id
+FROM team_members
+WHERE team_members.team_id = $1
+ AND role = 'admin';
+END;
+$function$;
+REVOKE ALL ON FUNCTION public.get_team_admins_by_team_id
+FROM PUBLIC;
+REVOKE ALL ON FUNCTION public.get_team_admins_by_team_id
+FROM ANON;
+-- Get project members of a team
+-- This function is used to get all members of a team
+CREATE OR REPLACE FUNCTION public.get_team_members_team_id(team_id bigint) RETURNS TABLE(user_id uuid) LANGUAGE plpgsql SECURITY DEFINER AS $function$ BEGIN RETURN QUERY
+SELECT team_members.user_id
+FROM team_members
+WHERE team_members.team_id = $1;
+END;
+$function$;
+REVOKE ALL ON FUNCTION public.get_team_members_team_id
+FROM PUBLIC;
+REVOKE ALL ON FUNCTION public.get_team_members_team_id
+FROM ANON;
+
+CREATE OR REPLACE FUNCTION get_team_id_for_project_id(project_id UUID) RETURNS INT8 AS $$
+DECLARE team_id INT8;
+BEGIN
+SELECT p.team_id INTO team_id
+FROM projects p
+WHERE p.id = project_id;
+RETURN team_id;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+REVOKE ALL ON FUNCTION public.get_team_id_for_project_id
+FROM PUBLIC;
+REVOKE ALL ON FUNCTION public.get_team_id_for_project_id
+FROM ANON;
\ No newline at end of file
diff --git a/supabase/migrations/20240731082359_add_team_policies.sql b/supabase/migrations/20240731082359_add_team_policies.sql
new file mode 100644
index 00000000..bfb3b134
--- /dev/null
+++ b/supabase/migrations/20240731082359_add_team_policies.sql
@@ -0,0 +1,187 @@
+-- Drop the policy for reading project comments
+DO $$
+BEGIN
+ IF EXISTS (
+ SELECT 1
+ FROM pg_policies
+ WHERE schemaname = 'public'
+ AND tablename = 'project_comments'
+ AND policyname = 'All organization members of a project can read project comments'
+ ) THEN
+ DROP POLICY "All organization members of a project can read project comments" ON "public"."project_comments";
+ END IF;
+END $$;
+
+-- Drop the policy for inserting project comments
+DO $$
+BEGIN
+ IF EXISTS (
+ SELECT 1
+ FROM pg_policies
+ WHERE schemaname = 'public'
+ AND tablename = 'project_comments'
+ AND policyname = 'All organization members of a project can make project comments'
+ ) THEN
+ DROP POLICY "All organization members of a project can make project comments" ON "public"."project_comments";
+ END IF;
+END $$;
+
+
+CREATE policy "Enable delete for org admins only" ON "public"."teams" AS permissive FOR DELETE TO authenticated USING (
+ (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_organization_admin_ids(teams.organization_id) AS get_organization_admin_ids
+ )
+ )
+);
+
+CREATE policy "Enable insert for org admins only" ON "public"."teams" AS permissive FOR
+INSERT TO authenticated WITH CHECK (
+ (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_organization_admin_ids(teams.organization_id) AS get_organization_admin_ids
+ )
+ )
+ );
+
+
+
+CREATE policy "Enable read access for org admins or team members" ON "public"."teams" AS permissive FOR
+SELECT TO authenticated USING (
+ (
+ (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_organization_admin_ids(teams.organization_id) AS get_organization_admin_ids
+ )
+ )
+ OR (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_team_members_team_id(teams.id) AS get_team_members_team_id
+ )
+ )
+ )
+ );
+
+
+CREATE policy "Enable update for org admins" ON "public"."teams" AS permissive FOR
+UPDATE TO authenticated USING (
+ (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_organization_admin_ids(teams.organization_id) AS get_organization_admin_ids
+ )
+ )
+ ) WITH CHECK (
+ (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_organization_admin_ids(teams.organization_id) AS get_organization_admin_ids
+ )
+ )
+ );
+
+
+CREATE policy "Enable read access for all team members" ON "public"."projects" AS permissive FOR
+SELECT TO authenticated USING (
+ (
+ (organization_id IS NULL)
+ OR (
+ (team_id IS NULL)
+ AND (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_organization_member_ids(projects.organization_id) AS get_organization_member_ids
+ )
+ )
+ )
+ OR (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_team_members_team_id(projects.team_id) AS get_team_members_team_id
+ )
+ )
+ OR (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_organization_admin_ids(projects.organization_id) AS get_organization_admin_ids
+ )
+ )
+ )
+ );
+
+
+CREATE policy "All team members can read organizations" ON "public"."organizations" AS permissive FOR
+SELECT TO authenticated USING (
+ (
+ (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_organization_member_ids(organizations.id) AS get_organization_member_ids
+ )
+ )
+ OR (
+ id IN (
+ SELECT get_invited_organizations_for_user_v2(
+ (
+ SELECT auth.uid()
+ ),
+ ((auth.jwt()->>'email'::text))::character varying
+ ) AS get_invited_organizations_for_user_v2
+ )
+ )
+ )
+ );
+
+CREATE policy "All team members of a project can make project comments" ON "public"."project_comments" AS permissive FOR
+INSERT TO authenticated WITH CHECK (
+ (
+ (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_organization_admin_ids(get_organization_id_for_project_id(project_id)) AS get_organization_admin_ids
+ )
+ )
+ OR (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_team_members_team_id(get_team_id_for_project_id(project_id)) AS get_team_members_team_id
+ )
+ )
+ )
+ );
+
+ CREATE policy "All team members of a project can read project comments" ON "public"."project_comments" AS permissive FOR
+SELECT TO authenticated USING (
+ (
+ (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_organization_admin_ids(get_organization_id_for_project_id(project_id)) AS get_organization_admin_ids
+ )
+ )
+ OR (
+ (
+ SELECT auth.uid()
+ ) IN (
+ SELECT get_team_members_team_id(get_team_id_for_project_id(project_id)) AS get_team_members_team_id
+ )
+ )
+ )
+ );
\ No newline at end of file