web-admin: Add auth_user screen

This commit is contained in:
nemunaire 2026-01-21 21:08:51 +08:00
commit 42c36db56b
9 changed files with 1043 additions and 0 deletions

View file

@ -109,6 +109,7 @@ func DeclareRoutes(cfg *happydns.Options, router *gin.Engine) {
router.GET("/manifest.json", serveOrReverse("", cfg))
// Routes to virtual content
router.GET("/auth_users/*_", serveOrReverse("/", cfg))
router.GET("/domains/*_", serveOrReverse("/", cfg))
router.GET("/providers/*_", serveOrReverse("/", cfg))
router.GET("/users/*_", serveOrReverse("/", cfg))

View file

@ -86,6 +86,9 @@
<NavItem>
<NavLink href="/" active={page && page.url.pathname == '/'}>Dashboard</NavLink>
</NavItem>
<NavItem>
<NavLink href="/auth_users" active={page && page.url.pathname.startsWith('/auth_users')}>Auth</NavLink>
</NavItem>
<NavItem>
<NavLink href="/users" active={page && page.url.pathname.startsWith('/users')}>Users</NavLink>
</NavItem>

View file

@ -0,0 +1,139 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {
Button,
Card,
Col,
Container,
Icon,
Input,
InputGroup,
InputGroupText,
Table,
Row,
} from "@sveltestrap/sveltestrap";
import { getAuth, deleteAuthByUid } from '$lib/api-admin';
import { toasts } from '$lib/stores/toasts';
let authUsersQ = $state(getAuth());
let searchQuery = $state('');
</script>
<Container class="flex-fill my-5">
<Row class="mb-4">
<Col md={8}>
<h1 class="display-5">
<Icon name="shield-lock-fill"></Icon>
Auth User Management
</h1>
<p class="d-flex gap-3 align-items-center text-muted">
<span class="lead">
Manage all authentication accounts
</span>
{#await authUsersQ then authUsersR}
<span>Total: {authUsersR.data?.length ?? 0} auth users</span>
{/await}
</p>
</Col>
<Col md={4} class="text-end">
<Button color="primary" href="/auth_users/new">
<Icon name="plus-circle"></Icon>
Create Auth User
</Button>
</Col>
</Row>
<Row class="mb-4">
<Col md={8} lg={6}>
<InputGroup>
<InputGroupText>
<Icon name="search"></Icon>
</InputGroupText>
<Input
type="text"
placeholder="Search auth users..."
bind:value={searchQuery}
/>
</InputGroup>
</Col>
</Row>
{#await authUsersQ}
Please wait...
{:then authUsersR}
{@const authUsers = authUsersR.data}
<div class="table-responsive">
<Table hover bordered>
<thead>
<tr>
<th>ID</th>
<th>Email</th>
<th>Created</th>
<th>Last login</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each (authUsers ?? []).filter(i => (i.id?.toLowerCase() ?? '').indexOf(searchQuery.toLowerCase()) > -1 || (i.email?.toLowerCase() ?? '').indexOf(searchQuery.toLowerCase()) > -1) as authUser}
<tr>
<td>{authUser.id}</td>
<td>{authUser.email}</td>
<td>{authUser.createdAt?.replace(/\.[0-9]+/, "") ?? ''}</td>
<td>{authUser.lastLoggedIn?.replace(/\.[0-9]+/, "")}</td>
<td class="d-flex flex-nowrap gap-1">
<Button color="primary" outline size="sm" href="/auth_users/{authUser.id}">
<Icon name="pencil"></Icon>
</Button>
<Button color="primary" outline size="sm" onclick={async () => {
if (confirm(`Are you sure you want to delete auth user "${authUser.email}"?`)) {
try {
await deleteAuthByUid({ path: { uid: authUser.id ?? '' } });
// Refresh the auth users list
authUsersQ = getAuth();
toasts.addToast({
message: `Auth user "${authUser.email}" has been deleted successfully`,
type: 'success',
timeout: 5000,
});
} catch (error) {
toasts.addErrorToast({
message: 'Failed to delete auth user: ' + error,
timeout: 10000,
});
}
}
}}>
<Icon name="trash"></Icon>
</Button>
</td>
</tr>
{/each}
</tbody>
</Table>
</div>
{/await}
</Container>

View file

@ -0,0 +1,114 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { page } from '$app/stores';
import {
Alert,
Button,
Col,
Container,
Icon,
Row,
Spinner,
} from "@sveltestrap/sveltestrap";
import { getAuthByUid } from '$lib/api-admin';
import UserInfoCard from './UserInfoCard.svelte';
import EmailVerificationCard from './EmailVerificationCard.svelte';
import AccountRecoveryCard from './AccountRecoveryCard.svelte';
import PasswordResetCard from './PasswordResetCard.svelte';
const uid = $page.params.uid as string;
let authUserQ = $state(getAuthByUid({ path: { uid } }));
function refreshAuthUser() {
authUserQ = getAuthByUid({ path: { uid } });
}
</script>
<Container class="flex-fill my-5">
<Row class="mb-4">
<Col>
<h1 class="display-5">
<Icon name="pencil"></Icon>
Edit Auth User
</h1>
</Col>
</Row>
{#await authUserQ}
<div class="text-center my-5">
<Spinner color="primary" />
<p class="mt-3">Loading auth user...</p>
</div>
{:then authUserR}
{@const authUser = authUserR.data}
{#if authUser}
<Row>
<Col lg={6}>
<UserInfoCard
{authUser}
/>
</Col>
<Col lg={6}>
<EmailVerificationCard
{authUser}
{uid}
onRefresh={refreshAuthUser}
/>
<AccountRecoveryCard
{uid}
/>
<PasswordResetCard
{uid}
/>
</Col>
</Row>
{:else}
<Alert color="warning">
<h4 class="alert-heading">Auth user not found</h4>
<p>The requested auth user could not be loaded.</p>
<hr />
<Button type="button" color="secondary" outline href="/auth_users">
<Icon name="arrow-left"></Icon>
Back to Auth Users
</Button>
</Alert>
{/if}
{:catch error}
<Alert color="danger">
<h4 class="alert-heading">Error loading auth user</h4>
<p>{error}</p>
<hr />
<Button type="button" color="secondary" outline href="/auth_users">
<Icon name="arrow-left"></Icon>
Back to Auth Users
</Button>
</Alert>
{/await}
</Container>

View file

@ -0,0 +1,128 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {
Button,
Card,
CardBody,
CardHeader,
Icon,
Spinner,
} from "@sveltestrap/sveltestrap";
import {
postAuthByUidRecoverLink,
postAuthByUidSendRecoverEmail,
} from '$lib/api-admin';
import { toasts } from '$lib/stores/toasts';
interface Props {
uid: string;
}
let {
uid,
}: Props = $props();
let actionLoading = $state('');
async function handleGenerateRecoveryLink() {
actionLoading = 'recovery_link';
try {
const response = await postAuthByUidRecoverLink({ path: { uid } });
if (response.data) {
await navigator.clipboard.writeText(response.data);
}
toasts.addToast({
message: 'Recovery link generated and copied to clipboard',
type: 'success',
timeout: 5000,
});
} catch (error) {
toasts.addErrorToast({
message: 'Failed to generate recovery link: ' + error,
timeout: 10000,
});
} finally {
actionLoading = '';
}
}
async function handleSendRecoveryEmail() {
actionLoading = 'send_recovery';
try {
await postAuthByUidSendRecoverEmail({ path: { uid } });
toasts.addToast({
message: 'Recovery email sent successfully',
type: 'success',
timeout: 5000,
});
} catch (error) {
toasts.addErrorToast({
message: 'Failed to send recovery email: ' + error,
timeout: 10000,
});
} finally {
actionLoading = '';
}
}
</script>
<Card class="mb-4">
<CardHeader>
<h5 class="mb-0">
<Icon name="key"></Icon>
Account Recovery
</h5>
</CardHeader>
<CardBody class="d-flex flex-column gap-2">
<Button
color="warning"
outline
onclick={handleGenerateRecoveryLink}
disabled={actionLoading !== ''}
>
{#if actionLoading === 'recovery_link'}
<Spinner size="sm" class="me-2" />
{:else}
<Icon name="link-45deg" class="me-2"></Icon>
{/if}
Generate Recovery Link
</Button>
<Button
color="warning"
outline
onclick={handleSendRecoveryEmail}
disabled={actionLoading !== ''}
>
{#if actionLoading === 'send_recovery'}
<Spinner size="sm" class="me-2" />
{:else}
<Icon name="envelope" class="me-2"></Icon>
{/if}
Send Recovery Email
</Button>
</CardBody>
</Card>

View file

@ -0,0 +1,221 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {
Badge,
Button,
Card,
CardBody,
CardHeader,
Icon,
Spinner,
} from "@sveltestrap/sveltestrap";
import type { HappydnsUserAuth } from '$lib/api-admin';
import {
putAuthByUid,
postAuthByUidValidationLink,
postAuthByUidSendValidationEmail,
postAuthByUidValidateEmail,
} from '$lib/api-admin';
import { toasts } from '$lib/stores/toasts';
interface Props {
authUser: HappydnsUserAuth;
uid: string;
onRefresh: () => void;
}
let {
authUser,
uid,
onRefresh,
}: Props = $props();
let actionLoading = $state('');
async function handleGenerateValidationLink() {
actionLoading = 'validation_link';
try {
const response = await postAuthByUidValidationLink({ path: { uid } });
if (response.data) {
await navigator.clipboard.writeText(response.data);
}
toasts.addToast({
message: 'Validation link generated and copied to clipboard',
type: 'success',
timeout: 5000,
});
} catch (error) {
toasts.addErrorToast({
message: 'Failed to generate validation link: ' + error,
timeout: 10000,
});
} finally {
actionLoading = '';
}
}
async function handleSendValidationEmail() {
actionLoading = 'send_validation';
try {
await postAuthByUidSendValidationEmail({ path: { uid } });
toasts.addToast({
message: 'Validation email sent successfully',
type: 'success',
timeout: 5000,
});
} catch (error) {
toasts.addErrorToast({
message: 'Failed to send validation email: ' + error,
timeout: 10000,
});
} finally {
actionLoading = '';
}
}
async function handleValidateEmail() {
if (!confirm('Are you sure you want to mark this email as verified?')) return;
actionLoading = 'validate_email';
try {
await postAuthByUidValidateEmail({ path: { uid } });
onRefresh();
toasts.addToast({
message: 'Email marked as verified successfully',
type: 'success',
timeout: 5000,
});
} catch (error) {
toasts.addErrorToast({
message: 'Failed to validate email: ' + error,
timeout: 10000,
});
} finally {
actionLoading = '';
}
}
async function handleUnverifyEmail() {
if (!confirm('Are you sure you want to mark this email as unverified?')) return;
actionLoading = 'validate_email';
try {
await putAuthByUid({
path: { uid },
body: {
email: authUser.email,
emailVerification: undefined
}
});
onRefresh();
toasts.addToast({
message: 'Email marked as unverified successfully',
type: 'success',
timeout: 5000,
});
} catch (error) {
toasts.addErrorToast({
message: 'Failed to unverify email: ' + error,
timeout: 10000,
});
} finally {
actionLoading = '';
}
}
</script>
<Card class="mb-4">
<CardHeader>
<h5 class="mb-0 d-flex align-items-center gap-2">
<Icon name={authUser.emailVerification === null ? "envelope-exclamation" : "envelope-check"}></Icon>
Email Verification
{#if authUser.emailVerification !== null}
<Badge color="success">Verified</Badge>
{:else}
<Badge color="danger">Not Verified</Badge>
{/if}
</h5>
</CardHeader>
<CardBody class="d-flex flex-column gap-2">
{#if authUser.emailVerification !== null}
<Button
color="warning"
outline
onclick={handleUnverifyEmail}
disabled={actionLoading !== ''}
>
{#if actionLoading === 'validate_email'}
<Spinner size="sm" class="me-2" />
{:else}
<Icon name="x-circle" class="me-2"></Icon>
{/if}
Mark Email as Unverified
</Button>
{:else}
<Button
color="success"
outline
onclick={handleValidateEmail}
disabled={actionLoading !== ''}
>
{#if actionLoading === 'validate_email'}
<Spinner size="sm" class="me-2" />
{:else}
<Icon name="check-circle" class="me-2"></Icon>
{/if}
Mark Email as Verified
</Button>
{/if}
<Button
color="primary"
outline
onclick={handleGenerateValidationLink}
disabled={actionLoading !== ''}
>
{#if actionLoading === 'validation_link'}
<Spinner size="sm" class="me-2" />
{:else}
<Icon name="link-45deg" class="me-2"></Icon>
{/if}
Generate Validation Link
</Button>
<Button
color="primary"
outline
onclick={handleSendValidationEmail}
disabled={actionLoading !== ''}
>
{#if actionLoading === 'send_validation'}
<Spinner size="sm" class="me-2" />
{:else}
<Icon name="envelope" class="me-2"></Icon>
{/if}
Send Validation Email
</Button>
</CardBody>
</Card>

View file

@ -0,0 +1,96 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {
Button,
Card,
CardBody,
CardHeader,
Icon,
Spinner,
} from "@sveltestrap/sveltestrap";
import { postAuthByUidResetPassword } from '$lib/api-admin';
import { toasts } from '$lib/stores/toasts';
interface Props {
uid: string;
}
let {
uid,
}: Props = $props();
let actionLoading = $state(false);
async function handleResetPassword() {
if (!confirm('Are you sure you want to reset this user\'s password? A random password will be generated.')) return;
actionLoading = true;
try {
const response = await postAuthByUidResetPassword({
path: { uid },
body: { password: '' }
});
const password = response.data?.password || '';
await navigator.clipboard.writeText(password);
toasts.addToast({
message: 'Password reset successfully and copied to clipboard',
type: 'success',
timeout: 5000,
});
} catch (error) {
toasts.addErrorToast({
message: 'Failed to reset password: ' + error,
timeout: 10000,
});
} finally {
actionLoading = false;
}
}
</script>
<Card class="mb-4">
<CardHeader>
<h5 class="mb-0">
<Icon name="shield-lock"></Icon>
Password Reset
</h5>
</CardHeader>
<CardBody class="d-flex flex-column gap-2">
<Button
color="danger"
outline
onclick={handleResetPassword}
disabled={actionLoading}
>
{#if actionLoading}
<Spinner size="sm" class="me-2" />
{:else}
<Icon name="arrow-clockwise" class="me-2"></Icon>
{/if}
Reset Password (Generate Random)
</Button>
</CardBody>
</Card>

View file

@ -0,0 +1,204 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { goto } from '$app/navigation';
import {
Alert,
Button,
Card,
CardBody,
CardHeader,
Form,
FormGroup,
Icon,
Input,
Label,
Spinner,
} from "@sveltestrap/sveltestrap";
import type { HappydnsUserAuth } from '$lib/api-admin';
import { putAuthByUid } from '$lib/api-admin';
import { toasts } from '$lib/stores/toasts';
import { toDatetimeLocal, fromDatetimeLocal } from '$lib/utils';
interface Props {
authUser: HappydnsUserAuth;
}
let {
authUser,
}: Props = $props();
let email = $state('');
let createdAt = $state('');
let lastLoggedIn = $state('');
let emailVerification = $state('');
let allowCommercials = $state(false);
let loading = $state(false);
let errorMessage = $state('');
// Load auth user data
$effect(() => {
email = authUser.email || '';
createdAt = toDatetimeLocal(authUser.createdAt);
lastLoggedIn = toDatetimeLocal(authUser.lastLoggedIn);
emailVerification = toDatetimeLocal(authUser.emailVerification);
allowCommercials = authUser.allowCommercials || false;
});
async function handleSubmit(e: SubmitEvent) {
e.preventDefault();
loading = true;
errorMessage = '';
try {
const body: any = {
email: email,
createdAt: fromDatetimeLocal(createdAt),
allowCommercials: allowCommercials,
};
// Only include optional fields if they have values
if (lastLoggedIn) {
body.lastLoggedIn = fromDatetimeLocal(lastLoggedIn);
} else {
body.lastLoggedIn = null;
}
if (emailVerification) {
body.emailVerification = fromDatetimeLocal(emailVerification);
} else {
body.emailVerification = null;
}
await putAuthByUid({
path: { uid: authUser.id ?? '' },
body: body
});
toasts.addToast({
message: `Auth user "${email}" has been updated successfully`,
type: 'success',
timeout: 5000,
});
goto('/auth_users');
} catch (error) {
errorMessage = 'Failed to update auth user: ' + error;
toasts.addErrorToast({
message: errorMessage,
timeout: 10000,
});
} finally {
loading = false;
}
}
</script>
<Card class="mb-4">
<CardHeader>
<h5 class="mb-0">Auth User Information</h5>
</CardHeader>
<CardBody>
{#if errorMessage}
<Alert color="danger" dismissible fade>
{errorMessage}
</Alert>
{/if}
<Form on:submit={handleSubmit}>
<FormGroup>
<Label for="userId">User ID</Label>
<Input
type="text"
id="userId"
value={authUser.id}
disabled
readonly
/>
</FormGroup>
<FormGroup>
<Label for="email">Email</Label>
<Input
type="email"
id="email"
bind:value={email}
required
placeholder="user@example.com"
/>
</FormGroup>
<FormGroup>
<Label for="createdAt">Created At</Label>
<Input
type="datetime-local"
id="createdAt"
bind:value={createdAt}
/>
</FormGroup>
<FormGroup>
<Label for="lastLoggedIn">Last Logged In</Label>
<Input
type="datetime-local"
id="lastLoggedIn"
bind:value={lastLoggedIn}
/>
</FormGroup>
<FormGroup>
<Label for="emailVerification">Email Verification</Label>
<Input
type="datetime-local"
id="emailVerification"
bind:value={emailVerification}
placeholder="Not verified"
/>
</FormGroup>
<Input
type="checkbox"
id="allowCommercials"
bind:checked={allowCommercials}
label="Allow Commercial Communications"
/>
<div class="d-flex gap-2 mt-4">
<Button color="primary" type="submit" disabled={loading}>
{#if loading}
<Spinner size="sm" class="me-2" />
{:else}
<Icon name="check-circle" class="me-2"></Icon>
{/if}
Save Changes
</Button>
<Button type="button" color="secondary" outline href="/auth_users" disabled={loading}>
Cancel
</Button>
</div>
</Form>
</CardBody>
</Card>

View file

@ -0,0 +1,137 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { goto } from '$app/navigation';
import {
Alert,
Button,
Card,
CardBody,
CardHeader,
Col,
Container,
Form,
FormGroup,
Icon,
Input,
Label,
Row,
Spinner,
} from "@sveltestrap/sveltestrap";
import { postAuth } from '$lib/api-admin';
import { toasts } from '$lib/stores/toasts';
let email = $state('');
let loading = $state(false);
let errorMessage = $state('');
async function handleSubmit() {
loading = true;
errorMessage = '';
try {
const response = await postAuth({
body: {
email: email,
}
});
toasts.addToast({
message: `Auth user "${email}" has been created successfully`,
type: 'success',
timeout: 5000,
});
goto('/auth_users');
} catch (error) {
errorMessage = 'Failed to create auth user: ' + error;
toasts.addErrorToast({
message: errorMessage,
timeout: 10000,
});
} finally {
loading = false;
}
}
</script>
<Container class="flex-fill my-5">
<Row class="mb-4">
<Col>
<h1 class="display-5">
<Icon name="plus-circle"></Icon>
Create New Auth User
</h1>
</Col>
</Row>
<Row>
<Col md={8} lg={6}>
<Card>
<CardHeader>
<h5 class="mb-0">Auth User Information</h5>
</CardHeader>
<CardBody>
{#if errorMessage}
<Alert color="danger" dismissible fade>
{errorMessage}
</Alert>
{/if}
<Form on:submit={handleSubmit}>
<FormGroup>
<Label for="email">Email *</Label>
<Input
type="email"
id="email"
bind:value={email}
required
placeholder="user@example.com"
autofocus
/>
<small class="form-text text-muted">
The email address will be used as the authentication account's login.
</small>
</FormGroup>
<div class="d-flex gap-2 mt-4">
<Button color="primary" type="submit" disabled={loading}>
{#if loading}
<Spinner size="sm" class="me-2" />
{:else}
<Icon name="plus-circle" class="me-2"></Icon>
{/if}
Create Auth User
</Button>
<Button type="button" color="secondary" outline href="/auth_users" disabled={loading}>
Cancel
</Button>
</div>
</Form>
</CardBody>
</Card>
</Col>
</Row>
</Container>