feat: ✨ multi emails
This commit is contained in:
parent
882065381a
commit
fc6cc0b333
57
package-lock.json
generated
57
package-lock.json
generated
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"name": "ava",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ava",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"dependencies": {
|
||||
"@fluentui/svg-icons": "^1.1.265",
|
||||
"@fontsource-variable/inter": "^5.0.18",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
||||
"@prisma/client": "5.16.2",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"@sveltejs/kit": "^2.5.27",
|
||||
"mime": "^4.0.4",
|
||||
"sharp": "^0.33.5"
|
||||
|
@ -22,7 +22,7 @@
|
|||
"@types/node": "^20.14.10",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"prisma": "^5.16.2",
|
||||
"prisma": "^5.22.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
|
@ -512,7 +512,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.16.2",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
|
||||
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
|
@ -528,43 +530,53 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "5.16.2",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
|
||||
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.16.2",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
|
||||
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.16.2",
|
||||
"@prisma/engines-version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303",
|
||||
"@prisma/fetch-engine": "5.16.2",
|
||||
"@prisma/get-platform": "5.16.2"
|
||||
"@prisma/debug": "5.22.0",
|
||||
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"@prisma/fetch-engine": "5.22.0",
|
||||
"@prisma/get-platform": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303",
|
||||
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
|
||||
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "5.16.2",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
|
||||
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.16.2",
|
||||
"@prisma/engines-version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303",
|
||||
"@prisma/get-platform": "5.16.2"
|
||||
"@prisma/debug": "5.22.0",
|
||||
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"@prisma/get-platform": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "5.16.2",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
|
||||
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.16.2"
|
||||
"@prisma/debug": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs": {
|
||||
|
@ -1630,18 +1642,23 @@
|
|||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.16.2",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
||||
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.16.2"
|
||||
"@prisma/engines": "5.22.0"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ava",
|
||||
"version": "2.0.2",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
@ -17,7 +17,7 @@
|
|||
"@types/node": "^20.14.10",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"prisma": "^5.16.2",
|
||||
"prisma": "^5.22.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
|
@ -29,7 +29,7 @@
|
|||
"@fluentui/svg-icons": "^1.1.265",
|
||||
"@fontsource-variable/inter": "^5.0.18",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
||||
"@prisma/client": "5.16.2",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"@sveltejs/kit": "^2.5.27",
|
||||
"mime": "^4.0.4",
|
||||
"sharp": "^0.33.5"
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- The primary key for the `EmailHashes` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_EmailHashes" (
|
||||
"forUserId" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL DEFAULT 'unknown:${uuid()}',
|
||||
"sha256" BLOB NOT NULL,
|
||||
"md5" BLOB NOT NULL,
|
||||
"isPrimaryForUserId" TEXT,
|
||||
CONSTRAINT "EmailHashes_forUserId_fkey" FOREIGN KEY ("forUserId") REFERENCES "User" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "EmailHashes_isPrimaryForUserId_fkey" FOREIGN KEY ("isPrimaryForUserId") REFERENCES "User" ("userId") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_EmailHashes" ("forUserId", "md5", "sha256") SELECT "forUserId", "md5", "sha256" FROM "EmailHashes";
|
||||
DROP TABLE "EmailHashes";
|
||||
ALTER TABLE "new_EmailHashes" RENAME TO "EmailHashes";
|
||||
CREATE UNIQUE INDEX "EmailHashes_email_key" ON "EmailHashes"("email");
|
||||
CREATE UNIQUE INDEX "EmailHashes_isPrimaryForUserId_key" ON "EmailHashes"("isPrimaryForUserId");
|
||||
CREATE UNIQUE INDEX "EmailHashes_sha256_md5_key" ON "EmailHashes"("sha256", "md5");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
27
prisma/migrations/20241123022424_add_id/migration.sql
Normal file
27
prisma/migrations/20241123022424_add_id/migration.sql
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- The required column `id` was added to the `EmailHashes` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_EmailHashes" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"forUserId" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL DEFAULT 'unknown:${uuid()}',
|
||||
"sha256" BLOB NOT NULL,
|
||||
"md5" BLOB NOT NULL,
|
||||
"isPrimaryForUserId" TEXT,
|
||||
CONSTRAINT "EmailHashes_forUserId_fkey" FOREIGN KEY ("forUserId") REFERENCES "User" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "EmailHashes_isPrimaryForUserId_fkey" FOREIGN KEY ("isPrimaryForUserId") REFERENCES "User" ("userId") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_EmailHashes" ("email", "forUserId", "isPrimaryForUserId", "md5", "sha256") SELECT "email", "forUserId", "isPrimaryForUserId", "md5", "sha256" FROM "EmailHashes";
|
||||
DROP TABLE "EmailHashes";
|
||||
ALTER TABLE "new_EmailHashes" RENAME TO "EmailHashes";
|
||||
CREATE UNIQUE INDEX "EmailHashes_email_key" ON "EmailHashes"("email");
|
||||
CREATE UNIQUE INDEX "EmailHashes_isPrimaryForUserId_key" ON "EmailHashes"("isPrimaryForUserId");
|
||||
CREATE UNIQUE INDEX "EmailHashes_sha256_md5_key" ON "EmailHashes"("sha256", "md5");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
|
@ -24,7 +24,8 @@ model User {
|
|||
name String?
|
||||
avatars Avatar[]
|
||||
webhooks Webhook[]
|
||||
emailHashes EmailHashes?
|
||||
emailHashes EmailHashes[]
|
||||
primaryEmail EmailHashes? @relation("PrimaryEmail")
|
||||
|
||||
currentAvatarId String? @unique
|
||||
currentAvatar Avatar? @relation("CurrentAvatar", fields: [currentAvatarId], references: [id])
|
||||
|
@ -51,8 +52,17 @@ model Webhook {
|
|||
}
|
||||
|
||||
model EmailHashes {
|
||||
forUserId String @id
|
||||
id String @id @default(uuid())
|
||||
|
||||
forUserId String
|
||||
user User @relation(fields: [forUserId], references: [userId])
|
||||
|
||||
email String @unique @default("unknown:${uuid()}")
|
||||
sha256 Bytes
|
||||
md5 Bytes
|
||||
|
||||
isPrimaryForUserId String? @unique
|
||||
isPrimaryFor User? @relation("PrimaryEmail", fields: [isPrimaryForUserId], references: [userId])
|
||||
|
||||
@@unique([sha256, md5])
|
||||
}
|
||||
|
|
|
@ -126,20 +126,6 @@ export async function getUserInfo(id: string) {
|
|||
|
||||
userInfo = await userInfoRequest.json()
|
||||
|
||||
// get emailHashes
|
||||
|
||||
let emailHashes: Omit<EmailHashes, "forUserId"> | undefined = undefined
|
||||
|
||||
if (userInfo.email) {
|
||||
emailHashes = {
|
||||
sha256: crypto
|
||||
.createHash("sha256")
|
||||
.update(userInfo.email)
|
||||
.digest(),
|
||||
md5: crypto.createHash("md5").update(userInfo.email).digest(),
|
||||
}
|
||||
}
|
||||
|
||||
// update user
|
||||
await prisma.user.upsert({
|
||||
where: {
|
||||
|
@ -148,31 +134,54 @@ export async function getUserInfo(id: string) {
|
|||
update: {
|
||||
identifier: userInfo[configuration.userinfo.identifier],
|
||||
name: userInfo.name,
|
||||
...(emailHashes
|
||||
? {
|
||||
emailHashes: {
|
||||
upsert: {
|
||||
create: emailHashes,
|
||||
update: emailHashes,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
create: {
|
||||
userId: userInfo.sub,
|
||||
identifier: userInfo[configuration.userinfo.identifier],
|
||||
name: userInfo.name,
|
||||
...(emailHashes
|
||||
? {
|
||||
emailHashes: {
|
||||
create: emailHashes,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
|
||||
// get emailHashes
|
||||
|
||||
let emailHashes: Omit<EmailHashes, "id"> | undefined = undefined
|
||||
|
||||
if (userInfo.email) {
|
||||
emailHashes = {
|
||||
sha256: crypto
|
||||
.createHash("sha256")
|
||||
.update(userInfo.email)
|
||||
.digest(),
|
||||
md5: crypto.createHash("md5").update(userInfo.email).digest(),
|
||||
email: userInfo.email,
|
||||
isPrimaryForUserId: userInfo.sub,
|
||||
forUserId: userInfo.sub,
|
||||
}
|
||||
|
||||
// manual upsert here so we can use findFirst
|
||||
|
||||
let f = await prisma.emailHashes.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
isPrimaryForUserId: userInfo.sub,
|
||||
},
|
||||
{
|
||||
sha256: emailHashes.sha256,
|
||||
md5: emailHashes.md5,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await (f
|
||||
? prisma.emailHashes.update({
|
||||
where: { id: f.id },
|
||||
data: emailHashes,
|
||||
})
|
||||
: prisma.emailHashes.create({ data: emailHashes }))
|
||||
}
|
||||
|
||||
// cache userinfo
|
||||
userInfoCache.set(tokenInfo.owner, userInfo)
|
||||
setTimeout(() => userInfoCache.delete(tokenInfo.owner), 15 * 60 * 1000)
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
<a href="/">{@html ava}</a>
|
||||
<a href="/set">Set avatar</a>
|
||||
{#if data.user}
|
||||
<a href="/user/{data.user.identifier}">Public page</a>
|
||||
<a href="/logout">Log out</a>
|
||||
{/if}
|
||||
</nav>
|
||||
|
|
126
src/routes/emails/+page.server.ts
Normal file
126
src/routes/emails/+page.server.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
import { getRequestUser, launchLogin } from "$lib/oidc"
|
||||
import configuration from "$lib/configuration.js"
|
||||
import { fail, redirect } from "@sveltejs/kit"
|
||||
import {
|
||||
avatarDirectory,
|
||||
createNewAvatar,
|
||||
getMetadataForUserId,
|
||||
} from "$lib/avatars.js"
|
||||
import { join } from "path"
|
||||
import { prisma } from "$lib/clientsingleton"
|
||||
import { URL_REGEX } from "$lib/common.js"
|
||||
import crypto from "node:crypto"
|
||||
export async function load({ request, parent, url }) {
|
||||
const { user } = await parent()
|
||||
if (!user) return launchLogin(url.toString())
|
||||
|
||||
return {
|
||||
url: url.toString(),
|
||||
emails: (
|
||||
await prisma.emailHashes.findMany({
|
||||
where: { forUserId: user.sub },
|
||||
})
|
||||
).map(e =>
|
||||
Object.fromEntries([
|
||||
...Object.entries(e).filter(([k]) =>
|
||||
["email", "id"].includes(k)
|
||||
),
|
||||
["isPrimary", Boolean(e.isPrimaryForUserId)],
|
||||
])
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
create: async ({ request, cookies }) => {
|
||||
let user = await getRequestUser(request, cookies)
|
||||
if (!user) return fail(401, { error: "unauthenticated" })
|
||||
|
||||
let { email } = Object.fromEntries((await request.formData()).entries())
|
||||
|
||||
if (!email || email instanceof File)
|
||||
return fail(400, { error: "no email supplied" })
|
||||
|
||||
email = email.toLowerCase()
|
||||
|
||||
const sha256 = crypto.createHash("sha256").update(email).digest(),
|
||||
md5 = crypto.createHash("md5").update(email).digest()
|
||||
|
||||
// check hashes because 2.0 didn't store email string
|
||||
let { user: ownedBy } =
|
||||
(await prisma.emailHashes.findUnique({
|
||||
where: {
|
||||
sha256_md5: {
|
||||
sha256,
|
||||
md5,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
identifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})) || {}
|
||||
|
||||
if (ownedBy)
|
||||
return fail(409, {
|
||||
error: `Email already owned by ${user.name} (${user.identifier})`,
|
||||
})
|
||||
|
||||
await prisma.emailHashes.create({
|
||||
data: {
|
||||
email,
|
||||
sha256,
|
||||
md5,
|
||||
forUserId: user.sub,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Email claimed",
|
||||
}
|
||||
},
|
||||
manage: async ({ request, cookies }) => {
|
||||
let user = await getRequestUser(request, cookies)
|
||||
if (!user) return fail(401, { error: "unauthenticated" })
|
||||
|
||||
let { action, id } = Object.fromEntries(
|
||||
(await request.formData()).entries()
|
||||
)
|
||||
|
||||
if (!id || id instanceof File)
|
||||
return fail(400, { error: "No email supplied" })
|
||||
|
||||
id = id.toLowerCase()
|
||||
|
||||
let emhash = await prisma.emailHashes.findUnique({
|
||||
where: {
|
||||
id,
|
||||
forUserId: user.sub,
|
||||
},
|
||||
})
|
||||
|
||||
if (!emhash) return fail(404, { error: "Email doesn't exist" })
|
||||
|
||||
if (emhash.isPrimaryForUserId == user.sub)
|
||||
return fail(403, { error: "You can't delete the primary email" })
|
||||
|
||||
if (action == "Delete") {
|
||||
await prisma.emailHashes.delete({
|
||||
where: {
|
||||
id,
|
||||
forUserId: user.sub,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Email removed from account",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
96
src/routes/emails/+page.svelte
Normal file
96
src/routes/emails/+page.svelte
Normal file
|
@ -0,0 +1,96 @@
|
|||
<script lang="ts">
|
||||
import StatusBanner from "$lib/components/StatusBanner.svelte";
|
||||
import type { User } from "$lib/types";
|
||||
import ReversibleHeading from "$lib/components/ReversibleHeading.svelte"
|
||||
|
||||
export interface Props {
|
||||
data: {
|
||||
user: User,
|
||||
emails: {isPrimary: boolean, email: string, id: string}[]
|
||||
};
|
||||
form: { success: true, message: string } | { success: false, error: string } | undefined;
|
||||
}
|
||||
|
||||
let { data = $bindable(), form }: Props = $props();
|
||||
|
||||
let otherEmails = data.emails.filter(e => !e.isPrimary)
|
||||
let primaryEmail = data.emails.find(e => e.isPrimary)
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
input[type="email"] {
|
||||
flex-basis: 100%;
|
||||
min-height: 1em;
|
||||
}
|
||||
input[type="submit"], form {
|
||||
cursor: pointer;
|
||||
}
|
||||
form input[name="id"] {
|
||||
display: none;
|
||||
}
|
||||
input {
|
||||
font-family: "Inter Variable", "Inter", sans-serif;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--link);
|
||||
color: var(--text);
|
||||
background-color: var(--crust);
|
||||
}
|
||||
div.flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ReversibleHeading to="/set">
|
||||
Manage emails
|
||||
</ReversibleHeading>
|
||||
{#if form}
|
||||
<br>
|
||||
<StatusBanner status={form.success ? "success" : "error"}>
|
||||
{form.success ? form.message : form.error}
|
||||
</StatusBanner>
|
||||
<br>
|
||||
{/if}
|
||||
{#if primaryEmail}
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>Your primary email</h2>
|
||||
<p>
|
||||
This email is provided by your OIDC provider and cannot be changed. Go to your OIDC provider to change it.
|
||||
</p>
|
||||
</hgroup>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="email" name="email" readonly value={primaryEmail.email}>
|
||||
</form>
|
||||
</div>
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
{/if}
|
||||
{#if otherEmails.length > 0}
|
||||
<div class="flex">
|
||||
{#each otherEmails as email}
|
||||
<form method="post" enctype="multipart/form-data" action="?/manage">
|
||||
<input name="id" readonly value={email.id}>
|
||||
<input type="email" name="email" readonly value={email.email}>
|
||||
<input type="submit" name="action" value="Delete">
|
||||
</form>
|
||||
{/each}
|
||||
</div>
|
||||
<br>
|
||||
{/if}
|
||||
<form method="post" enctype="multipart/form-data" action="?/create">
|
||||
<input type="email" name="email" placeholder="Email">
|
||||
<input type="submit" name="action" value="Add">
|
||||
</form>
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
These emails redirect to your avatar in apps that use the libravatar API. Use this page to add any email aliases you may have.
|
|
@ -94,5 +94,5 @@
|
|||
•
|
||||
<a href="/webhooks">Webhooks</a>
|
||||
•
|
||||
<a href="/user/{data.user.identifier}" target="_blank">See user page</a>
|
||||
<a href="/emails">Emails</a>
|
||||
</footer>
|
Loading…
Reference in a new issue