mirror of
https://github.com/rajnandan1/kener.git
synced 2026-06-23 04:10:22 +00:00
changes
This commit is contained in:
@@ -2,6 +2,7 @@ import subscriptionAccountCodeTemplate from "../src/lib/server/templates/general
|
||||
import subscriptionUpdateTemplate from "../src/lib/server/templates/general/subscription_update_template.ts";
|
||||
import forgotPasswordTemplate from "../src/lib/server/templates/general/forgot_password_template.ts";
|
||||
import inviteUserTemplate from "../src/lib/server/templates/general/invite_user_template.ts";
|
||||
import verifyEmailTemplate from "../src/lib/server/templates/general/verify_email_template.ts";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function seed(knex: Knex): Promise<void> {
|
||||
@@ -58,4 +59,18 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
template_text_body: inviteUserTemplate.template_text_body,
|
||||
});
|
||||
}
|
||||
|
||||
count = await knex("general_email_templates")
|
||||
.where({ template_id: verifyEmailTemplate.template_id })
|
||||
.count("template_id as CNT")
|
||||
.first();
|
||||
|
||||
if (count && count.CNT == 0) {
|
||||
await knex("general_email_templates").insert({
|
||||
template_id: verifyEmailTemplate.template_id,
|
||||
template_subject: verifyEmailTemplate.template_subject,
|
||||
template_html_body: verifyEmailTemplate.template_html_body,
|
||||
template_text_body: verifyEmailTemplate.template_text_body,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ export default {
|
||||
STATUS: "STATUS",
|
||||
LATENCY: "LATENCY",
|
||||
UPTIME: "UPTIME",
|
||||
DOCS_URL: "https://kener.ing/docs",
|
||||
} as const;
|
||||
|
||||
const AnalyticsProviders = {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const IsResendSetup = () => {
|
||||
};
|
||||
|
||||
export const IsEmailSetup = () => {
|
||||
return !!GetSMTPFromENV || IsResendSetup();
|
||||
return !!GetSMTPFromENV() || IsResendSetup();
|
||||
};
|
||||
export const SendEmailWithTemplate = async (
|
||||
template: string,
|
||||
|
||||
@@ -377,3 +377,48 @@ export const ResendInvitationEmail = async (email: string, currentUserRole: stri
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// send verification email with verification link
|
||||
export const SendVerificationEmail = async (toUserId: number, currentUser: { id: number; role: string }) => {
|
||||
if (!toUserId) {
|
||||
throw new Error("User ID is required");
|
||||
}
|
||||
|
||||
// Only admins/editors can send verification to other users.
|
||||
// Members can only send verification email to themselves.
|
||||
if (currentUser.role === "member" && currentUser.id !== toUserId) {
|
||||
throw new Error("You do not have permission to send verification email for this user");
|
||||
}
|
||||
|
||||
const user = await db.getUserById(toUserId);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
if (user.is_verified) {
|
||||
throw new Error("User email is already verified");
|
||||
}
|
||||
|
||||
const token = await GenerateToken({
|
||||
email: user.email,
|
||||
validTill: Date.now() + 24 * 60 * 60 * 1000, //24 hours
|
||||
});
|
||||
|
||||
const siteData = await GetAllSiteData();
|
||||
const siteVars = siteDataToVariables(siteData);
|
||||
const siteUrl = siteVars.site_url || "";
|
||||
const verificationLink = `${siteUrl}/account/verify?view=confirm_token&token=${token}`;
|
||||
|
||||
const emailVars = {
|
||||
...siteVars,
|
||||
verification_link: verificationLink,
|
||||
};
|
||||
|
||||
const template = await GetGeneralEmailTemplateById("verify_email");
|
||||
if (!template) {
|
||||
throw new Error("Verify email template not found");
|
||||
}
|
||||
await sendEmail(template.template_html_body || "", template.template_subject || "Verify Your Email", emailVars, [
|
||||
user.email,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -47,7 +47,11 @@ export default async function send(
|
||||
html: htmlBody,
|
||||
text: textBody,
|
||||
};
|
||||
return await resend.emails.send(emailBody);
|
||||
let resp = await resend.emails.send(emailBody);
|
||||
if (!!resp.error) {
|
||||
throw new Error(`Resend API error: ${resp.error.message}`);
|
||||
}
|
||||
return resp;
|
||||
} else if (mySMTPData) {
|
||||
// SMTP Configuration
|
||||
const transport = getSMTPTransport(mySMTPData as SMTPConfiguration);
|
||||
@@ -59,9 +63,11 @@ export default async function send(
|
||||
html: htmlBody, // HTML body (if any)
|
||||
};
|
||||
return await transport.sendMail(mailOptions);
|
||||
} else {
|
||||
throw new Error("No valid email configuration found. Please check your SMTP or Resend settings.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending email", error);
|
||||
return error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
const emailTemplate = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<link rel="preload" as="image" href="{{site_logo_url}}" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
</head>
|
||||
<body
|
||||
style="
|
||||
background-color: rgb(243, 244, 246);
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
"
|
||||
>
|
||||
<!--$-->
|
||||
<div
|
||||
style="
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
line-height: 1px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
"
|
||||
>
|
||||
Verify your email for {{site_name}}
|
||||
</div>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 8px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
padding: 24px;
|
||||
max-width: 600px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr style="width: 100%">
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="margin-top: 8px; margin-bottom: 32px; text-align: center"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
alt="{{site_name}}"
|
||||
height="40"
|
||||
src="{{site_logo_url}}"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
"
|
||||
width="120"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h1
|
||||
style="
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: rgb(31, 41, 55);
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
Verify Your Email
|
||||
</h1>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
Please confirm your email address for {{site_name}} by clicking the button below:
|
||||
</p>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{verification_link}}"
|
||||
style="
|
||||
display: inline-block;
|
||||
background-color: rgb(59, 130, 246);
|
||||
color: rgb(255, 255, 255);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
padding: 12px 32px;
|
||||
border-radius: 8px;
|
||||
line-height: 24px;
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
Verify Email
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p
|
||||
style="
|
||||
font-size: 14px;
|
||||
color: rgb(107, 114, 128);
|
||||
margin-bottom: 16px;
|
||||
line-height: 20px;
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
Or copy and paste this URL into your browser:
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 14px;
|
||||
color: rgb(59, 130, 246);
|
||||
margin-bottom: 24px;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
{{verification_link}}
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
If you didn't request this, you can safely ignore this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--7--><!--/$-->
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
export default {
|
||||
template_id: "verify_email",
|
||||
template_subject: "{{site_name}} - Verify Your Email",
|
||||
template_html_body: emailTemplate,
|
||||
template_text_body: `Verify your email for {{site_name}}\n\nPlease confirm your email address by clicking the link below:\n\n{{verification_link}}\n\nIf you didn't request this, you can safely ignore this email.`,
|
||||
};
|
||||
+4
-12
@@ -1,7 +1,3 @@
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import fs from "fs";
|
||||
|
||||
function getVersionUsingVite() {
|
||||
try {
|
||||
if (!!import.meta.env.PACKAGE_VERSION) {
|
||||
@@ -18,14 +14,10 @@ export default function version() {
|
||||
if (!!v) {
|
||||
return v;
|
||||
} else {
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const packagePath = resolve(__dirname, "../../package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
return packageJson.version;
|
||||
} catch (e) {
|
||||
return "0.0.0";
|
||||
// Browser-safe fallback (avoid importing Node modules in client bundle)
|
||||
if (typeof process !== "undefined" && process.env?.npm_package_version) {
|
||||
return process.env.npm_package_version;
|
||||
}
|
||||
return "0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { VerifyToken } from "$lib/server/controllers/commonController.js";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import serverResolve from "$lib/server/resolver.js";
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
const view = url.searchParams.get("view") || "";
|
||||
const token = url.searchParams.get("token") || "";
|
||||
|
||||
if (view !== "confirm_token" || !token) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Invalid or missing verification link.",
|
||||
};
|
||||
}
|
||||
|
||||
const tokenData = await VerifyToken(token);
|
||||
if (!tokenData) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Invalid or expired verification link.",
|
||||
};
|
||||
}
|
||||
|
||||
const email = tokenData.email;
|
||||
if (!email) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Invalid verification link.",
|
||||
};
|
||||
}
|
||||
|
||||
const validTill = tokenData.validTill;
|
||||
if (!validTill || Date.now() > validTill) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "This verification link has expired. Please request a new one.",
|
||||
};
|
||||
}
|
||||
|
||||
const user = await db.getUserByEmail(email);
|
||||
if (!user) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "No user found for this verification link.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!user.is_verified) {
|
||||
await db.updateIsVerified(user.id, 1);
|
||||
}
|
||||
|
||||
throw redirect(302, serverResolve("/manage/app/users"));
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import * as Card from "$lib/components/ui/card/index.js";
|
||||
import AlertCircleIcon from "@lucide/svelte/icons/alert-circle";
|
||||
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
|
||||
import { resolve } from "$app/paths";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
|
||||
const { data } = $props();
|
||||
const error: string = $derived(data.error || "Invalid verification link.");
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Email Verification</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center p-4">
|
||||
<Card.Root class="kener-card w-full max-w-md">
|
||||
<Card.Header class="text-center">
|
||||
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
|
||||
<AlertCircleIcon class="h-8 w-8 text-red-600" />
|
||||
</div>
|
||||
<Card.Title>Email Verification Failed</Card.Title>
|
||||
<Card.Description>{error}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Button href={clientResolver(resolve, "/account/signin")} class="w-full">
|
||||
<ArrowLeftIcon class="mr-2 h-4 w-4" />
|
||||
Go to Sign In
|
||||
</Button>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
@@ -28,11 +28,9 @@ export const load: LayoutServerLoad = async ({ cookies }) => {
|
||||
|
||||
const siteData = await GetAllSiteData();
|
||||
|
||||
let selectedLang = GetLocaleFromCookie(siteData, cookies);
|
||||
const siteStatusColors = siteData.colors;
|
||||
const siteStatusColorsDark = siteData.colorsDark || siteStatusColors;
|
||||
const font = siteData.font || { cssSrc: "", family: "" };
|
||||
|
||||
// const emailSubscriptionTrigger = await GetSubscriptionTriggerByEmail();
|
||||
return {
|
||||
userDb: loggedInUser,
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
GetSiteLogoURL,
|
||||
UpdatePassword,
|
||||
SendInvitationEmail,
|
||||
SendVerificationEmail,
|
||||
ResendInvitationEmail,
|
||||
GetUserPasswordHashById,
|
||||
GetAllUsersPaginatedDashboard,
|
||||
@@ -180,6 +181,13 @@ export async function POST({ request, cookies }) {
|
||||
AdminEditorCan(userDB.role);
|
||||
await ResendInvitationEmail(data.email, userDB.role);
|
||||
resp = { success: true };
|
||||
} else if (action == "sendVerificationEmail") {
|
||||
const toId = parseInt(String(data.toId));
|
||||
if (!toId) {
|
||||
throw new Error("User ID is required");
|
||||
}
|
||||
await SendVerificationEmail(toId, { id: userDB.id, role: userDB.role });
|
||||
resp = { success: true };
|
||||
} else if (action == "getUsers") {
|
||||
const page = parseInt(String(data.page)) || 1;
|
||||
const limit = parseInt(String(data.limit)) || 10;
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import NavMain from "./nav-main.svelte";
|
||||
import NavUser from "./nav-user.svelte";
|
||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||
import version from "$lib/version";
|
||||
import type { Component, ComponentProps } from "svelte";
|
||||
import { resolve } from "$app/paths";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
type NavItem = { title: string; url: string; icon: Component };
|
||||
|
||||
let { navItems, ...restProps }: { navItems: NavItem[] } & ComponentProps<typeof Sidebar.Root> = $props();
|
||||
const appVersion = version();
|
||||
</script>
|
||||
|
||||
<Sidebar.Root collapsible="offcanvas" {...restProps}>
|
||||
@@ -17,9 +19,14 @@
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton class="data-[slot=sidebar-menu-button]:p-1.5!">
|
||||
{#snippet child({ props })}
|
||||
<a href={clientResolver(resolve, "/manage/app")} {...props}>
|
||||
<a
|
||||
href={clientResolver(resolve, "/manage/app")}
|
||||
{...props}
|
||||
class="justify-start-safe flex items-center gap-2"
|
||||
>
|
||||
<img src={clientResolver(resolve, "/logo96.png")} class="size-5!" alt="Kener Logo" />
|
||||
<span class="text-base font-semibold"> Kener </span>
|
||||
<span class="text-muted-foreground pt-0.5 text-xs font-medium"> v{appVersion} </span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { IsEmailSetup } from "$lib/server/controllers/emailController.js";
|
||||
import { GetLoggedInSession } from "$lib/server/controllers/userController.js";
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const canSendEmail = IsEmailSetup();
|
||||
const loggedInUser = await GetLoggedInSession(cookies);
|
||||
|
||||
return {
|
||||
canSendEmail,
|
||||
user: loggedInUser || null,
|
||||
};
|
||||
};
|
||||
@@ -8,6 +8,9 @@
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { Input } from "$lib/components/ui/input/index.js";
|
||||
import { Label } from "$lib/components/ui/label/index.js";
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import GC from "$lib/global-constants";
|
||||
|
||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
||||
import UsersIcon from "@lucide/svelte/icons/users";
|
||||
@@ -20,10 +23,11 @@
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import EyeClosedIcon from "@lucide/svelte/icons/eye-closed";
|
||||
import EyeOpenIcon from "@lucide/svelte/icons/eye";
|
||||
import * as InputGroup from "$lib/components/ui/input-group/index.js";
|
||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { format } from "date-fns";
|
||||
import type { UserRecordDashboard } from "$lib/server/types/db.js";
|
||||
import { onMount } from "svelte";
|
||||
import type { UserRecordDashboard, UserRecordPublic } from "$lib/server/types/db.js";
|
||||
import { resolve } from "$app/paths";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
|
||||
@@ -45,14 +49,14 @@
|
||||
}
|
||||
|
||||
interface PageData {
|
||||
user: UserRecordDashboard;
|
||||
userDb: UserRecordPublic;
|
||||
canSendEmail: boolean;
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Derived from data
|
||||
let currentUser = $derived(data.user);
|
||||
let currentUser = $derived(data.userDb);
|
||||
let canSendEmail = $derived(data.canSendEmail);
|
||||
|
||||
// State
|
||||
@@ -79,6 +83,7 @@
|
||||
let toEditUser = $state<EditUser | null>(null);
|
||||
let manualUpdateError = $state("");
|
||||
let manualSuccess = $state("");
|
||||
let sendingSelfVerification = $state(false);
|
||||
|
||||
// Fetch users
|
||||
async function fetchUsers() {
|
||||
@@ -183,8 +188,9 @@
|
||||
|
||||
// Send verification email
|
||||
async function sendVerificationEmail(id: number) {
|
||||
sendingSelfVerification = true;
|
||||
try {
|
||||
await fetch(clientResolver(resolve, "/manage/api"), {
|
||||
const res = await fetch(clientResolver(resolve, "/manage/api"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -192,9 +198,16 @@
|
||||
data: { toId: id }
|
||||
})
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok || result.error) {
|
||||
throw new Error(result.error || "Failed to send verification email");
|
||||
}
|
||||
toast.success("Verification email sent");
|
||||
} catch (error) {
|
||||
toast.error("Failed to send verification email");
|
||||
const message = error instanceof Error ? error.message : "Failed to send verification email";
|
||||
toast.error(message);
|
||||
} finally {
|
||||
sendingSelfVerification = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +278,7 @@
|
||||
}
|
||||
|
||||
// Initial load
|
||||
$effect(() => {
|
||||
onMount(() => {
|
||||
fetchUsers();
|
||||
});
|
||||
</script>
|
||||
@@ -284,9 +297,18 @@
|
||||
{#if loading}
|
||||
<Spinner class="size-5" />
|
||||
{/if}
|
||||
{#if currentUser.role === "admin"}
|
||||
<Button onclick={() => (showAddUserDialog = true)}>
|
||||
<PlusIcon class="mr-2 h-4 w-4" />
|
||||
{#if currentUser.role === "admin" || currentUser.role === "editor"}
|
||||
{#if !canSendEmail}
|
||||
<p class="text-muted-foreground max-w-xs text-xs">
|
||||
Email service not configured. Cannot invite new users. Please go to
|
||||
<a href={`${GC.DOCS_URL}/setup/email-setup`} target="_blank" class="text-blue-500 underline">
|
||||
setup email
|
||||
</a>
|
||||
for more info.
|
||||
</p>
|
||||
{/if}
|
||||
<Button onclick={() => (showAddUserDialog = true)} disabled={!canSendEmail}>
|
||||
<PlusIcon class="h-4 w-4" />
|
||||
Add User
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -321,7 +343,7 @@
|
||||
<Table.Cell colspan={6} class="text-muted-foreground py-8 text-center">No users found.</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each users as user}
|
||||
{#each users as user (user.id)}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">{user.name}</Table.Cell>
|
||||
<Table.Cell>{user.email}</Table.Cell>
|
||||
@@ -349,6 +371,18 @@
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => openSettingsSheet(user)}>
|
||||
<SettingsIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
{:else if currentUser.id === user.id && !!!currentUser.is_verified}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={sendingSelfVerification}
|
||||
onclick={() => sendVerificationEmail(user.id)}
|
||||
>
|
||||
{#if sendingSelfVerification}
|
||||
<Spinner class="mr-2 size-4" />
|
||||
{/if}
|
||||
Verify Email
|
||||
</Button>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
@@ -370,7 +404,7 @@
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
</Button>
|
||||
<div class="flex items-center gap-1">
|
||||
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as pageNum}
|
||||
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as pageNum (pageNum)}
|
||||
{#if pageNum === 1 || pageNum === totalPages || (pageNum >= page - 1 && pageNum <= page + 1)}
|
||||
<Button variant={pageNum === page ? "default" : "ghost"} size="sm" onclick={() => goToPage(pageNum)}>
|
||||
{pageNum}
|
||||
@@ -467,15 +501,20 @@
|
||||
</p>
|
||||
</div>
|
||||
<!-- Resend Invitation -->
|
||||
{#if !toEditUser.has_password && canSendEmail}
|
||||
{#if !toEditUser.has_password}
|
||||
<Card.Root>
|
||||
<Card.Content class="p-4">
|
||||
<Card.Content class="">
|
||||
<p class="mb-3 text-sm">
|
||||
This user ha2sn't set their password yet. Resend the invitation email to {toEditUser.email}.
|
||||
This user hasn't set their password yet. Resend the invitation email to {toEditUser.email}.
|
||||
</p>
|
||||
{#if !canSendEmail}
|
||||
<Alert.Root variant="destructive" class="mb-4">
|
||||
<Alert.Description>Email service not configured. Cannot resend invitation email.</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={toEditUser.actions.resendingInvitation}
|
||||
disabled={toEditUser.actions.resendingInvitation || !canSendEmail}
|
||||
onclick={async () => {
|
||||
toEditUser!.actions.resendingInvitation = true;
|
||||
manualSuccess = "";
|
||||
|
||||
Reference in New Issue
Block a user