web-admin: Add auth_user screen
This commit is contained in:
parent
a231731610
commit
42c36db56b
9 changed files with 1043 additions and 0 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
139
web-admin/src/routes/auth_users/+page.svelte
Normal file
139
web-admin/src/routes/auth_users/+page.svelte
Normal 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>
|
||||
114
web-admin/src/routes/auth_users/[uid]/+page.svelte
Normal file
114
web-admin/src/routes/auth_users/[uid]/+page.svelte
Normal 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>
|
||||
128
web-admin/src/routes/auth_users/[uid]/AccountRecoveryCard.svelte
Normal file
128
web-admin/src/routes/auth_users/[uid]/AccountRecoveryCard.svelte
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
204
web-admin/src/routes/auth_users/[uid]/UserInfoCard.svelte
Normal file
204
web-admin/src/routes/auth_users/[uid]/UserInfoCard.svelte
Normal 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>
|
||||
137
web-admin/src/routes/auth_users/new/+page.svelte
Normal file
137
web-admin/src/routes/auth_users/new/+page.svelte
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue