Compare commits

..

1 commit

Author SHA1 Message Date
Lea f00cf653c0
stuff 2024-06-16 23:35:53 +02:00
17 changed files with 3474 additions and 4136 deletions

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Install Docker - name: Install Docker
run: |- run: |-

View file

@ -6,9 +6,15 @@ FROM base AS deps
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
# Install dependencies # Install dependencies based on the preferred package manager
COPY package.json pnpm-lock.yaml ./ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN npm i -g pnpm && pnpm i --frozen-lockfile RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM base AS builder FROM base AS builder

View file

@ -16,7 +16,7 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"lucide-react": "^0.311.0", "lucide-react": "^0.311.0",
"next": "14.0.4", "next": "14.0.4",
"next-auth": "^4.24.5", "next-auth": "5.0.0-beta.18",
"next-swagger-doc": "^0.4.0", "next-swagger-doc": "^0.4.0",
"random-words": "^2.0.0", "random-words": "^2.0.0",
"react": "^18", "react": "^18",
@ -35,6 +35,5 @@
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"typescript": "^5" "typescript": "^5"
}, }
"packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee"
} }

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ import { fetchAuditLog } from "@/lib/actions";
import { AuditLog, AuditLogAction } from "@/lib/audit"; import { AuditLog, AuditLogAction } from "@/lib/audit";
import GhostMessage from "@/lib/components/ui/GhostMessage"; import GhostMessage from "@/lib/components/ui/GhostMessage";
import LoadingSpinner from "@/lib/components/ui/LoadingSpinner"; import LoadingSpinner from "@/lib/components/ui/LoadingSpinner";
import { avatarUrl, GRAVATAR_DEFAULT } from "@/lib/constants"; import { GRAVATAR_DEFAULT } from "@/lib/constants";
import { sha256sum } from "@/lib/util"; import { sha256sum } from "@/lib/util";
import { Avatar, Card, Code, Flex, Grid, Heading, IconButton, Text } from "@radix-ui/themes"; import { Avatar, Card, Code, Flex, Grid, Heading, IconButton, Text } from "@radix-ui/themes";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -98,7 +98,7 @@ export default function Audit() {
<Flex direction='row' gap='4' align='center'> <Flex direction='row' gap='4' align='center'>
<Avatar <Avatar
size="2" size="2"
src={avatarUrl(item.user ?? "")} src={`https://gravatar.com/avatar/${sha256sum(item.user)}?d=${GRAVATAR_DEFAULT}`}
radius='full' radius='full'
fallback={item.user?.slice(0, 1) || "@"} fallback={item.user?.slice(0, 1) || "@"}
/> />

View file

@ -5,7 +5,7 @@ import GhostMessage from "@/lib/components/ui/GhostMessage";
import LoadingSpinner from "@/lib/components/ui/LoadingSpinner"; import LoadingSpinner from "@/lib/components/ui/LoadingSpinner";
import CreateUserButton from "@/lib/components/ui/admin/CreateUserButton"; import CreateUserButton from "@/lib/components/ui/admin/CreateUserButton";
import ManageUserButton from "@/lib/components/ui/admin/ManageUserButton"; import ManageUserButton from "@/lib/components/ui/admin/ManageUserButton";
import { avatarUrl, GRAVATAR_DEFAULT } from "@/lib/constants"; import { GRAVATAR_DEFAULT } from "@/lib/constants";
import { isAdmin, sha256sum } from "@/lib/util"; import { isAdmin, sha256sum } from "@/lib/util";
import { Avatar, Badge, Button, Card, Flex, Heading, Table, Text, TextField } from "@radix-ui/themes"; import { Avatar, Badge, Button, Card, Flex, Heading, Table, Text, TextField } from "@radix-ui/themes";
import { SearchIcon, UserRoundXIcon } from "lucide-react"; import { SearchIcon, UserRoundXIcon } from "lucide-react";
@ -59,7 +59,7 @@ export default function Users() {
<Flex direction='row' gap='4' align='center'> <Flex direction='row' gap='4' align='center'>
<Avatar <Avatar
size="2" size="2"
src={avatarUrl(email)} src={`https://gravatar.com/avatar/${sha256sum(email)}?d=${GRAVATAR_DEFAULT}`}
radius='full' radius='full'
fallback={email.slice(0, 1) || "@"} fallback={email.slice(0, 1) || "@"}
/> />

View file

@ -2,6 +2,11 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
a {
text-decoration: none;
color: var(--accent-a11);
}
/* mfw "ui library" doesnt ship styles */ /* mfw "ui library" doesnt ship styles */
.ToastViewport { .ToastViewport {
--viewport-padding: 25px; --viewport-padding: 25px;

View file

@ -2,7 +2,7 @@ import type { Metadata } from 'next';
import ThemeWrapper from '@/lib/components/wrapper/ThemeWrapper'; import ThemeWrapper from '@/lib/components/wrapper/ThemeWrapper';
import './globals.css'; import './globals.css';
import AuthWrapper from '@/lib/components/wrapper/AuthWrapper'; import AuthWrapper from '@/lib/components/wrapper/AuthWrapper';
import { getServerSession } from 'next-auth'; import { auth } from "@/auth";
import NavigationWrapper from '@/lib/components/wrapper/NavigationWrapper'; import NavigationWrapper from '@/lib/components/wrapper/NavigationWrapper';
import BackgroundImage from '@/lib/components/ui/BackgroundImage'; import BackgroundImage from '@/lib/components/ui/BackgroundImage';
import ToastProvider from '@/lib/providers/ToastProvider'; import ToastProvider from '@/lib/providers/ToastProvider';
@ -23,7 +23,7 @@ export default async function RootLayout({
<ThemeWrapper> <ThemeWrapper>
<BackgroundImage /> <BackgroundImage />
<ToastProvider> <ToastProvider>
<AuthWrapper session={await getServerSession()}> <AuthWrapper session={await auth()}>
<NavigationWrapper> <NavigationWrapper>
{children} {children}
</NavigationWrapper> </NavigationWrapper>

View file

@ -1,48 +1,2 @@
import NextAuth, { AuthOptions } from "next-auth"; import { handlers } from "@/auth";
import CredentialProvider from "next-auth/providers/credentials"; export const { GET, POST } = handlers;
import { sha256sum } from "@/lib/util";
import { validateCredentials } from "@/lib/db";
import { avatarUrl, 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`);
await auditLogRaw({
user: credentials.email,
ts: new Date().toISOString(),
action: "login",
});
return {
id: credentials.email,
email: credentials.email,
image: avatarUrl(credentials.email),
};
}
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 };

58
src/auth.ts Normal file
View file

@ -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,
}
});

View file

@ -2,7 +2,7 @@
import crypto from "crypto"; import crypto from "crypto";
import fs from "fs/promises"; import fs from "fs/promises";
import { getServerSession } from "next-auth"; import { auth } from "@/auth";
import { AuditLog, auditLog } from "./audit"; 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 { 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"; import { aliasesNeedApproval, anonymizeApiKey, isAdmin } from "./util";
@ -10,7 +10,7 @@ import { generateAliasEmail } from "./util-server";
export async function fetchAllUsers(): Promise<string[]> { export async function fetchAllUsers(): Promise<string[]> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const session = await getServerSession(); const session = await auth();
if (!isAdmin(session)) return reject("Unauthenticated"); if (!isAdmin(session)) return reject("Unauthenticated");
const db = database('credentials'); const db = database('credentials');
@ -24,20 +24,20 @@ export async function fetchAllUsers(): Promise<string[]> {
export async function changeOwnPassword(newPass: string) { export async function changeOwnPassword(newPass: string) {
if (newPass.length < 8) throw new Error("Invalid password"); 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"); if (!session?.user?.email) throw new Error("Unauthenticated");
await setUserPassword(session.user.email, newPass); await setUserPassword(session.user.email, newPass);
auditLog("changeOwnPassword"); auditLog("changeOwnPassword");
} }
export async function fetchOwnAliases(tempAliases?: boolean) { export async function fetchOwnAliases(tempAliases?: boolean) {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
return await getUserAliases(session.user.email, tempAliases); return await getUserAliases(session.user.email, tempAliases);
} }
export async function fetchUserAliases(email: string) { export async function fetchUserAliases(email: string) {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
if (!isAdmin(session)) throw new Error("Unauthorized"); if (!isAdmin(session)) throw new Error("Unauthorized");
@ -45,7 +45,7 @@ export async function fetchUserAliases(email: string) {
} }
export async function fetchAllAliases() { export async function fetchAllAliases() {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
if (!isAdmin(session)) throw new Error("Unauthorized"); if (!isAdmin(session)) throw new Error("Unauthorized");
@ -53,14 +53,14 @@ export async function fetchAllAliases() {
} }
export async function aliasAvailable(email: string, searchTempRequests: boolean = false) { export async function aliasAvailable(email: string, searchTempRequests: boolean = false) {
const session = await getServerSession(); const session = await auth();
if (!session?.user) throw new Error("Unauthenticated"); if (!session?.user) throw new Error("Unauthenticated");
return await isAliasAvailable(email, searchTempRequests); return await isAliasAvailable(email, searchTempRequests);
} }
export async function createAlias(user: string, alias: string): Promise<AliasEntry> { export async function createAlias(user: string, alias: string): Promise<AliasEntry> {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
if (!isAdmin(session)) throw new Error("Unauthenticated"); if (!isAdmin(session)) throw new Error("Unauthenticated");
if (!await isAliasAvailable(alias)) throw new Error("Alias unavailable"); if (!await isAliasAvailable(alias)) throw new Error("Alias unavailable");
@ -80,7 +80,7 @@ export async function createAlias(user: string, alias: string): Promise<AliasEnt
} }
export async function createAliasSelf(alias: string): Promise<AliasEntry> { export async function createAliasSelf(alias: string): Promise<AliasEntry> {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
const pending = aliasesNeedApproval(session); const pending = aliasesNeedApproval(session);
@ -105,7 +105,7 @@ export async function requestTemporaryAlias(
style: 'words' | 'random', style: 'words' | 'random',
oldToken?: string, oldToken?: string,
): Promise<AliasRequestEntry> { ): Promise<AliasRequestEntry> {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
if (!label.length || label.length > 16) throw new Error("Malformed request"); 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<AliasEntry> { export async function claimTemporaryAlias(key: string): Promise<AliasEntry> {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
const data = await getTempAliasRequestEntry(key); const data = await getTempAliasRequestEntry(key);
@ -159,7 +159,7 @@ export async function claimTemporaryAlias(key: string): Promise<AliasEntry> {
} }
export async function disposeTempAliasRequest(key: string) { export async function disposeTempAliasRequest(key: string) {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
const data = await getTempAliasRequestEntry(key); const data = await getTempAliasRequestEntry(key);
@ -170,7 +170,7 @@ export async function disposeTempAliasRequest(key: string) {
} }
export async function deleteAlias(alias: string) { export async function deleteAlias(alias: string) {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
if (!isAdmin(session) && (await getAlias(alias))?.address != session.user.email) { 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) { export async function approveAlias(alias: string) {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
if (!isAdmin(session)) throw new Error("Unauthorized"); 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) { 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 (!session?.user?.email) throw new Error("Unauthenticated");
if (!isAdmin(session)) throw new Error("Unauthorized"); 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 })[] }> { export async function fetchAuditLog(page: number): Promise<{ page: number, perPage: number, totalItems: number, items: (AuditLog & { index: number })[] }> {
const itemsPerPage = 10; const itemsPerPage = 10;
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
if (!isAdmin(session)) throw new Error("Unauthorized"); 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<ApiKeyEntry> { export async function createApiKey(label: string): Promise<ApiKeyEntry> {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
if (!label.length || label.length > 64) throw new Error("Malformed label"); if (!label.length || label.length > 64) throw new Error("Malformed label");
@ -260,7 +260,7 @@ export async function createApiKey(label: string): Promise<ApiKeyEntry> {
} }
export async function fetchOwnApiKeys(): Promise<ApiKeyEntry[]> { export async function fetchOwnApiKeys(): Promise<ApiKeyEntry[]> {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
const keys = await getUserApiKeys(session.user.email); const keys = await getUserApiKeys(session.user.email);
@ -268,7 +268,7 @@ export async function fetchOwnApiKeys(): Promise<ApiKeyEntry[]> {
} }
export async function deleteOwnApikey(id: number) { export async function deleteOwnApikey(id: number) {
const session = await getServerSession(); const session = await auth();
if (!session?.user?.email) throw new Error("Unauthenticated"); if (!session?.user?.email) throw new Error("Unauthenticated");
const key = await getApiKeyById(id); const key = await getApiKeyById(id);

View file

@ -1,4 +1,4 @@
import { getServerSession } from "next-auth"; import { auth } from "@/auth";
import fs from "fs/promises"; import fs from "fs/promises";
export type AuditLogAction = 'login' | 'changeOwnPassword' | 'requestAlias' | 'deleteAlias' | 'createTempAlias' // Unprivileged export type AuditLogAction = 'login' | 'changeOwnPassword' | 'requestAlias' | 'deleteAlias' | 'createTempAlias' // Unprivileged
@ -15,7 +15,7 @@ export type AuditLog = {
export function auditLog(action: AuditLogAction, data?: any) { export function auditLog(action: AuditLogAction, data?: any) {
(async () => { (async () => {
try { try {
const session = await getServerSession(); const session = await auth();
const log: AuditLog = { const log: AuditLog = {
user: session?.user?.email, user: session?.user?.email,

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { avatarUrl, GRAVATAR_DEFAULT } from "@/lib/constants"; import { GRAVATAR_DEFAULT } from "@/lib/constants";
import { AliasEntry } from "@/lib/db"; import { AliasEntry } from "@/lib/db";
import { isAdmin, sha256sum } from "@/lib/util"; import { isAdmin, sha256sum } from "@/lib/util";
import { Avatar, Badge, Button, Card, Code, Dialog, Flex, Grid, Heading, Table, Text } from "@radix-ui/themes"; import { Avatar, Badge, Button, Card, Code, Dialog, Flex, Grid, Heading, Table, Text } from "@radix-ui/themes";
@ -36,7 +36,7 @@ export default function ManageUserButton({ email }: { email: string }) {
<Flex direction="row" gap="3" align="center"> <Flex direction="row" gap="3" align="center">
<Avatar <Avatar
size="4" size="4"
src={avatarUrl(email)} src={`https://gravatar.com/avatar/${sha256sum(email)}?d=${GRAVATAR_DEFAULT}`}
fallback={email.slice(0, 1) || "@"} fallback={email.slice(0, 1) || "@"}
/> />
<Flex direction="column"> <Flex direction="column">

View file

@ -2,9 +2,10 @@
import { changeOwnPassword } from "@/lib/actions"; import { changeOwnPassword } from "@/lib/actions";
import { ToastContext } from "@/lib/providers/ToastProvider"; 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 { AlertCircleIcon, InfoIcon, LockIcon, UserIcon } from "lucide-react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import Link from "next/link";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
export default function OwnCredentialsCard() { export default function OwnCredentialsCard() {
@ -16,8 +17,16 @@ export default function OwnCredentialsCard() {
return ( return (
<Card className="h-fit"> <Card className="h-fit">
<Heading size="3">Credentials</Heading> <Heading size="3">Credentials</Heading>
<Text weight="light" size="2">These are the details you can use to authenticate via SMTP and IMAP.</Text> <Text weight="light" size="2">These are the details you can use to authenticate to the webmail, settings panel and via other mail clients.</Text>
<Tabs.Root defaultValue="password">
<Tabs.List size="2">
<Tabs.Trigger value="password">Password</Tabs.Trigger>
<Tabs.Trigger value="passkey">Passkeys</Tabs.Trigger>
</Tabs.List>
<Box pt="3" pb="2">
<Tabs.Content value="password">
<Flex direction="column" gap="3" className="pt-2"> <Flex direction="column" gap="3" className="pt-2">
<TextField.Root> <TextField.Root>
<TextField.Slot> <TextField.Slot>
@ -120,6 +129,21 @@ export default function OwnCredentialsCard() {
</Dialog.Root> </Dialog.Root>
</Flex> </Flex>
</Flex> </Flex>
</Tabs.Content>
<Tabs.Content value="passkey">
<Card>
<Text weight="light" size="2">
You can use a passkey (<Link href="https://bitwarden.com/passwordless-passkeys/">software</Link> or
<Link href="https://www.yubico.com/products/yubikey-5-overview/">hardware</Link> based) instead of
your password to sign into the web UI. However, they are not compatible with the webmail or external
mail clients.
</Text>
</Card>
</Tabs.Content>
</Box>
</Tabs.Root>
</Card> </Card>
); );
} }

View file

@ -1,5 +1,5 @@
import { disposeTempAliasRequest, fetchOwnAliases, requestTemporaryAlias, claimTemporaryAlias, deleteAlias } from "@/lib/actions"; import { disposeTempAliasRequest, fetchOwnAliases, requestTemporaryAlias, claimTemporaryAlias, deleteAlias } from "@/lib/actions";
import { avatarUrl, GRAVATAR_DEFAULT } from "@/lib/constants"; import { GRAVATAR_DEFAULT } from "@/lib/constants";
import { AliasEntry, AliasRequestEntry } from "@/lib/db"; import { AliasEntry, AliasRequestEntry } from "@/lib/db";
import { ToastContext } from "@/lib/providers/ToastProvider"; import { ToastContext } from "@/lib/providers/ToastProvider";
import { sha256sum } from "@/lib/util"; import { sha256sum } from "@/lib/util";
@ -120,7 +120,7 @@ export default function TempAliasesCard() {
<Flex direction="row" align="center" gap="3"> <Flex direction="row" align="center" gap="3">
<Avatar <Avatar
size="3" size="3"
src={avatarUrl(aliasPreview?.alias || "")} src={`https://gravatar.com/avatar/${sha256sum(aliasPreview?.alias || "")}?d=${GRAVATAR_DEFAULT}`}
fallback={"@"} fallback={"@"}
/> />
<Flex direction="column" gap="0"> <Flex direction="column" gap="0">
@ -171,7 +171,7 @@ export default function TempAliasesCard() {
<Flex direction="row" align="center" gap="3"> <Flex direction="row" align="center" gap="3">
<Avatar <Avatar
size="3" size="3"
src={avatarUrl(aliasPreview!.alias)} src={`https://gravatar.com/avatar/${sha256sum(aliasPreview!.alias)}?d=${GRAVATAR_DEFAULT}`}
fallback={"@"} fallback={"@"}
/> />
<Flex direction="column" gap="0"> <Flex direction="column" gap="0">

View file

@ -1,4 +1,3 @@
import { sha256sum } from "./util";
// TODO read these from environment // TODO read these from environment
@ -8,11 +7,6 @@ export const IMAP_PORT = "993";
export const SMTP_SECURITY = "SSL/TLS"; export const SMTP_SECURITY = "SSL/TLS";
export const IMAP_SECURITY = "SSL/TLS"; export const IMAP_SECURITY = "SSL/TLS";
export const WEBMAIL_URL = "https://webmail.amogus.cloud"; export const WEBMAIL_URL = "https://webmail.amogus.cloud";
export const ALIAS_DOMAINS = ["amogus.cloud", "lea.pet", "futacockinside.me", "cybercrime.gay"]; export const ALIAS_DOMAINS = ["amogus.cloud", "lea.pet", "futacockinside.me"];
export const GRAVATAR_DEFAULT = "retro"; export const GRAVATAR_DEFAULT = "retro";
export const TEMP_EMAIL_DOMAIN = "t.amogus.cloud"; export const TEMP_EMAIL_DOMAIN = "t.amogus.cloud";
export function avatarUrl(email: string) {
//return `https://gravatar.com/avatar/${sha256sum(email)}?d=${GRAVATAR_DEFAULT}`;
return `https://picvatar.lea.pet/generate/47882/${sha256sum(email)}?gravatar=1`;
}

View file

@ -10,7 +10,7 @@ export function sha256sum(input: any) {
export function isAdmin(session: Session | string | null) { export function isAdmin(session: Session | string | null) {
let email = typeof session == 'string' ? session : session?.user?.email; let email = typeof session == 'string' ? session : session?.user?.email;
return email && ["lea@amogus.cloud", "lexi@futacockinside.me", "wiki@amogus.cloud"].includes(email); // todo return email && ["lea@amogus.cloud", "lexi@futacockinside.me"].includes(email); // todo
} }
export function aliasesNeedApproval(session: Session | null) { export function aliasesNeedApproval(session: Session | null) {