feat: add backup recovery, setup wizard and user-view guards
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import InviteDeliveryChoice from "../../ui/InviteDeliveryChoice";
|
||||
|
||||
import PageHeading from "../../ui/PageHeading";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { authFetch, clearToken, getApiBase, getToken } from "../../lib/auth";
|
||||
import { useEffectiveRole } from "../../lib/viewMode";
|
||||
import InviteDeliveryChoice from "../../ui/InviteDeliveryChoice";
|
||||
import PageHeading from "../../ui/PageHeading";
|
||||
|
||||
type ProfileInfo = { username: string; role: string; invite_management_enabled?: boolean };
|
||||
type OwnedInvite = {
|
||||
@@ -77,6 +76,10 @@ export default function ProfileInvitesPage() {
|
||||
const [deliveryMethod, setDeliveryMethod] = useState<DeliveryMethod>("");
|
||||
const [inviteForm, setInviteForm] = useState<InviteForm>(defaultInviteForm());
|
||||
const [createdInvite, setCreatedInvite] = useState<OwnedInvite | null>(null);
|
||||
const effectiveRole = useEffectiveRole(profile?.role);
|
||||
const canManageInvites =
|
||||
effectiveRole === "admin" ||
|
||||
(profile?.role === "admin" ? Boolean(profile.invite_management_enabled) : inviteAccessEnabled);
|
||||
|
||||
const signupBaseUrl = useMemo(() => {
|
||||
if (typeof window === "undefined") return "/signup";
|
||||
@@ -158,6 +161,7 @@ export default function ProfileInvitesPage() {
|
||||
|
||||
const saveInvite = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!canManageInvites) return;
|
||||
const inviteName = inviteForm.label.trim();
|
||||
const recipientEmail = inviteForm.recipient_email.trim();
|
||||
if (!inviteName) {
|
||||
@@ -225,6 +229,7 @@ export default function ProfileInvitesPage() {
|
||||
};
|
||||
|
||||
const deleteInvite = async (invite: OwnedInvite) => {
|
||||
if (!canManageInvites) return;
|
||||
if (!window.confirm(`Delete invite “${invite.label || invite.code}”?`)) return;
|
||||
setError(null);
|
||||
try {
|
||||
@@ -240,6 +245,7 @@ export default function ProfileInvitesPage() {
|
||||
};
|
||||
|
||||
const copyInviteLink = async (invite: OwnedInvite) => {
|
||||
if (!canManageInvites) return;
|
||||
try {
|
||||
let usableInvite = invite;
|
||||
if (!invite.code_available) {
|
||||
@@ -263,7 +269,6 @@ export default function ProfileInvitesPage() {
|
||||
|
||||
const codeCharacters = inviteForm.code.replace(/[^a-z0-9]/gi, "");
|
||||
const identityReady = Boolean(inviteForm.label.trim() && (!useCustomCode || codeCharacters.length >= 6));
|
||||
const canManageInvites = profile?.role === "admin" || inviteAccessEnabled;
|
||||
const createdInviteUrl = createdInvite ? `${signupBaseUrl}?code=${encodeURIComponent(createdInvite.code)}` : "";
|
||||
|
||||
if (loading) return <main className="card">Loading invite workspace…</main>;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { type FormEvent, type KeyboardEvent, useCallback, useEffect, useState } from "react";
|
||||
import { authFetch, clearToken, getApiBase, getToken } from "../lib/auth";
|
||||
import { canAccess, type FeatureAccess } from "../lib/features";
|
||||
import { useEffectiveRole } from "../lib/viewMode";
|
||||
import PageHeading from "../ui/PageHeading";
|
||||
import MonthlyRecapPreference from "./MonthlyRecapPreference";
|
||||
import NewsletterPreference from "./NewsletterPreference";
|
||||
|
||||
import { useCallback, useEffect, useState, type FormEvent, type KeyboardEvent } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { authFetch, clearToken, getApiBase, getToken } from "../lib/auth";
|
||||
|
||||
type ProfileInfo = {
|
||||
features?: FeatureAccess;
|
||||
username: string;
|
||||
@@ -214,6 +214,7 @@ export default function ProfilePage() {
|
||||
};
|
||||
|
||||
const user = data?.user;
|
||||
const effectiveRole = useEffectiveRole(user?.role);
|
||||
const passwordProvider = user?.password_provider ?? (user?.auth_provider === "jellyfin" ? "jellyfin" : "local");
|
||||
const canChangePassword =
|
||||
user?.password_change_supported ?? ["local", "jellyfin"].includes(user?.auth_provider ?? "");
|
||||
@@ -239,7 +240,7 @@ export default function ProfilePage() {
|
||||
</span>
|
||||
<div>
|
||||
<strong>{user.username}</strong>
|
||||
<span>{user.role === "admin" ? "Administrator" : "Member"}</span>
|
||||
<span>{effectiveRole === "admin" ? "Administrator" : "Member"}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -340,7 +341,9 @@ export default function ProfilePage() {
|
||||
: "Signed in with your media account"}
|
||||
</span>
|
||||
</div>
|
||||
{canAccess(user, "stats") && <MonthlyRecapPreference key={user.email || "no-email"} />}
|
||||
{canAccess({ ...user, role: effectiveRole ?? undefined }, "stats") && (
|
||||
<MonthlyRecapPreference key={user.email || "no-email"} />
|
||||
)}
|
||||
<NewsletterPreference key={`newsletter-${user.email || "no-email"}`} />
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user