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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"jupiter-mail.org/web/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
|
|
@ -15,6 +17,7 @@ type Account struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
MailDirectory string `json:"mail_directory"`
|
MailDirectory string `json:"mail_directory"`
|
||||||
|
Activated bool `json:"activated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var mockAccounts = []*Account{
|
var mockAccounts = []*Account{
|
||||||
|
|
@ -23,18 +26,21 @@ var mockAccounts = []*Account{
|
||||||
Username: "ari",
|
Username: "ari",
|
||||||
Domain: "example.org",
|
Domain: "example.org",
|
||||||
MailDirectory: `/var/mail/example.org/ari/home`,
|
MailDirectory: `/var/mail/example.org/ari/home`,
|
||||||
|
Activated: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
DisplayName: "Test Account",
|
DisplayName: "Test Account",
|
||||||
Username: "testaccount",
|
Username: "testaccount",
|
||||||
Domain: "mydomain.ie",
|
Domain: "mydomain.ie",
|
||||||
MailDirectory: `/var/mail/mydomain.ie/testaccount/home`,
|
MailDirectory: `/var/mail/mydomain.ie/testaccount/home`,
|
||||||
|
Activated: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
DisplayName: "Cooler One",
|
DisplayName: "Cooler One",
|
||||||
Username: "otherone",
|
Username: "otherone",
|
||||||
Domain: "cooler.space",
|
Domain: "cooler.space",
|
||||||
MailDirectory: `/var/mail/cooler.space/otherone/home`,
|
MailDirectory: `/var/mail/cooler.space/otherone/home`,
|
||||||
|
Activated: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +49,7 @@ func main() {
|
||||||
|
|
||||||
r.Use(cors.New(cors.Config{
|
r.Use(cors.New(cors.Config{
|
||||||
AllowAllOrigins: true,
|
AllowAllOrigins: true,
|
||||||
|
AllowHeaders: []string{"Content-Type"},
|
||||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||||
MaxAge: 12 * time.Hour,
|
MaxAge: 12 * time.Hour,
|
||||||
}))
|
}))
|
||||||
|
|
@ -78,6 +85,67 @@ func main() {
|
||||||
ctx.JSON(http.StatusOK, accounts)
|
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) {
|
r.GET("/api/v1/accounts/:email", func(ctx *gin.Context) {
|
||||||
email := ctx.Params.ByName("email")
|
email := ctx.Params.ByName("email")
|
||||||
|
|
||||||
|
|
@ -142,10 +210,10 @@ func main() {
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, Settings{
|
ctx.JSON(http.StatusOK, Settings{
|
||||||
MailDelivery: MailDelivery{
|
MailDelivery: MailDelivery{
|
||||||
Version: "v1.2.3",
|
Version: "Dovecot v1.2.3",
|
||||||
},
|
},
|
||||||
MailTransfer: MailTransfer{
|
MailTransfer: MailTransfer{
|
||||||
Version: "v4.5.6",
|
Version: "Postfix v4.5.6",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
--info: #1a94ff;
|
--info: #1a94ff;
|
||||||
--success: #92a40a;
|
--success: #92a40a;
|
||||||
--warning: #ffc107;
|
--warning: #e7ad00;
|
||||||
--error: #d42c2c;
|
--error: #d42c2c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,3 +125,12 @@ code {
|
||||||
|
|
||||||
transition: background-color .1s, border-color .1s;
|
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">
|
<script lang="ts">
|
||||||
import { type Account } from '@jupiter/lib/account';
|
import { type Account } from '@jupiter/lib/account';
|
||||||
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
||||||
import { Copy, User } from '@lucide/svelte';
|
import { CircleAlert, Copy, User } from '@lucide/svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
account,
|
account,
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="account"
|
class={["account", { warning: !account.activated }]}
|
||||||
role="button"
|
role="button"
|
||||||
data-email="{email}"
|
data-email="{email}"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
|
@ -41,7 +41,12 @@
|
||||||
<div class="icon-container"><User /></div>
|
<div class="icon-container"><User /></div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="info-container">
|
<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) => {
|
<button class="email" onclick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
copyEmail();
|
copyEmail();
|
||||||
|
|
@ -53,6 +58,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.account {
|
.account {
|
||||||
|
--shadow: 0 var(--sp-ssm) var(--sp-sm) #0001;
|
||||||
width: 480px;
|
width: 480px;
|
||||||
height: var(--sp-xxxl);
|
height: var(--sp-xxxl);
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
|
|
@ -64,13 +70,21 @@
|
||||||
border: 1px solid var(--bg-2);
|
border: 1px solid var(--bg-2);
|
||||||
border-radius: var(--radius-0);
|
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;
|
transition: background-color .1s, border-color .1s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
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 {
|
.account:hover {
|
||||||
background-color: var(--bg-1);
|
background-color: var(--bg-1);
|
||||||
border-color: var(--bg-3);
|
border-color: var(--bg-3);
|
||||||
|
|
@ -116,10 +130,18 @@
|
||||||
|
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-container .name :global(svg) {
|
||||||
|
height: 1em;
|
||||||
|
transform: translateY(.1em);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-container .email {
|
.info-container .email {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export interface Account {
|
||||||
username: string,
|
username: string,
|
||||||
domain: string,
|
domain: string,
|
||||||
mail_directory: string,
|
mail_directory: string,
|
||||||
|
activated: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export let accounts = writable<Account[] | null>(null);
|
export let accounts = writable<Account[] | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
import Header from '@jupiter/components/ui/Header.svelte';
|
import Header from '@jupiter/components/ui/Header.svelte';
|
||||||
import AccountListItem from '@jupiter/components/ui/AccountListItem.svelte';
|
import AccountListItem from '@jupiter/components/ui/AccountListItem.svelte';
|
||||||
import AccountAddButton from '@jupiter/components/ui/AccountAddButton.svelte';
|
import AccountAddButton from '@jupiter/components/ui/AccountAddButton.svelte';
|
||||||
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
const accounts = (() => data.accounts)();
|
const accounts = (() => data.accounts)();
|
||||||
|
|
||||||
function addAccount() {
|
function addAccount() {
|
||||||
pushToast("TODO: account creation", ToastType.INFO);
|
goto("/accounts/create");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<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 Header from '@jupiter/components/ui/Header.svelte';
|
||||||
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
import { pushToast, ToastType } from '@jupiter/lib/toasts';
|
||||||
import Button from '@jupiter/components/ui/Button.svelte';
|
import Button from '@jupiter/components/ui/Button.svelte';
|
||||||
|
|
@ -58,13 +58,22 @@
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<h2 class="account-title">
|
<h2 class="account-title">
|
||||||
|
<span>
|
||||||
|
{#if !account.activated}
|
||||||
|
<span class="warning-badge"><CircleAlert /></span>
|
||||||
|
{/if}
|
||||||
{account.display_name}
|
{account.display_name}
|
||||||
|
</span>
|
||||||
<button class="email" onclick={() => { copyEmail() }}>{email} <Copy /></button>
|
<button class="email" onclick={() => { copyEmail() }}>{email} <Copy /></button>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
{#if !account.activated}
|
||||||
|
<p class="warning">* This account is not activated yet.</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Label>Actions</Label>
|
<Label>Actions</Label>
|
||||||
<div class="account-actions">
|
<div class="account-actions">
|
||||||
<Button href="mailto:{email}"><Mail /> Send Email</Button>
|
<Button href="mailto:{email}"><Mail /> Send Email</Button>
|
||||||
|
|
@ -103,6 +112,10 @@
|
||||||
gap: .2em;
|
gap: .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-title-container .warning-badge {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
.account-title-icon {
|
.account-title-icon {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
|
@ -133,6 +146,12 @@
|
||||||
gap: .2em;
|
gap: .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-title span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
.account-title .email {
|
.account-title .email {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
height: 1.5em;
|
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