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:
@@ -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>
|
||||||
|
|||||||
@@ -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) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user