diff --git a/package.json b/package.json index 1743271..7a56986 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dayjs": "^1.11.10", "lucide-react": "^0.311.0", "next": "14.0.4", - "next-auth": "^4.24.5", + "next-auth": "5.0.0-beta.18", "next-swagger-doc": "^0.4.0", "random-words": "^2.0.0", "react": "^18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 207bbaf..200b792 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ dependencies: specifier: 14.0.4 version: 14.0.4(react-dom@18.2.0)(react@18.2.0) next-auth: - specifier: ^4.24.5 - version: 4.24.5(next@14.0.4)(react-dom@18.2.0)(react@18.2.0) + specifier: 5.0.0-beta.18 + version: 5.0.0-beta.18(next@14.0.4)(react@18.2.0) next-swagger-doc: specifier: ^0.4.0 version: 0.4.0(next@14.0.4)(openapi-types@12.1.3) @@ -124,6 +124,29 @@ packages: z-schema: 5.0.5 dev: false + /@auth/core@0.31.0: + resolution: {integrity: sha512-UKk3psvA1cRbk4/c9CkpWB8mdWrkKvzw0DmEYRsWolUQytQ2cRqx+hYuV6ZCsngw/xbj9hpmkZmAZEyq2g4fMg==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + nodemailer: ^6.8.0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + dependencies: + '@panva/hkdf': 1.1.1 + '@types/cookie': 0.6.0 + cookie: 0.6.0 + jose: 5.3.0 + oauth4webapi: 2.10.4 + preact: 10.11.3 + preact-render-to-string: 5.2.3(preact@10.11.3) + dev: false + /@babel/runtime-corejs3@7.23.8: resolution: {integrity: sha512-2ZzmcDugdm0/YQKFVYsXiwUN7USPX8PM7cytpb4PFl87fM+qYPSvTZX//8tyeJB1j0YDmafBJEbl5f8NfLyuKw==} engines: {node: '>=6.9.0'} @@ -1976,6 +1999,10 @@ packages: resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} dev: false + /@types/cookie@0.6.0: + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + dev: false + /@types/hast@2.3.9: resolution: {integrity: sha512-pTHyNlaMD/oKJmS+ZZUyFUcsZeBZpC0lmGquw98CqRVNgAdJZJeD7GoeLiT6Xbx5rU9VCjSt0RwEvDgzh4obFw==} dependencies: @@ -2634,11 +2661,6 @@ packages: dev: false optional: true - /cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} - dev: false - /cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -3973,8 +3995,8 @@ packages: hasBin: true dev: true - /jose@4.15.4: - resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==} + /jose@5.3.0: + resolution: {integrity: sha512-IChe9AtAE79ru084ow8jzkN2lNrG3Ntfiv65Cvj9uOCE2m5LNsdHG+9EbxWxAoWRF9TgDOqLN5jm08++owDVRg==} dev: false /js-file-download@0.4.12: @@ -4358,29 +4380,25 @@ packages: dev: false optional: true - /next-auth@4.24.5(next@14.0.4)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==} + /next-auth@5.0.0-beta.18(next@14.0.4)(react@18.2.0): + resolution: {integrity: sha512-x55L8wZb8PcPGCYA3e/l9tdpd7YL3FDuhas4W8pxq3PjrWJ9OoDxNN0otK9axJamJBbBgjfzTJjVQB6hXoe0ZQ==} peerDependencies: - next: ^12.2.5 || ^13 || ^14 + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + next: ^14 nodemailer: ^6.6.5 - react: ^17.0.2 || ^18 - react-dom: ^17.0.2 || ^18 + react: ^18.2.0 peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true nodemailer: optional: true dependencies: - '@babel/runtime': 7.23.8 - '@panva/hkdf': 1.1.1 - cookie: 0.5.0 - jose: 4.15.4 + '@auth/core': 0.31.0 next: 14.0.4(react-dom@18.2.0)(react@18.2.0) - oauth: 0.9.15 - openid-client: 5.6.4 - preact: 10.19.3 - preact-render-to-string: 5.2.6(preact@10.19.3) react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - uuid: 8.3.2 dev: false /next-swagger-doc@0.4.0(next@14.0.4)(openapi-types@12.1.3): @@ -4525,19 +4543,14 @@ packages: dev: false optional: true - /oauth@0.9.15: - resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + /oauth4webapi@2.10.4: + resolution: {integrity: sha512-DSoj8QoChzOCQlJkRmYxAJCIpnXFW32R0Uq7avyghIeB6iJq0XAblOD7pcq3mx4WEBDwMuKr0Y1qveCBleG2Xw==} dev: false /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - /object-hash@2.2.0: - resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} - engines: {node: '>= 6'} - dev: false - /object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} @@ -4603,11 +4616,6 @@ packages: es-abstract: 1.22.3 dev: true - /oidc-token-hash@5.0.3: - resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} - engines: {node: ^10.13.0 || >=12.0.0} - dev: false - /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -4625,15 +4633,6 @@ packages: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} dev: false - /openid-client@5.6.4: - resolution: {integrity: sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==} - dependencies: - jose: 4.15.4 - lru-cache: 6.0.0 - object-hash: 2.2.0 - oidc-token-hash: 5.0.3 - dev: false - /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -4840,17 +4839,17 @@ packages: source-map-js: 1.0.2 dev: true - /preact-render-to-string@5.2.6(preact@10.19.3): - resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} + /preact-render-to-string@5.2.3(preact@10.11.3): + resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} peerDependencies: preact: '>=10' dependencies: - preact: 10.19.3 + preact: 10.11.3 pretty-format: 3.8.0 dev: false - /preact@10.19.3: - resolution: {integrity: sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==} + /preact@10.11.3: + resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==} dev: false /prebuild-install@7.1.1: @@ -6117,11 +6116,6 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - /uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - dev: false - /validator@13.11.0: resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} engines: {node: '>= 0.10'} diff --git a/src/app/(dashboard)/globals.css b/src/app/(dashboard)/globals.css index d412ca6..c2327ff 100644 --- a/src/app/(dashboard)/globals.css +++ b/src/app/(dashboard)/globals.css @@ -2,6 +2,11 @@ @tailwind components; @tailwind utilities; +a { + text-decoration: none; + color: var(--accent-a11); +} + /* mfw "ui library" doesnt ship styles */ .ToastViewport { --viewport-padding: 25px; diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 6431dc7..e22d103 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata } from 'next'; import ThemeWrapper from '@/lib/components/wrapper/ThemeWrapper'; import './globals.css'; import AuthWrapper from '@/lib/components/wrapper/AuthWrapper'; -import { getServerSession } from 'next-auth'; +import { auth } from "@/auth"; import NavigationWrapper from '@/lib/components/wrapper/NavigationWrapper'; import BackgroundImage from '@/lib/components/ui/BackgroundImage'; import ToastProvider from '@/lib/providers/ToastProvider'; @@ -23,7 +23,7 @@ export default async function RootLayout({ - + {children} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 33fce79..7c62e2d 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,50 +1,2 @@ -import NextAuth, { AuthOptions } from "next-auth"; -import CredentialProvider from "next-auth/providers/credentials"; -import { sha256sum } from "@/lib/util"; -import { validateCredentials } from "@/lib/db"; -import { GRAVATAR_DEFAULT } from "@/lib/constants"; -import { auditLogRaw } from "@/lib/audit"; - -const authOptions: AuthOptions = { - providers: [ - CredentialProvider({ - name: 'Mail account', - credentials: { - email: { label: "E-Mail", type: "email", placeholder: "webmistress@amogus.cloud" }, - password: { label: "Password", type: "password" }, - }, - async authorize(credentials, req) { - console.log(`[${credentials?.email}] Authentication attempt`); - - if (credentials && await validateCredentials(credentials.email, credentials.password)) { - console.log(`[${credentials.email}] Authentication succeeded`); - - const emailHash = sha256sum(credentials.email.trim().toLowerCase()); - - await auditLogRaw({ - user: credentials.email, - ts: new Date().toISOString(), - action: "login", - }); - - return { - id: credentials.email, - email: credentials.email, - image: `https://gravatar.com/avatar/${emailHash}?d=${GRAVATAR_DEFAULT}`, - }; - } - - console.log(`[${credentials?.email}] Authentication failed`); - return null; - }, - }), - ], - session: { - strategy: 'jwt', - maxAge: 4 * 60 * 60 // 4 hours - }, -}; - -const handler = NextAuth(authOptions); - -export { handler as GET, handler as POST }; +import { handlers } from "@/auth"; +export const { GET, POST } = handlers; diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..72f1b0c --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,58 @@ +import nextAuth, { CredentialsSignin } from "next-auth"; +import CredentialProvider from "next-auth/providers/credentials"; +import WebAuthnProvider from "next-auth/providers/webauthn"; +import { sha256sum } from "@/lib/util"; +import { validateCredentials } from "@/lib/db"; +import { GRAVATAR_DEFAULT } from "@/lib/constants"; +import { auditLogRaw } from "@/lib/audit"; + +export const { auth, handlers, signIn, signOut } = nextAuth({ + providers: [ + CredentialProvider({ + name: "Mail Account", + credentials: { + email: { label: "E-Mail", type: "email", placeholder: "webmistress@amogus.cloud" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials, req) { + if (typeof credentials.email != "string" || typeof credentials.password != "string") { + console.log("Someone tried to fuck with the auth payload, uh oh"); + throw new CredentialsSignin("what"); + } + + console.log(`[${credentials?.email}] Authentication attempt`); + + if (credentials && await validateCredentials(credentials.email, credentials.password)) { + console.log(`[${credentials.email}] Authentication succeeded`); + + const emailHash = sha256sum(credentials.email.trim().toLowerCase()); + + await auditLogRaw({ + user: credentials.email, + ts: new Date().toISOString(), + action: "login", + }); + + return { + id: credentials.email, + email: credentials.email, + image: `https://gravatar.com/avatar/${emailHash}?d=${GRAVATAR_DEFAULT}`, + }; + } + + console.log(`[${credentials?.email}] Authentication failed`); + return null; + }, + }), + WebAuthnProvider({ + name: "Passkey", + }), + ], + session: { + strategy: 'jwt', + maxAge: 4 * 60 * 60, // 4 hours + }, + experimental: { + enableWebAuthn: true, + } +}); diff --git a/src/lib/actions.ts b/src/lib/actions.ts index f44cb2d..c4bae57 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -2,7 +2,7 @@ import crypto from "crypto"; import fs from "fs/promises"; -import { getServerSession } from "next-auth"; +import { auth } from "@/auth"; import { AuditLog, auditLog } from "./audit"; import { AliasEntry, AliasRequestEntry, ApiKeyEntry, approveAliasEntry, createAliasEntry, createApiKeyEntry, createTempAliasRequestEntry, createUserEntry, database, deleteAliasEntry, deleteApiKey, deleteTempAliasRequestEntry, getAlias, getAllAliases, getApiKeyById, getTempAliasRequestEntry, getUserAliases, getUserApiKeys, isAliasAvailable, setUserPassword } from "./db"; import { aliasesNeedApproval, anonymizeApiKey, isAdmin } from "./util"; @@ -10,7 +10,7 @@ import { generateAliasEmail } from "./util-server"; export async function fetchAllUsers(): Promise { return new Promise(async (resolve, reject) => { - const session = await getServerSession(); + const session = await auth(); if (!isAdmin(session)) return reject("Unauthenticated"); const db = database('credentials'); @@ -24,20 +24,20 @@ export async function fetchAllUsers(): Promise { export async function changeOwnPassword(newPass: string) { if (newPass.length < 8) throw new Error("Invalid password"); - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); await setUserPassword(session.user.email, newPass); auditLog("changeOwnPassword"); } export async function fetchOwnAliases(tempAliases?: boolean) { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); return await getUserAliases(session.user.email, tempAliases); } export async function fetchUserAliases(email: string) { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); if (!isAdmin(session)) throw new Error("Unauthorized"); @@ -45,7 +45,7 @@ export async function fetchUserAliases(email: string) { } export async function fetchAllAliases() { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); if (!isAdmin(session)) throw new Error("Unauthorized"); @@ -53,14 +53,14 @@ export async function fetchAllAliases() { } export async function aliasAvailable(email: string, searchTempRequests: boolean = false) { - const session = await getServerSession(); + const session = await auth(); if (!session?.user) throw new Error("Unauthenticated"); return await isAliasAvailable(email, searchTempRequests); } export async function createAlias(user: string, alias: string): Promise { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); if (!isAdmin(session)) throw new Error("Unauthenticated"); if (!await isAliasAvailable(alias)) throw new Error("Alias unavailable"); @@ -80,7 +80,7 @@ export async function createAlias(user: string, alias: string): Promise { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); const pending = aliasesNeedApproval(session); @@ -105,7 +105,7 @@ export async function requestTemporaryAlias( style: 'words' | 'random', oldToken?: string, ): Promise { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); if (!label.length || label.length > 16) throw new Error("Malformed request"); @@ -134,7 +134,7 @@ export async function requestTemporaryAlias( } export async function claimTemporaryAlias(key: string): Promise { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); const data = await getTempAliasRequestEntry(key); @@ -159,7 +159,7 @@ export async function claimTemporaryAlias(key: string): Promise { } export async function disposeTempAliasRequest(key: string) { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); const data = await getTempAliasRequestEntry(key); @@ -170,7 +170,7 @@ export async function disposeTempAliasRequest(key: string) { } export async function deleteAlias(alias: string) { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); if (!isAdmin(session) && (await getAlias(alias))?.address != session.user.email) { @@ -182,7 +182,7 @@ export async function deleteAlias(alias: string) { } export async function approveAlias(alias: string) { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); if (!isAdmin(session)) throw new Error("Unauthorized"); @@ -191,7 +191,7 @@ export async function approveAlias(alias: string) { } export async function createUser(email: string, password: string) { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); if (!isAdmin(session)) throw new Error("Unauthorized"); @@ -210,7 +210,7 @@ export async function createUser(email: string, password: string) { export async function fetchAuditLog(page: number): Promise<{ page: number, perPage: number, totalItems: number, items: (AuditLog & { index: number })[] }> { const itemsPerPage = 10; - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); if (!isAdmin(session)) throw new Error("Unauthorized"); @@ -239,7 +239,7 @@ export async function fetchAuditLog(page: number): Promise<{ page: number, perPa } export async function createApiKey(label: string): Promise { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); if (!label.length || label.length > 64) throw new Error("Malformed label"); @@ -260,7 +260,7 @@ export async function createApiKey(label: string): Promise { } export async function fetchOwnApiKeys(): Promise { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); const keys = await getUserApiKeys(session.user.email); @@ -268,7 +268,7 @@ export async function fetchOwnApiKeys(): Promise { } export async function deleteOwnApikey(id: number) { - const session = await getServerSession(); + const session = await auth(); if (!session?.user?.email) throw new Error("Unauthenticated"); const key = await getApiKeyById(id); diff --git a/src/lib/audit.ts b/src/lib/audit.ts index 3f9e571..4a5765c 100644 --- a/src/lib/audit.ts +++ b/src/lib/audit.ts @@ -1,4 +1,4 @@ -import { getServerSession } from "next-auth"; +import { auth } from "@/auth"; import fs from "fs/promises"; export type AuditLogAction = 'login' | 'changeOwnPassword' | 'requestAlias' | 'deleteAlias' | 'createTempAlias' // Unprivileged @@ -15,7 +15,7 @@ export type AuditLog = { export function auditLog(action: AuditLogAction, data?: any) { (async () => { try { - const session = await getServerSession(); + const session = await auth(); const log: AuditLog = { user: session?.user?.email, diff --git a/src/lib/components/ui/user/OwnCredentialsCard.tsx b/src/lib/components/ui/user/OwnCredentialsCard.tsx index d56db60..ed9a327 100644 --- a/src/lib/components/ui/user/OwnCredentialsCard.tsx +++ b/src/lib/components/ui/user/OwnCredentialsCard.tsx @@ -2,9 +2,10 @@ import { changeOwnPassword } from "@/lib/actions"; import { ToastContext } from "@/lib/providers/ToastProvider"; -import { Box, Button, Callout, Card, Dialog, Flex, Heading, IconButton, Link, Popover, Text, TextField, Tooltip } from "@radix-ui/themes"; +import { Box, Button, Callout, Card, Dialog, Flex, Heading, IconButton, Popover, Tabs, Text, TextField, Tooltip } from "@radix-ui/themes"; import { AlertCircleIcon, InfoIcon, LockIcon, UserIcon } from "lucide-react"; import { useSession } from "next-auth/react"; +import Link from "next/link"; import { useContext, useState } from "react"; export default function OwnCredentialsCard() { @@ -16,110 +17,133 @@ export default function OwnCredentialsCard() { return ( Credentials - These are the details you can use to authenticate via SMTP and IMAP. + These are the details you can use to authenticate to the webmail, settings panel and via other mail clients. - - - - - - - - - - - - - - - - Your username uniquely identifies your account and therefore cannot be changed. If you need a different email address, you can request an alias instead. - - - - - - - - - - - - - - - - - - - - - Update password - - - - - - - You will be logged out of every configured mail client! - - + + + Password + Passkeys + + + + - + + + + + + + + + + + + + Your username uniquely identifies your account and therefore cannot be changed. If you need a different email address, you can request an alias instead. + + + - setNewPassword(e.currentTarget.value)} - /> - - - - - - {validPassword - ? ( - - ) - : ( - - - - ) - } - + + + + + + + Update password + + + + + + + You will be logged out of every configured mail client! + + + + + + + + setNewPassword(e.currentTarget.value)} + /> + + + + + + + + {validPassword + ? ( + + ) + : ( + + + + ) + } + + + + - - - - + + + + + + You can use a passkey (software or + hardware based) instead of + your password to sign into the web UI. However, they are not compatible with the webmail or external + mail clients. + + + + + + + ); } \ No newline at end of file