Skip to content

Commit

Permalink
Permissions and settings (#58)
Browse files Browse the repository at this point in the history
* wip

* wip
  • Loading branch information
pontusab authored Jan 11, 2025
1 parent 96bb8cb commit f4334c5
Show file tree
Hide file tree
Showing 28 changed files with 764 additions and 160 deletions.
1 change: 0 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
"zod": "^3.24.1"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.63.0",
"@types/node": "^22",
"@types/react": "^19",
"react-email": "3.0.5",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app/[locale]/(dashboard)/(sidebar)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default async function Layout({
</SidebarInset>
</div>
</SidebarProvider>

<GlobalModals />
</NuqsAdapter>
</TRPCProvider>
Expand Down

This file was deleted.

45 changes: 45 additions & 0 deletions apps/web/src/app/api/invite/[inviteId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { acceptInvitation } from "@/lib/auth/queries";
import { getSession } from "@/lib/session";
import { trpc } from "@/trpc/server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function GET(
_: Request,
{ params }: { params: { inviteId: string } },
) {
try {
const { inviteId } = params;

// Check if user is logged in
const session = await getSession();
const cookieStore = await cookies();

if (!session?.data) {
// Set invitation cookie before redirecting
cookieStore.set("invitationId", inviteId);
redirect("/login");
}

const invite = await acceptInvitation(inviteId);

if (!invite) {
redirect("/login");
}

// Get organization details
const organization = await trpc.organization.getById({
organizationId: invite.invitation.organizationId,
});

// Redirect to organization dashboard if found
if (organization) {
redirect(`/${organization.slug}/default`);
}

// Fallback redirect to login
redirect("/login");
} catch (error) {
redirect("/login");
}
}
38 changes: 38 additions & 0 deletions apps/web/src/app/api/invite/accept/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { deleteInvitation } from "@/db/queries/organization";
import { acceptInvitation } from "@/lib/auth/queries";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
const cookieStore = await cookies();
const invitationId = cookieStore.get("invitationId")?.value;

if (!invitationId) {
return NextResponse.redirect(new URL("/login", request.url));
}

try {
const invite = await acceptInvitation(invitationId);

if (!invite) {
return NextResponse.redirect(new URL("/login", request.url));
}

// Clear the invitation cookie
cookieStore.delete("invitationId");

await deleteInvitation(invitationId);

// Redirect to organization dashboard if found
if (invite.invitation.organizationId) {
return NextResponse.redirect(
new URL(`/${invite.invitation.organizationId}/default`, request.url),
);
}

// Fallback redirect to login
return NextResponse.redirect(new URL("/login", request.url));
} catch {
return NextResponse.redirect(new URL("/login", request.url));
}
}
49 changes: 35 additions & 14 deletions apps/web/src/components/copy-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { Check, Copy } from "lucide-react";
import { Check, Copy, RotateCw } from "lucide-react";
import * as React from "react";

interface CopyInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
value: string;
onUpdate?: () => void;
}

export function CopyInput({ value, className, ...props }: CopyInputProps) {
export function CopyInput({
value,
className,
onUpdate,
...props
}: CopyInputProps) {
const [copied, setCopied] = React.useState(false);

const onCopy = async () => {
Expand All @@ -20,24 +26,39 @@ export function CopyInput({ value, className, ...props }: CopyInputProps) {
};

return (
<div className="relative flex items-center cursor-pointer" onClick={onCopy}>
<div className="relative flex items-center">
<Input
value={value}
className={cn("pr-12 cursor-pointer", className)}
className={cn("pr-24 cursor-pointer", className)}
onClick={onCopy}
{...props}
readOnly
/>
<Button
variant="ghost"
size="icon"
className="absolute right-0 h-full px-3 transition-opacity hover:bg-transparent pointer-events-none"
>
{copied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4 text-muted-foreground" />
<div className="absolute right-4 flex">
{onUpdate && (
<Button
variant="ghost"
size="icon"
onClick={onUpdate}
className="h-full px-3 transition-opacity hover:bg-transparent w-8"
>
<RotateCw className="h-4 w-4 text-muted-foreground" />
</Button>
)}
</Button>

<Button
variant="ghost"
size="icon"
onClick={onCopy}
className="h-full px-3 transition-opacity hover:bg-transparent w-8"
>
{copied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4 text-muted-foreground" />
)}
</Button>
</div>
</div>
);
}
2 changes: 0 additions & 2 deletions apps/web/src/components/google-sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ export default function GoogleSignIn() {
const [isLoading, setIsLoading] = useState(false);

const handleGoogleLogin = async () => {
console.log("Google login", window.location.origin);

setIsLoading(true);
try {
await authClient.signIn.social({
Expand Down
28 changes: 24 additions & 4 deletions apps/web/src/components/settings-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { useI18n } from "@/locales/client";
import { TRPCClientError } from "@trpc/client";
import { useState } from "react";
import { toast } from "sonner";

Expand All @@ -42,6 +43,7 @@ export function SettingsCard({
placeholder,
isLoading,
validate,
onUpdate,
}: {
title: string;
description: string;
Expand All @@ -55,6 +57,7 @@ export function SettingsCard({
placeholder?: string;
isLoading?: boolean;
validate?: "email" | "url" | "number" | "password" | "text";
onUpdate?: () => void;
}) {
const t = useI18n();
const [isSaving, setIsSaving] = useState(false);
Expand All @@ -64,13 +67,26 @@ export function SettingsCard({
try {
setIsSaving(true);
await onSave?.(inputValue);

toast.success(t("settings.saved"), {
description: t("settings.savedDescription"),
});
} catch (error) {
toast.error(t("settings.error"), {
description: t("settings.errorDescription"),
});
if (error instanceof TRPCClientError) {
if (error.data?.code === "FORBIDDEN") {
toast.error(t("settings.permissionDenied"), {
description: t("settings.permissionDeniedDescription"),
});
} else {
toast.error(t("settings.error"), {
description: t("settings.errorDescription"),
});
}
} else {
toast.error(t("settings.error"), {
description: t("settings.errorDescription"),
});
}
} finally {
setIsSaving(false);
}
Expand Down Expand Up @@ -174,7 +190,11 @@ export function SettingsCard({
/>
)}
{type === "copy-input" && value && (
<CopyInput value={value} placeholder={placeholder} />
<CopyInput
value={value}
placeholder={placeholder}
onUpdate={onUpdate}
/>
)}
</CardContent>
</Card>
Expand Down
Loading

0 comments on commit f4334c5

Please sign in to comment.