fix: footer rebrand + iç sayfada genel-bakış'a yanlış redirect

1) Footer

The template's 'Made with ❤ by ShadcnStore Team' is gone. Footer now
shows '© <year> İşletmem — bir KovakSoft ürünüdür.' on the left and
Kullanım şartları / Gizlilik links on the right. Compact one-line layout
on desktop, stacked on mobile.

2) Internal pages were redirecting users to /onboarding (and from there
   to /dashboard) even when they had a valid tenant.

Root cause: requireTenant() called getActiveTenantId() and threw
NO_TENANT when the active-tenant cookie/prefs were missing — even though
the user's actually a member of one or more teams. getActiveContext()
already handled this fallback for the layout, but the per-page
requireTenant() guard didn't, so /customers, /tasks, /invoices, etc. all
bounced through /onboarding back to /dashboard.

- requireTenant() now falls back to the user's first team via
  teams.list(), and writes the cookie so the next request is fast.

3) Side fix: acceptInviteAction now sets account.prefs.activeTenant + the
   ACTIVE_TENANT cookie after successful join, so a freshly invited
   member lands directly in the right workspace instead of relying on
   the team-list fallback.
This commit is contained in:
kovakmedya
2026-04-30 06:39:27 +03:00
parent 858c916d95
commit 02a02ba9e6
3 changed files with 72 additions and 17 deletions
+19 -14
View File
@@ -1,27 +1,32 @@
import { Heart } from "lucide-react"
import Link from "next/link" import Link from "next/link"
export function SiteFooter() { export function SiteFooter() {
const year = new Date().getFullYear()
return ( return (
<footer className="border-t bg-background"> <footer className="bg-background border-t">
<div className="px-4 py-6 lg:px-6"> <div className="px-4 py-4 lg:px-6">
<div className="flex flex-col items-center justify-center space-y-2 text-center"> <div className="text-muted-foreground flex flex-col items-center justify-between gap-2 text-xs sm:flex-row">
<div className="flex items-center space-x-2 text-sm text-muted-foreground"> <p>
<span>Made with</span> © {year} İşletmem bir{" "}
<Heart className="h-4 w-4 fill-red-500 text-red-500" />
<span>by</span>
<Link <Link
href="https://shadcnstore.com" href="https://kovaksoft.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-foreground hover:text-primary transition-colors" className="text-foreground hover:text-primary font-medium transition-colors"
> >
ShadcnStore Team KovakSoft
</Link>{" "}
ürünüdür.
</p>
<div className="flex items-center gap-3">
<Link href="#" className="hover:text-foreground transition-colors">
Kullanım şartları
</Link>
<span aria-hidden>·</span>
<Link href="#" className="hover:text-foreground transition-colors">
Gizlilik
</Link> </Link>
</div> </div>
<p className="text-xs text-muted-foreground">
Building beautiful, accessible blocks, templates and dashboards for modern web applications.
</p>
</div> </div>
</div> </div>
</footer> </footer>
+30 -1
View File
@@ -1,5 +1,6 @@
"use server"; "use server";
import { cookies } from "next/headers";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { AppwriteException, ID, Permission, Query, Role } from "node-appwrite"; import { AppwriteException, ID, Permission, Query, Role } from "node-appwrite";
@@ -8,6 +9,7 @@ import { DATABASE_ID, TABLES, type InviteLink, type InviteRole } from "./schema"
import { createAdminClient, createSessionClient } from "./server"; import { createAdminClient, createSessionClient } from "./server";
import { requireRole, requireTenant } from "./tenant-guard"; import { requireRole, requireTenant } from "./tenant-guard";
import type { InviteState, MemberActionState } from "./team-types"; import type { InviteState, MemberActionState } from "./team-types";
import { ACTIVE_TENANT_COOKIE } from "./tenant-types";
const APP_URL = process.env.APP_URL ?? "http://localhost:3000"; const APP_URL = process.env.APP_URL ?? "http://localhost:3000";
const INVITE_TTL_DAYS = 7; const INVITE_TTL_DAYS = 7;
@@ -306,7 +308,7 @@ export async function resolveInviteCode(code: string): Promise<InviteLink | null
export async function acceptInviteAction(code: string): Promise<MemberActionState> { export async function acceptInviteAction(code: string): Promise<MemberActionState> {
if (!code) return { ok: false, error: "Geçersiz davet linki." }; if (!code) return { ok: false, error: "Geçersiz davet linki." };
let user; let user: Awaited<ReturnType<Awaited<ReturnType<typeof createSessionClient>>["account"]["get"]>>;
try { try {
const session = await createSessionClient(); const session = await createSessionClient();
user = await session.account.get(); user = await session.account.get();
@@ -331,6 +333,30 @@ export async function acceptInviteAction(code: string): Promise<MemberActionStat
const admin = createAdminClient(); const admin = createAdminClient();
const activeTenantId = invite.tenantId;
async function activateTenant() {
try {
const session = await createSessionClient();
await session.account.updatePrefs({
...(user.prefs ?? {}),
activeTenant: activeTenantId,
});
} catch {
/* ignore */
}
try {
(await cookies()).set(ACTIVE_TENANT_COOKIE, activeTenantId, {
path: "/",
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24 * 365,
});
} catch {
/* ignore */
}
}
// Already a member? Mark accepted, redirect. // Already a member? Mark accepted, redirect.
try { try {
const memberships = await admin.teams.listMemberships(invite.tenantId); const memberships = await admin.teams.listMemberships(invite.tenantId);
@@ -341,6 +367,7 @@ export async function acceptInviteAction(code: string): Promise<MemberActionStat
acceptedAt: new Date().toISOString(), acceptedAt: new Date().toISOString(),
acceptedBy: user.$id, acceptedBy: user.$id,
}); });
await activateTenant();
return { ok: true }; return { ok: true };
} }
} catch { } catch {
@@ -369,6 +396,8 @@ export async function acceptInviteAction(code: string): Promise<MemberActionStat
entityId: invite.$id, entityId: invite.$id,
changes: { via: "invite", code }, changes: { via: "invite", code },
}); });
await activateTenant();
} catch (e) { } catch (e) {
return { ok: false, error: appwriteError(e) }; return { ok: false, error: appwriteError(e) };
} }
+23 -2
View File
@@ -1,10 +1,12 @@
import "server-only"; import "server-only";
import { cookies } from "next/headers";
import { Query } from "node-appwrite"; import { Query } from "node-appwrite";
import { createAdminClient, getCurrentUser } from "./server"; import { createAdminClient, getCurrentUser } from "./server";
import { DATABASE_ID, TABLES, type TenantSettings } from "./schema"; import { DATABASE_ID, TABLES, type TenantSettings } from "./schema";
import { getActiveTenantId } from "./tenant"; import { ACTIVE_TENANT_COOKIE } from "./tenant-types";
import { getActiveTenantId, getUserTeams } from "./tenant";
export type TenantRole = "owner" | "admin" | "member"; export type TenantRole = "owner" | "admin" | "member";
@@ -26,7 +28,26 @@ export async function requireTenant(): Promise<TenantContext> {
const user = await getCurrentUser(); const user = await getCurrentUser();
if (!user) throw new Error("UNAUTHENTICATED"); if (!user) throw new Error("UNAUTHENTICATED");
const tenantId = await getActiveTenantId(); let tenantId = await getActiveTenantId();
if (!tenantId) {
// Fallback: pick the user's first team (handles invite acceptees and
// sessions where the active-tenant cookie/prefs weren't set yet).
const userTeams = await getUserTeams();
tenantId = userTeams?.teams[0]?.$id ?? null;
if (tenantId) {
try {
(await cookies()).set(ACTIVE_TENANT_COOKIE, tenantId, {
path: "/",
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24 * 365,
});
} catch {
/* setting cookie can fail in some Server Component paths; ignore */
}
}
}
if (!tenantId) throw new Error("NO_TENANT"); if (!tenantId) throw new Error("NO_TENANT");
const { tablesDB, teams } = createAdminClient(); const { tablesDB, teams } = createAdminClient();