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",
|
"name": "ava",
|
||||||
"version": "2.0.0",
|
"version": "2.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ava",
|
"name": "ava",
|
||||||
"version": "2.0.0",
|
"version": "2.0.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/svg-icons": "^1.1.265",
|
"@fluentui/svg-icons": "^1.1.265",
|
||||||
"@fontsource-variable/inter": "^5.0.18",
|
"@fontsource-variable/inter": "^5.0.18",
|
||||||
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
||||||
"@prisma/client": "5.16.2",
|
"@prisma/client": "^5.22.0",
|
||||||
"@sveltejs/kit": "^2.5.27",
|
"@sveltejs/kit": "^2.5.27",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
"sharp": "^0.33.5"
|
"sharp": "^0.33.5"
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.10",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"prisma": "^5.16.2",
|
"prisma": "^5.22.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
|
@ -512,7 +512,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"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,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -528,43 +530,53 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/debug": {
|
"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,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"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,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "5.16.2",
|
"@prisma/debug": "5.22.0",
|
||||||
"@prisma/engines-version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303",
|
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||||
"@prisma/fetch-engine": "5.16.2",
|
"@prisma/fetch-engine": "5.22.0",
|
||||||
"@prisma/get-platform": "5.16.2"
|
"@prisma/get-platform": "5.22.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"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,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"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,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "5.16.2",
|
"@prisma/debug": "5.22.0",
|
||||||
"@prisma/engines-version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303",
|
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||||
"@prisma/get-platform": "5.16.2"
|
"@prisma/get-platform": "5.22.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"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,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "5.16.2"
|
"@prisma/debug": "5.22.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/plugin-commonjs": {
|
"node_modules/@rollup/plugin-commonjs": {
|
||||||
|
@ -1630,18 +1642,23 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"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,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/engines": "5.16.2"
|
"@prisma/engines": "5.22.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js"
|
"prisma": "build/index.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.13"
|
"node": ">=16.13"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ava",
|
"name": "ava",
|
||||||
"version": "2.0.2",
|
"version": "2.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.10",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"prisma": "^5.16.2",
|
"prisma": "^5.22.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
"@fluentui/svg-icons": "^1.1.265",
|
"@fluentui/svg-icons": "^1.1.265",
|
||||||
"@fontsource-variable/inter": "^5.0.18",
|
"@fontsource-variable/inter": "^5.0.18",
|
||||||
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
||||||
"@prisma/client": "5.16.2",
|
"@prisma/client": "^5.22.0",
|
||||||
"@sveltejs/kit": "^2.5.27",
|
"@sveltejs/kit": "^2.5.27",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
"sharp": "^0.33.5"
|
"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;
|
|
@ -19,12 +19,13 @@ model Token {
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
userId String @id @unique
|
userId String @id @unique
|
||||||
identifier String
|
identifier String
|
||||||
name String?
|
name String?
|
||||||
avatars Avatar[]
|
avatars Avatar[]
|
||||||
webhooks Webhook[]
|
webhooks Webhook[]
|
||||||
emailHashes EmailHashes?
|
emailHashes EmailHashes[]
|
||||||
|
primaryEmail EmailHashes? @relation("PrimaryEmail")
|
||||||
|
|
||||||
currentAvatarId String? @unique
|
currentAvatarId String? @unique
|
||||||
currentAvatar Avatar? @relation("CurrentAvatar", fields: [currentAvatarId], references: [id])
|
currentAvatar Avatar? @relation("CurrentAvatar", fields: [currentAvatarId], references: [id])
|
||||||
|
@ -51,8 +52,17 @@ model Webhook {
|
||||||
}
|
}
|
||||||
|
|
||||||
model EmailHashes {
|
model EmailHashes {
|
||||||
forUserId String @id
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
forUserId String
|
||||||
user User @relation(fields: [forUserId], references: [userId])
|
user User @relation(fields: [forUserId], references: [userId])
|
||||||
sha256 Bytes
|
|
||||||
md5 Bytes
|
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()
|
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
|
// update user
|
||||||
await prisma.user.upsert({
|
await prisma.user.upsert({
|
||||||
where: {
|
where: {
|
||||||
|
@ -148,31 +134,54 @@ export async function getUserInfo(id: string) {
|
||||||
update: {
|
update: {
|
||||||
identifier: userInfo[configuration.userinfo.identifier],
|
identifier: userInfo[configuration.userinfo.identifier],
|
||||||
name: userInfo.name,
|
name: userInfo.name,
|
||||||
...(emailHashes
|
|
||||||
? {
|
|
||||||
emailHashes: {
|
|
||||||
upsert: {
|
|
||||||
create: emailHashes,
|
|
||||||
update: emailHashes,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
userId: userInfo.sub,
|
userId: userInfo.sub,
|
||||||
identifier: userInfo[configuration.userinfo.identifier],
|
identifier: userInfo[configuration.userinfo.identifier],
|
||||||
name: userInfo.name,
|
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
|
// cache userinfo
|
||||||
userInfoCache.set(tokenInfo.owner, userInfo)
|
userInfoCache.set(tokenInfo.owner, userInfo)
|
||||||
setTimeout(() => userInfoCache.delete(tokenInfo.owner), 15 * 60 * 1000)
|
setTimeout(() => userInfoCache.delete(tokenInfo.owner), 15 * 60 * 1000)
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
<a href="/">{@html ava}</a>
|
<a href="/">{@html ava}</a>
|
||||||
<a href="/set">Set avatar</a>
|
<a href="/set">Set avatar</a>
|
||||||
{#if data.user}
|
{#if data.user}
|
||||||
|
<a href="/user/{data.user.identifier}">Public page</a>
|
||||||
<a href="/logout">Log out</a>
|
<a href="/logout">Log out</a>
|
||||||
{/if}
|
{/if}
|
||||||
</nav>
|
</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="/webhooks">Webhooks</a>
|
||||||
•
|
•
|
||||||
<a href="/user/{data.user.identifier}" target="_blank">See user page</a>
|
<a href="/emails">Emails</a>
|
||||||
</footer>
|
</footer>
|
Loading…
Reference in a new issue