mirror of
https://github.com/rybbit-io/rybbit.git
synced 2025-05-11 12:25:36 +02:00
Add member functionality and organization management enhancements
- Introduced `AddMemberDialog` to facilitate adding members to organizations in the settings page. - Updated the organization component layout for improved user experience and accessibility. - Refactored member handling logic to ensure proper refresh and state management. - Added a new API endpoint for adding users to organizations, enhancing backend functionality. - Modified account creation to include organization association, improving user onboarding process.
This commit is contained in:
parent
e09a239c8a
commit
0d0b7c33b2
8 changed files with 410 additions and 107 deletions
|
@ -0,0 +1,131 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { UserPlus } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Alert } from "../../../../components/ui/alert";
|
||||||
|
import { BACKEND_URL } from "../../../../lib/const";
|
||||||
|
|
||||||
|
interface AddMemberDialogProps {
|
||||||
|
organizationId: string;
|
||||||
|
onSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddMemberDialog({
|
||||||
|
organizationId,
|
||||||
|
onSuccess,
|
||||||
|
}: AddMemberDialogProps) {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [role, setRole] = useState("member");
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const handleInvite = async () => {
|
||||||
|
if (!email) {
|
||||||
|
toast.error("Email is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await fetch(`${BACKEND_URL}/add-user-to-organization`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
role: role,
|
||||||
|
organizationId: organizationId,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(`Invitation sent to ${email}`);
|
||||||
|
setOpen(false);
|
||||||
|
onSuccess();
|
||||||
|
setEmail("");
|
||||||
|
setRole("member");
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || "Failed to send invitation");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button size="sm" variant="outline" className="ml-2">
|
||||||
|
<UserPlus className="h-4 w-4 mr-1" />
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add a new member</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Add a new member to this organization.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="email@example.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setEmail(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="role">Role</Label>
|
||||||
|
<Select value={role} onValueChange={setRole}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a role" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="owner">Owner</SelectItem>
|
||||||
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
<SelectItem value="member">Member</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{error && <Alert variant="destructive">{error}</Alert>}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleInvite} disabled={isLoading} variant="success">
|
||||||
|
{isLoading ? "Adding..." : "Add"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
|
@ -19,13 +19,16 @@ import { authClient } from "../../../lib/auth";
|
||||||
// Import the separated dialog components
|
// Import the separated dialog components
|
||||||
import { DeleteOrganizationDialog } from "./components/DeleteOrganizationDialog";
|
import { DeleteOrganizationDialog } from "./components/DeleteOrganizationDialog";
|
||||||
import { EditOrganizationDialog } from "./components/EditOrganizationDialog";
|
import { EditOrganizationDialog } from "./components/EditOrganizationDialog";
|
||||||
import { InviteMemberDialog } from "./components/InviteMemberDialog";
|
|
||||||
import { RemoveMemberDialog } from "./components/RemoveMemberDialog";
|
import { RemoveMemberDialog } from "./components/RemoveMemberDialog";
|
||||||
import { useOrganizationMembers } from "../../../api/admin/auth";
|
import { useOrganizationMembers } from "../../../api/admin/auth";
|
||||||
import {
|
import {
|
||||||
UserOrganization,
|
UserOrganization,
|
||||||
useUserOrganizations,
|
useUserOrganizations,
|
||||||
} from "../../../api/admin/organizations";
|
} from "../../../api/admin/organizations";
|
||||||
|
import { AddMemberDialog } from "./components/AddMemberDialog";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Badge } from "../../../components/ui/badge";
|
||||||
|
import { Button } from "../../../components/ui/button";
|
||||||
|
|
||||||
// Types for our component
|
// Types for our component
|
||||||
export type Organization = {
|
export type Organization = {
|
||||||
|
@ -53,6 +56,23 @@ export type Member = {
|
||||||
function Organization({ org }: { org: UserOrganization }) {
|
function Organization({ org }: { org: UserOrganization }) {
|
||||||
const { data: members, refetch } = useOrganizationMembers(org.id);
|
const { data: members, refetch } = useOrganizationMembers(org.id);
|
||||||
|
|
||||||
|
// const { data: invitations, refetch: refetchInvitations } = useQuery({
|
||||||
|
// queryKey: ["invitations", org.id],
|
||||||
|
// queryFn: async () => {
|
||||||
|
// const invitations = await authClient.organization.listInvitations({
|
||||||
|
// query: {
|
||||||
|
// organizationId: org.id,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (invitations.error) {
|
||||||
|
// throw new Error(invitations.error.message);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return invitations.data;
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
const { data } = authClient.useSession();
|
const { data } = authClient.useSession();
|
||||||
|
|
||||||
const isOwner = members?.data.find(
|
const isOwner = members?.data.find(
|
||||||
|
@ -61,88 +81,166 @@ function Organization({ org }: { org: UserOrganization }) {
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
refetch();
|
refetch();
|
||||||
|
// refetchInvitations();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<>
|
||||||
<CardHeader className="pb-2">
|
<Card className="w-full">
|
||||||
<div className="flex items-center justify-between">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-xl">
|
<div className="flex items-center justify-between">
|
||||||
{org.name}
|
<CardTitle className="text-xl">
|
||||||
<span className="text-sm font-normal text-muted-foreground ml-2">
|
{org.name}
|
||||||
({org.slug})
|
<span className="text-sm font-normal text-muted-foreground ml-2">
|
||||||
</span>
|
({org.slug})
|
||||||
</CardTitle>
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<>
|
<>
|
||||||
<InviteMemberDialog
|
<AddMemberDialog
|
||||||
organizationId={org.id}
|
organizationId={org.id}
|
||||||
onSuccess={handleRefresh}
|
onSuccess={handleRefresh}
|
||||||
/>
|
/>
|
||||||
<EditOrganizationDialog
|
<EditOrganizationDialog
|
||||||
organization={org}
|
organization={org}
|
||||||
onSuccess={handleRefresh}
|
onSuccess={handleRefresh}
|
||||||
/>
|
/>
|
||||||
<DeleteOrganizationDialog
|
<DeleteOrganizationDialog
|
||||||
organization={org}
|
organization={org}
|
||||||
onSuccess={handleRefresh}
|
onSuccess={handleRefresh}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardHeader>
|
||||||
</CardHeader>
|
<CardContent>
|
||||||
<CardContent>
|
<Table>
|
||||||
<Table>
|
<TableHeader>
|
||||||
<TableHeader>
|
<TableRow>
|
||||||
<TableRow>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead>Name</TableHead>
|
<TableHead>Email</TableHead>
|
||||||
<TableHead>Email</TableHead>
|
<TableHead>Role</TableHead>
|
||||||
<TableHead>Role</TableHead>
|
<TableHead>Joined</TableHead>
|
||||||
<TableHead>Joined</TableHead>
|
{isOwner && <TableHead className="w-12">Actions</TableHead>}
|
||||||
{isOwner && <TableHead className="w-12">Actions</TableHead>}
|
</TableRow>
|
||||||
</TableRow>
|
</TableHeader>
|
||||||
</TableHeader>
|
<TableBody>
|
||||||
<TableBody>
|
{members?.data?.map((member: any) => (
|
||||||
{members?.data?.map((member: any) => (
|
<TableRow key={member.id}>
|
||||||
<TableRow key={member.id}>
|
<TableCell>{member.user?.name || "—"}</TableCell>
|
||||||
<TableCell>{member.user?.name || "—"}</TableCell>
|
<TableCell>{member.user?.email}</TableCell>
|
||||||
<TableCell>{member.user?.email}</TableCell>
|
<TableCell className="capitalize">{member.role}</TableCell>
|
||||||
<TableCell className="capitalize">{member.role}</TableCell>
|
<TableCell>
|
||||||
<TableCell>
|
{DateTime.fromSQL(member.createdAt).toLocaleString(
|
||||||
{DateTime.fromISO(member.createdAt).toLocaleString(
|
DateTime.DATE_SHORT
|
||||||
DateTime.DATE_SHORT
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
{isOwner && (
|
|
||||||
<TableCell className="text-right">
|
|
||||||
{member.role !== "owner" && (
|
|
||||||
<RemoveMemberDialog
|
|
||||||
member={member}
|
|
||||||
organizationId={org.id}
|
|
||||||
onSuccess={handleRefresh}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
{isOwner && (
|
||||||
</TableRow>
|
<TableCell className="text-right">
|
||||||
))}
|
{member.role !== "owner" && (
|
||||||
{(!members?.data || members.data.length === 0) && (
|
<RemoveMemberDialog
|
||||||
|
member={member}
|
||||||
|
organizationId={org.id}
|
||||||
|
onSuccess={handleRefresh}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{(!members?.data || members.data.length === 0) && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={isOwner ? 5 : 4}
|
||||||
|
className="text-center py-6 text-muted-foreground"
|
||||||
|
>
|
||||||
|
No members found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{/* Disabled for now. We aren't using this */}
|
||||||
|
{/*
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-xl">Pending Invitations</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableHead>Email</TableHead>
|
||||||
colSpan={isOwner ? 5 : 4}
|
<TableHead>Role</TableHead>
|
||||||
className="text-center py-6 text-muted-foreground"
|
<TableHead>Status</TableHead>
|
||||||
>
|
<TableHead>Expires</TableHead>
|
||||||
No members found
|
{isOwner && <TableHead className="w-12">Actions</TableHead>}
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{invitations?.length && invitations.length > 0 ? (
|
||||||
</CardContent>
|
invitations.map((invitation) => (
|
||||||
</Card>
|
<TableRow key={invitation.id}>
|
||||||
|
<TableCell>{invitation.email}</TableCell>
|
||||||
|
<TableCell className="capitalize">
|
||||||
|
{invitation.role}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
invitation.status === "pending"
|
||||||
|
? "outline"
|
||||||
|
: invitation.status === "accepted"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{invitation.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{DateTime.fromJSDate(
|
||||||
|
new Date(invitation.expiresAt)
|
||||||
|
).toLocaleString(DateTime.DATE_SHORT)}
|
||||||
|
</TableCell>
|
||||||
|
{isOwner && (
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
await authClient.organization.cancelInvitation({
|
||||||
|
invitationId: invitation.id,
|
||||||
|
});
|
||||||
|
refetchInvitations();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={isOwner ? 5 : 4}
|
||||||
|
className="text-center py-6 text-muted-foreground"
|
||||||
|
>
|
||||||
|
No pending invitations
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card> */}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
73
server/src/api/user/addUserToOrganization.ts
Normal file
73
server/src/api/user/addUserToOrganization.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { FastifyReply, FastifyRequest } from "fastify";
|
||||||
|
import { db } from "../../db/postgres/postgres.js";
|
||||||
|
import { member, user } from "../../db/postgres/schema.js";
|
||||||
|
|
||||||
|
import { randomBytes } from "crypto";
|
||||||
|
|
||||||
|
function generateId(len = 32) {
|
||||||
|
const alphabet =
|
||||||
|
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||||
|
const bytes = randomBytes(len);
|
||||||
|
let id = "";
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
id += alphabet[bytes[i] % alphabet.length];
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddUserToOrganization {
|
||||||
|
Body: {
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addUserToOrganization(
|
||||||
|
request: FastifyRequest<AddUserToOrganization>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { email, role, organizationId } = request.body;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (!email || !role || !organizationId) {
|
||||||
|
return reply.status(400).send({
|
||||||
|
error: "Missing required fields: email, role, and organizationId",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check password strength
|
||||||
|
if (role !== "admin" && role !== "member" && role !== "owner") {
|
||||||
|
return reply.status(400).send({
|
||||||
|
error: "Role must be either admin, member, or owner",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundUser = await db.query.user.findFirst({
|
||||||
|
where: eq(user.email, email),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!foundUser) {
|
||||||
|
return reply.status(404).send({ error: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(member).values([
|
||||||
|
{
|
||||||
|
userId: foundUser.id,
|
||||||
|
organizationId: organizationId,
|
||||||
|
role: role,
|
||||||
|
id: generateId(),
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return reply.status(201).send({
|
||||||
|
message: "User added to organization successfully",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(String(error));
|
||||||
|
return reply.status(500).send({ error: String(error) });
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,10 +4,10 @@ import { auth } from "../../lib/auth.js";
|
||||||
interface CreateAccountRequest {
|
interface CreateAccountRequest {
|
||||||
Body: {
|
Body: {
|
||||||
email: string;
|
email: string;
|
||||||
username: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
password: string;
|
password: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
organizationId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,10 +16,10 @@ export async function createAccount(
|
||||||
reply: FastifyReply
|
reply: FastifyReply
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { email, name, username, password, isAdmin } = request.body;
|
const { email, name, password, isAdmin, organizationId } = request.body;
|
||||||
|
|
||||||
// Validate input
|
// Validate input
|
||||||
if (!email || !username || !password) {
|
if (!email || !password || !organizationId || !name) {
|
||||||
return reply.status(400).send({
|
return reply.status(400).send({
|
||||||
error:
|
error:
|
||||||
"Missing required fields: email, name, and password are required",
|
"Missing required fields: email, name, and password are required",
|
||||||
|
@ -35,10 +35,21 @@ export async function createAccount(
|
||||||
|
|
||||||
// Create the account using auth.api.signUpEmail
|
// Create the account using auth.api.signUpEmail
|
||||||
const result = await auth!.api.signUpEmail({
|
const result = await auth!.api.signUpEmail({
|
||||||
|
body: { email: "bill3@gmail.com", name: "billy3", password: "12345noob" },
|
||||||
|
});
|
||||||
|
// const result = await auth!.api.signUpEmail({
|
||||||
|
// body: {
|
||||||
|
// email,
|
||||||
|
// name,
|
||||||
|
// password,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
await (auth!.api as any).addMember({
|
||||||
body: {
|
body: {
|
||||||
email,
|
userId: result.user.id,
|
||||||
name,
|
organizationId: organizationId,
|
||||||
password,
|
role: isAdmin ? "admin" : "member", // this can also be an array for multiple roles (e.g. ["admin", "sale"])
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,6 +74,6 @@ export async function createAccount(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply.status(500).send({ error: "Failed to create account" });
|
return reply.status(500).send({ error: String(error) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,8 @@ export const initializeClickhouse = async () => {
|
||||||
country LowCardinality(FixedString(2)),
|
country LowCardinality(FixedString(2)),
|
||||||
region LowCardinality(String),
|
region LowCardinality(String),
|
||||||
city String,
|
city String,
|
||||||
latitude Float64,
|
lat Float64,
|
||||||
longitude Float64,
|
lon Float64,
|
||||||
screen_width UInt16,
|
screen_width UInt16,
|
||||||
screen_height UInt16,
|
screen_height UInt16,
|
||||||
device_type LowCardinality(String),
|
device_type LowCardinality(String),
|
||||||
|
|
|
@ -193,7 +193,6 @@ export const invitation = pgTable(
|
||||||
role: text().notNull(),
|
role: text().notNull(),
|
||||||
status: text().notNull(),
|
status: text().notNull(),
|
||||||
expiresAt: timestamp({ mode: "string" }).notNull(),
|
expiresAt: timestamp({ mode: "string" }).notNull(),
|
||||||
createdAt: timestamp({ mode: "string" }).notNull(),
|
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
foreignKey({
|
foreignKey({
|
||||||
|
|
|
@ -50,6 +50,7 @@ import { createPortalSession } from "./api/stripe/createPortalSession.js";
|
||||||
import { getSubscription } from "./api/stripe/getSubscription.js";
|
import { getSubscription } from "./api/stripe/getSubscription.js";
|
||||||
import { handleWebhook } from "./api/stripe/webhook.js";
|
import { handleWebhook } from "./api/stripe/webhook.js";
|
||||||
import { IS_CLOUD } from "./lib/const.js";
|
import { IS_CLOUD } from "./lib/const.js";
|
||||||
|
import { addUserToOrganization } from "./api/user/addUserToOrganization.js";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
@ -219,6 +220,7 @@ server.get(
|
||||||
listOrganizationMembers
|
listOrganizationMembers
|
||||||
);
|
);
|
||||||
server.get("/user/organizations", getUserOrganizations);
|
server.get("/user/organizations", getUserOrganizations);
|
||||||
|
server.post("/add-user-to-organization", addUserToOrganization);
|
||||||
|
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
// Stripe Routes
|
// Stripe Routes
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { admin, organization, username } from "better-auth/plugins";
|
import { admin, organization } from "better-auth/plugins";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import pg from "pg";
|
import pg from "pg";
|
||||||
|
@ -12,26 +12,15 @@ dotenv.config();
|
||||||
|
|
||||||
type AuthType = ReturnType<typeof betterAuth> | null;
|
type AuthType = ReturnType<typeof betterAuth> | null;
|
||||||
|
|
||||||
const pluginList = IS_CLOUD
|
const pluginList = [
|
||||||
? [
|
admin(),
|
||||||
admin(),
|
organization({
|
||||||
organization({
|
// Allow users to create organizations
|
||||||
// Allow users to create organizations
|
allowUserToCreateOrganization: true,
|
||||||
allowUserToCreateOrganization: true,
|
// Set the creator role to owner
|
||||||
// Set the creator role to owner
|
creatorRole: "owner",
|
||||||
creatorRole: "owner",
|
}),
|
||||||
}),
|
];
|
||||||
]
|
|
||||||
: [
|
|
||||||
username(),
|
|
||||||
admin(),
|
|
||||||
organization({
|
|
||||||
// Allow users to create organizations
|
|
||||||
allowUserToCreateOrganization: true,
|
|
||||||
// Set the creator role to owner
|
|
||||||
creatorRole: "owner",
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
export let auth: AuthType | null = betterAuth({
|
export let auth: AuthType | null = betterAuth({
|
||||||
basePath: "/auth",
|
basePath: "/auth",
|
||||||
|
@ -53,7 +42,7 @@ export let auth: AuthType | null = betterAuth({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: pluginList as any,
|
plugins: pluginList,
|
||||||
trustedOrigins: ["http://localhost:3002"],
|
trustedOrigins: ["http://localhost:3002"],
|
||||||
advanced: {
|
advanced: {
|
||||||
useSecureCookies: process.env.NODE_ENV === "production", // don't mark Secure in dev
|
useSecureCookies: process.env.NODE_ENV === "production", // don't mark Secure in dev
|
||||||
|
@ -140,7 +129,7 @@ export function initAuth(allowedOrigins: string[]) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: pluginList as any,
|
plugins: pluginList,
|
||||||
trustedOrigins: allowedOrigins,
|
trustedOrigins: allowedOrigins,
|
||||||
advanced: {
|
advanced: {
|
||||||
useSecureCookies: process.env.NODE_ENV === "production",
|
useSecureCookies: process.env.NODE_ENV === "production",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue