add "not activated" warning indicators
This commit is contained in:
parent
c22ab15895
commit
92edec869f
7 changed files with 241 additions and 16 deletions
|
|
@ -3,11 +3,13 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"jupiter-mail.org/web/model"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
|
|
@ -15,6 +17,7 @@ type Account struct {
|
|||
Domain string `json:"domain"`
|
||||
DisplayName string `json:"display_name"`
|
||||
MailDirectory string `json:"mail_directory"`
|
||||
Activated bool `json:"activated"`
|
||||
}
|
||||
|
||||
var mockAccounts = []*Account{
|
||||
|
|
@ -23,18 +26,21 @@ var mockAccounts = []*Account{
|
|||
Username: "ari",
|
||||
Domain: "example.org",
|
||||
MailDirectory: `/var/mail/example.org/ari/home`,
|
||||
Activated: true,
|
||||
},
|
||||
{
|
||||
DisplayName: "Test Account",
|
||||
Username: "testaccount",
|
||||
Domain: "mydomain.ie",
|
||||
MailDirectory: `/var/mail/mydomain.ie/testaccount/home`,
|
||||
Activated: false,
|
||||
},
|
||||
{
|
||||
DisplayName: "Cooler One",
|
||||
Username: "otherone",
|
||||
Domain: "cooler.space",
|
||||
MailDirectory: `/var/mail/cooler.space/otherone/home`,
|
||||
Activated: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +49,7 @@ func main() {
|
|||
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowAllOrigins: true,
|
||||
AllowHeaders: []string{"Content-Type"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||
MaxAge: 12 * time.Hour,
|
||||
}))
|
||||
|
|
@ -78,6 +85,67 @@ func main() {
|
|||
ctx.JSON(http.StatusOK, accounts)
|
||||
})
|
||||
|
||||
r.POST("/api/v1/accounts", func(ctx *gin.Context) {
|
||||
type Request struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
Username string `json:"username"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
var newAccountData Request
|
||||
|
||||
if err := ctx.BindJSON(&newAccountData); err != nil {
|
||||
ctx.String(http.StatusBadRequest, "Malformed request data")
|
||||
return
|
||||
}
|
||||
|
||||
emailAddressReq := fmt.Sprintf("%s@%s", newAccountData.Username, newAccountData.Domain)
|
||||
|
||||
emailAddress, err := mail.ParseAddress(emailAddressReq)
|
||||
if err != nil {
|
||||
ctx.String(http.StatusBadRequest, "Malformed address: %s", emailAddressReq)
|
||||
return
|
||||
}
|
||||
|
||||
search := func(account *Account) bool {
|
||||
return (
|
||||
account.Username == newAccountData.Username &&
|
||||
account.Domain == newAccountData.Domain)
|
||||
}
|
||||
if slices.ContainsFunc(mockAccounts, search) {
|
||||
ctx.String(http.StatusBadRequest, "Account already exists: %s", emailAddress.String())
|
||||
return
|
||||
}
|
||||
|
||||
account := model.MailAccount{
|
||||
DisplayName: newAccountData.DisplayName,
|
||||
Username: newAccountData.Username,
|
||||
Domain: newAccountData.Domain,
|
||||
}
|
||||
account.MailDirectory = account.DefaultMailHome()
|
||||
|
||||
mockAccounts = append(mockAccounts, &Account{
|
||||
DisplayName: account.DisplayName,
|
||||
Username: account.Username,
|
||||
Domain: account.Domain,
|
||||
MailDirectory: account.MailDirectory,
|
||||
})
|
||||
|
||||
/*
|
||||
account, err := controller.FetchAccountByEmail(email)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
ctx.Error(fmt.Errorf("Account does not exist: %s", email))
|
||||
return
|
||||
}
|
||||
log.Fatalf("Failed to fetch account: %v", err)
|
||||
ctx.Error(errors.New("Failed to fetch account"))
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
ctx.JSON(http.StatusOK, account)
|
||||
})
|
||||
|
||||
r.GET("/api/v1/accounts/:email", func(ctx *gin.Context) {
|
||||
email := ctx.Params.ByName("email")
|
||||
|
||||
|
|
@ -142,10 +210,10 @@ func main() {
|
|||
|
||||
ctx.JSON(http.StatusOK, Settings{
|
||||
MailDelivery: MailDelivery{
|
||||
Version: "v1.2.3",
|
||||
Version: "Dovecot v1.2.3",
|
||||
},
|
||||
MailTransfer: MailTransfer{
|
||||
Version: "v4.5.6",
|
||||
Version: "Postfix v4.5.6",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
--info: #1a94ff;
|
||||
--success: #92a40a;
|
||||
--warning: #ffc107;
|
||||
--warning: #e7ad00;
|
||||
--error: #d42c2c;
|
||||
}
|
||||
|
||||
|
|
@ -125,3 +125,12 @@ code {
|
|||
|
||||
transition: background-color .1s, border-color .1s;
|
||||
}
|
||||
|
||||
p.warning {
|
||||
width: fit-content;
|
||||
padding: .2em .4em;
|
||||
color: var(--on-bg-1);
|
||||
background-color: var(--warning);
|
||||
border: 1px solid color-mix(in srgb, var(--warning), #000 25%);
|
||||
border-radius: var(--radius-0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { type Account } from '@jupiter/lib/account';
|
||||
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
||||
import { Copy, User } from '@lucide/svelte';
|
||||
import { CircleAlert, Copy, User } from '@lucide/svelte';
|
||||
|
||||
let {
|
||||
account,
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="account"
|
||||
class={["account", { warning: !account.activated }]}
|
||||
role="button"
|
||||
data-email="{email}"
|
||||
tabindex="0"
|
||||
|
|
@ -41,7 +41,12 @@
|
|||
<div class="icon-container"><User /></div>
|
||||
<hr>
|
||||
<div class="info-container">
|
||||
<h2 class="name">{account.display_name}</h2>
|
||||
<h2 class="name">
|
||||
{#if !account.activated}
|
||||
<span class="warning-badge"><CircleAlert /></span>
|
||||
{/if}
|
||||
{account.display_name}
|
||||
</h2>
|
||||
<button class="email" onclick={(event) => {
|
||||
event.stopPropagation();
|
||||
copyEmail();
|
||||
|
|
@ -53,6 +58,7 @@
|
|||
|
||||
<style>
|
||||
.account {
|
||||
--shadow: 0 var(--sp-ssm) var(--sp-sm) #0001;
|
||||
width: 480px;
|
||||
height: var(--sp-xxxl);
|
||||
padding: .5em;
|
||||
|
|
@ -64,13 +70,21 @@
|
|||
border: 1px solid var(--bg-2);
|
||||
border-radius: var(--radius-0);
|
||||
|
||||
box-shadow: 0 var(--sp-ssm) var(--sp-sm) #0001;
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
transition: background-color .1s, border-color .1s;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.account.warning {
|
||||
box-shadow: var(--shadow), calc(0px - var(--sp-sm)) 0 0 var(--warning);
|
||||
}
|
||||
|
||||
.account .warning-badge {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.account:hover {
|
||||
background-color: var(--bg-1);
|
||||
border-color: var(--bg-3);
|
||||
|
|
@ -116,10 +130,18 @@
|
|||
|
||||
font-size: 1.2em;
|
||||
line-height: 1.6em;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .2em;
|
||||
}
|
||||
|
||||
.info-container .name :global(svg) {
|
||||
height: 1em;
|
||||
transform: translateY(.1em);
|
||||
}
|
||||
|
||||
.info-container .email {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export interface Account {
|
|||
username: string,
|
||||
domain: string,
|
||||
mail_directory: string,
|
||||
activated: boolean,
|
||||
}
|
||||
|
||||
export let accounts = writable<Account[] | null>(null);
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
import Header from '@jupiter/components/ui/Header.svelte';
|
||||
import AccountListItem from '@jupiter/components/ui/AccountListItem.svelte';
|
||||
import AccountAddButton from '@jupiter/components/ui/AccountAddButton.svelte';
|
||||
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let { data } = $props();
|
||||
const accounts = (() => data.accounts)();
|
||||
|
||||
function addAccount() {
|
||||
pushToast("TODO: account creation", ToastType.INFO);
|
||||
goto("/accounts/create");
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Copy, KeyRound, Mail, Trash, User } from '@lucide/svelte';
|
||||
import { CircleAlert, Copy, KeyRound, Mail, Trash, User } from '@lucide/svelte';
|
||||
import Header from '@jupiter/components/ui/Header.svelte';
|
||||
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
||||
import Button from '@jupiter/components/ui/Button.svelte';
|
||||
|
|
@ -58,13 +58,22 @@
|
|||
</div>
|
||||
<hr>
|
||||
<h2 class="account-title">
|
||||
<span>
|
||||
{#if !account.activated}
|
||||
<span class="warning-badge"><CircleAlert /></span>
|
||||
{/if}
|
||||
{account.display_name}
|
||||
</span>
|
||||
<button class="email" onclick={() => { copyEmail() }}>{email} <Copy /></button>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{#if !account.activated}
|
||||
<p class="warning">* This account is not activated yet.</p>
|
||||
{/if}
|
||||
|
||||
<Label>Actions</Label>
|
||||
<div class="account-actions">
|
||||
<Button href="mailto:{email}"><Mail /> Send Email</Button>
|
||||
|
|
@ -103,6 +112,10 @@
|
|||
gap: .2em;
|
||||
}
|
||||
|
||||
.account-title-container .warning-badge {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.account-title-icon {
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
|
|
@ -133,6 +146,12 @@
|
|||
gap: .2em;
|
||||
}
|
||||
|
||||
.account-title span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .25em;
|
||||
}
|
||||
|
||||
.account-title .email {
|
||||
width: fit-content;
|
||||
height: 1.5em;
|
||||
|
|
|
|||
106
src/web/frontend/src/routes/accounts/create/+page.svelte
Normal file
106
src/web/frontend/src/routes/accounts/create/+page.svelte
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<script lang="ts">
|
||||
import { UserRoundPlus } from '@lucide/svelte';
|
||||
import Header from '@jupiter/components/ui/Header.svelte';
|
||||
import * as api from '@jupiter/lib/api';
|
||||
import Label from '@jupiter/components/ui/Label.svelte';
|
||||
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
||||
import Button from '@jupiter/components/ui/Button.svelte';
|
||||
import { type Account } from '@jupiter/lib/account';
|
||||
import AccountListItem from '@jupiter/components/ui/AccountListItem.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let domainInput: HTMLInputElement;
|
||||
|
||||
let newAccount = $state<Account>({
|
||||
display_name: "",
|
||||
username: "",
|
||||
domain: "",
|
||||
mail_directory: "",
|
||||
activated: true,
|
||||
});
|
||||
|
||||
async function createAccount() {
|
||||
const data = {
|
||||
display_name: newAccount.display_name,
|
||||
username: newAccount.username,
|
||||
domain: newAccount.domain,
|
||||
};
|
||||
|
||||
const res = await fetch(api.BASE_URL + "/api/v1/accounts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
const err = new Error("Failed to create account: " + text);
|
||||
pushToast(err.message, ToastType.ERROR);
|
||||
throw err;
|
||||
}
|
||||
|
||||
pushToast("Account created successfully.", ToastType.SUCCESS);
|
||||
goto("/accounts");
|
||||
}
|
||||
|
||||
// TODO: domain should be a drop-down determined by domain config
|
||||
</script>
|
||||
|
||||
<div class="page-container">
|
||||
<Header>
|
||||
<UserRoundPlus />
|
||||
<h1>Create Account</h1>
|
||||
</Header>
|
||||
|
||||
<main>
|
||||
<div class="form">
|
||||
<Label>Display Name</Label>
|
||||
<input type="text" placeholder="Example User" bind:value={newAccount.display_name}>
|
||||
|
||||
<Label>Username</Label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="user"
|
||||
bind:value={newAccount.username}
|
||||
onkeydown={(event: KeyboardEvent) => {
|
||||
if (event.key === "@") {
|
||||
event.preventDefault();
|
||||
domainInput.focus();
|
||||
}
|
||||
}}>
|
||||
|
||||
<Label>Domain</Label>
|
||||
<input type="text" placeholder="example.org" bind:value={newAccount.domain} bind:this={domainInput}>
|
||||
|
||||
<AccountListItem account={newAccount} />
|
||||
|
||||
<Button onclick={() => {createAccount()}}>Create Account</Button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-container {
|
||||
min-height: 100%;
|
||||
|
||||
background-color: var(--bg-0);
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
margin-bottom: 1em;
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
padding: .2em .3em;
|
||||
border: 1px solid var(--bg-2);
|
||||
border-radius: var(--radius-0);
|
||||
}
|
||||
|
||||
.form :global(.account) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue