mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-01-02 02:58:39 -05:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f48591685a | ||
|
|
cae1813084 | ||
|
|
74e18a8fb1 | ||
|
|
a89546200c | ||
|
|
a40f29d467 | ||
|
|
bcda120351 | ||
|
|
ad1ffd63d5 | ||
|
|
4b55a21d33 | ||
|
|
183548616e | ||
|
|
4938129367 | ||
|
|
984f5a2c52 | ||
|
|
5969a9d437 | ||
|
|
efbb64637d | ||
|
|
b460023911 | ||
|
|
c0e869a586 | ||
|
|
cd306ef878 | ||
|
|
1a40e31470 | ||
|
|
30f9199a7e | ||
|
|
e830b9c482 | ||
|
|
bc6b9da10b | ||
|
|
40991d879e |
14
.vscode/tasks.json
vendored
14
.vscode/tasks.json
vendored
@@ -57,6 +57,20 @@
|
||||
"cwd": "${workspaceFolder}/apps/server/Services/AliasVault.SmtpService"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch TaskRunner",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/server/Services/AliasVault.TaskRunner"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Client CSS",
|
||||
"type": "shell",
|
||||
|
||||
@@ -126,6 +126,7 @@ Core features that are being worked on:
|
||||
- [x] iOS native app
|
||||
- [x] Android native app
|
||||
- [x] Editing in browser extension
|
||||
- [x] Multi-language support across all client applications
|
||||
- [ ] Data model and usability improvements (more flexible aliases and credential types, folder support, bulk selecting etc.)
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
|
||||
- [ ] Adding support for family/team sharing (organization features)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.21.0",
|
||||
"version": "0.21.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -460,7 +460,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.21.0;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -479,7 +479,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -492,7 +492,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.21.0;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -515,7 +515,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -530,7 +530,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.21.0;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -554,7 +554,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -569,7 +569,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.21.0;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import '@/i18n/i18n';
|
||||
|
||||
import App from '@/entrypoints/popup/App';
|
||||
import { AuthProvider } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { DbProvider } from '@/entrypoints/popup/context/DbContext';
|
||||
@@ -10,19 +8,33 @@ import { LoadingProvider } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { ThemeProvider } from '@/entrypoints/popup/context/ThemeContext';
|
||||
import { WebApiProvider } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
<DbProvider>
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<LoadingProvider>
|
||||
<HeaderButtonsProvider>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</HeaderButtonsProvider>
|
||||
</LoadingProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
</DbProvider>
|
||||
);
|
||||
import i18n from '@/i18n/i18n';
|
||||
|
||||
/**
|
||||
* Renders the main application.
|
||||
*/
|
||||
const renderApp = (): void => {
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
<DbProvider>
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<LoadingProvider>
|
||||
<HeaderButtonsProvider>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</HeaderButtonsProvider>
|
||||
</LoadingProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
</DbProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// Wait for i18n to be ready before rendering React. Not waiting can cause issues on some browsers, Firefox on Windows specifically.
|
||||
if (i18n.isInitialized) {
|
||||
renderApp();
|
||||
} else {
|
||||
i18n.on('initialized', renderApp);
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
title={t('emails.emailContent')}
|
||||
/>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap text-gray-700 dark:text-gray-300">
|
||||
<pre className="whitespace-pre-wrap text-gray-800 p-3">
|
||||
{email.messagePlain}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
@@ -52,7 +52,11 @@ const initI18n = async (): Promise<void> => {
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize immediately
|
||||
initI18n();
|
||||
// Initialize immediately and handle potential errors
|
||||
initI18n().catch((error) => {
|
||||
console.error('Failed to initialize i18n:', error);
|
||||
// Even if initialization fails, emit initialized event to prevent app from hanging
|
||||
i18n.emit('initialized');
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -6,7 +6,7 @@ export class AppInfo {
|
||||
/**
|
||||
* The current extension version. This should be updated with each release of the extension.
|
||||
*/
|
||||
public static readonly VERSION = '0.21.0';
|
||||
public static readonly VERSION = '0.21.2';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
manifest: {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.21.0",
|
||||
version: "0.21.2",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||
@@ -93,8 +93,8 @@ android {
|
||||
applicationId 'net.aliasvault.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 11
|
||||
versionName "0.21.0"
|
||||
versionCode 12
|
||||
versionName "0.21.2"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "AliasVault",
|
||||
"slug": "AliasVault",
|
||||
"version": "0.21.0",
|
||||
"version": "0.21.2",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "net.aliasvault.app",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ActivityIndicator, View, Text, StyleSheet, TouchableOpacity, Linking, Pressable } from 'react-native';
|
||||
import { ActivityIndicator, View, Text, StyleSheet, TouchableOpacity, Linking, Pressable, Platform } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
@@ -49,19 +49,32 @@ export default function CredentialDetailsScreen() : React.ReactNode {
|
||||
*/
|
||||
headerRight: () => (
|
||||
<View style={styles.headerRightContainer}>
|
||||
<Pressable
|
||||
onPressIn={handleEdit}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="edit"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</Pressable>
|
||||
{Platform.OS === 'android' ? (
|
||||
<Pressable
|
||||
onPressIn={handleEdit}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="edit"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</Pressable>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={handleEdit}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="edit"
|
||||
size={22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -567,11 +567,11 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
*/
|
||||
headerRight: () => (
|
||||
<Pressable
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
style={[styles.headerRightButton, isSaveDisabled && styles.headerRightButtonDisabled]}
|
||||
onPressIn={handleSubmit(onSubmit)}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={[styles.headerRightButton, isSaveDisabled && styles.headerRightButtonDisabled]}
|
||||
disabled={isSaveDisabled}
|
||||
>
|
||||
<MaterialIcons name="save" size={24} color={colors.primary} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useNavigation, useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, AppState } from 'react-native';
|
||||
import { StyleSheet, View, TouchableOpacity, AppState, Platform, Pressable } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
@@ -80,14 +80,25 @@ export default function AutofillCredentialCreatedScreen() : React.ReactNode {
|
||||
/**
|
||||
* Header right button.
|
||||
*/
|
||||
headerRight: () => (
|
||||
<TouchableOpacity
|
||||
onPress={handleStayInApp}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>{t('common.cancel')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () =>
|
||||
Platform.OS === 'android' ? (
|
||||
<Pressable
|
||||
onPressIn={handleStayInApp}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>{t('common.cancel')}</ThemedText>
|
||||
</Pressable>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={handleStayInApp}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>{t('common.cancel')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
});
|
||||
}, [navigation, colors.primary, styles.headerRightButton, handleStayInApp, t]);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as FileSystem from 'expo-file-system';
|
||||
import { useLocalSearchParams, useRouter, useNavigation, Stack } from 'expo-router';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, ActivityIndicator, Alert, Share, useColorScheme, Linking, Text, TextInput, Platform } from 'react-native';
|
||||
import { StyleSheet, View, TouchableOpacity, ActivityIndicator, Alert, Share, useColorScheme, Linking, Text, TextInput, Platform, Pressable } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
@@ -324,22 +324,45 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
*/
|
||||
headerRight: () => (
|
||||
<View style={styles.headerRightContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => setHtmlView(!isHtmlView)}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons
|
||||
name={isHtmlView ? 'text-outline' : 'document-outline'}
|
||||
size={22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handleDelete}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={22} color="#FF0000" />
|
||||
</TouchableOpacity>
|
||||
{Platform.OS === 'android' ? (
|
||||
<>
|
||||
<Pressable
|
||||
onPressIn={() => setHtmlView(!isHtmlView)}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons
|
||||
name={isHtmlView ? 'text-outline' : 'document-outline'}
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPressIn={handleDelete}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={24} color="#FF0000" />
|
||||
</Pressable>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={() => setHtmlView(!isHtmlView)}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons
|
||||
name={isHtmlView ? 'text-outline' : 'document-outline'}
|
||||
size={22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handleDelete}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={22} color="#FF0000" />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -322,8 +322,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
useEffect(() => {
|
||||
const subscription = AppState.addEventListener('change', async (nextAppState) => {
|
||||
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
|
||||
// App coming to foreground
|
||||
if (!pathname?.includes('unlock') && !pathname?.includes('login')) {
|
||||
/**
|
||||
* App coming to foreground
|
||||
* Skip vault re-initialization checks during unlock, login, initialize, and reinitialize flows to prevent race conditions
|
||||
* where the AppState listener fires during app initialization, especially on iOS release builds.
|
||||
*/
|
||||
if (!pathname?.includes('unlock') && !pathname?.includes('login') && !pathname?.includes('initialize') && !pathname?.includes('reinitialize')) {
|
||||
try {
|
||||
// Check if vault is unlocked.
|
||||
const isUnlocked = await isVaultUnlocked();
|
||||
|
||||
@@ -1095,7 +1095,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@@ -1110,7 +1110,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.21.0;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1135,7 +1135,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
INFOPLIST_FILE = AliasVault/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
|
||||
@@ -1145,7 +1145,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.21.0;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1295,7 +1295,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1348,7 +1348,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1397,7 +1397,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1432,7 +1432,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1465,7 +1465,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1518,7 +1518,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1567,7 +1567,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1619,7 +1619,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1670,7 +1670,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1686,7 +1686,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 0.21.0;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
@@ -1715,7 +1715,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1731,7 +1731,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 0.21.0;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.app.autofill;
|
||||
|
||||
@@ -8,7 +8,7 @@ export class AppInfo {
|
||||
/**
|
||||
* The current mobile app version. This should be updated with each release of the mobile app.
|
||||
*/
|
||||
public static readonly VERSION = '0.21.0';
|
||||
public static readonly VERSION = '0.21.2';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,11 @@ public class ServerStatistics
|
||||
/// </summary>
|
||||
public List<TopUserByAliases> TopUsersByAliases { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of top users by number of emails.
|
||||
/// </summary>
|
||||
public List<TopUserByEmails> TopUsersByEmails { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of top IP addresses by user activity.
|
||||
/// </summary>
|
||||
|
||||
29
apps/server/AliasVault.Admin/Main/Models/TopUserByEmails.cs
Normal file
29
apps/server/AliasVault.Admin/Main/Models/TopUserByEmails.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TopUserByEmails.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Admin.Main.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Model representing a user with many stored emails.
|
||||
/// </summary>
|
||||
public class TopUserByEmails
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user ID.
|
||||
/// </summary>
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username.
|
||||
/// </summary>
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of emails.
|
||||
/// </summary>
|
||||
public int EmailCount { get; set; }
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
@using AliasVault.Admin.Services
|
||||
@using AliasVault.Admin.Main.Pages.Dashboard.Components
|
||||
@using AliasVault.RazorComponents.Tables
|
||||
@using AliasVault.RazorComponents
|
||||
@inherits MainBase
|
||||
@inject StatisticsService StatisticsService
|
||||
|
||||
@@ -86,7 +87,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Top Users Analysis -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="space-y-6">
|
||||
<!-- Top Users by Storage -->
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -112,6 +113,7 @@
|
||||
}
|
||||
</SortableTable>
|
||||
</div>
|
||||
<Paginator CurrentPage="_storageCurrentPage" PageSize="_pageSize" TotalRecords="_storageTotalRecords" OnPageChanged="HandleStoragePageChanged" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -146,6 +148,42 @@
|
||||
}
|
||||
</SortableTable>
|
||||
</div>
|
||||
<Paginator CurrentPage="_aliasCurrentPage" PageSize="_pageSize" TotalRecords="_aliasTotalRecords" OnPageChanged="HandleAliasPageChanged" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="px-6 py-8 flex justify-center">
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Top Users by Emails -->
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Top Users by Emails</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Users with the most emails stored in their aliases</p>
|
||||
</div>
|
||||
</div>
|
||||
@if (_topUsersByEmails != null)
|
||||
{
|
||||
<div class="overflow-x-auto">
|
||||
<SortableTable Columns="@_emailTableColumns">
|
||||
@foreach (var user in _topUsersByEmails)
|
||||
{
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<a href="users/@user.UserId" class="text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer">
|
||||
@user.Username
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 text-right">@user.EmailCount.ToString("N0")</td>
|
||||
</tr>
|
||||
}
|
||||
</SortableTable>
|
||||
</div>
|
||||
<Paginator CurrentPage="_emailCurrentPage" PageSize="_pageSize" TotalRecords="_emailTotalRecords" OnPageChanged="HandleEmailPageChanged" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -239,10 +277,20 @@
|
||||
// Detailed statistics (loaded separately)
|
||||
private List<TopUserByStorage>? _topUsersByStorage;
|
||||
private List<TopUserByAliases>? _topUsersByAliases;
|
||||
private List<TopUserByEmails>? _topUsersByEmails;
|
||||
private List<TopIpAddress>? _topIpAddresses;
|
||||
private bool _ipAddressesLoading = true;
|
||||
private bool _loadingError = false;
|
||||
|
||||
// Pagination variables
|
||||
private const int _pageSize = 10;
|
||||
private int _storageCurrentPage = 1;
|
||||
private int _aliasCurrentPage = 1;
|
||||
private int _emailCurrentPage = 1;
|
||||
private int _storageTotalRecords = 0;
|
||||
private int _aliasTotalRecords = 0;
|
||||
private int _emailTotalRecords = 0;
|
||||
|
||||
private readonly List<TableColumn> _storageTableColumns = new()
|
||||
{
|
||||
new() { Title = "User", PropertyName = "Username", Sortable = false },
|
||||
@@ -255,6 +303,12 @@
|
||||
new() { Title = "Aliases", PropertyName = "AliasCount", Sortable = false }
|
||||
};
|
||||
|
||||
private readonly List<TableColumn> _emailTableColumns = new()
|
||||
{
|
||||
new() { Title = "User", PropertyName = "Username", Sortable = false },
|
||||
new() { Title = "Emails", PropertyName = "EmailCount", Sortable = false }
|
||||
};
|
||||
|
||||
private readonly List<TableColumn> _ipTableColumns = new()
|
||||
{
|
||||
new() { Title = "IP Range", PropertyName = "IpAddress", Sortable = false },
|
||||
@@ -282,10 +336,19 @@
|
||||
_totalEmailAttachments = null;
|
||||
_topUsersByStorage = null;
|
||||
_topUsersByAliases = null;
|
||||
_topUsersByEmails = null;
|
||||
_topIpAddresses = null;
|
||||
_ipAddressesLoading = true;
|
||||
_loadingError = false;
|
||||
|
||||
// Reset pagination
|
||||
_storageCurrentPage = 1;
|
||||
_aliasCurrentPage = 1;
|
||||
_emailCurrentPage = 1;
|
||||
_storageTotalRecords = 0;
|
||||
_aliasTotalRecords = 0;
|
||||
_emailTotalRecords = 0;
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
// Reload statistics
|
||||
@@ -318,10 +381,28 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
var stats = await StatisticsService.GetServerStatisticsAsync();
|
||||
// Load paginated data for all three tables
|
||||
var storageTask = StatisticsService.GetTopUsersByStoragePaginatedAsync(_storageCurrentPage, _pageSize);
|
||||
var aliasTask = StatisticsService.GetTopUsersByAliasesPaginatedAsync(_aliasCurrentPage, _pageSize);
|
||||
var emailTask = StatisticsService.GetTopUsersByEmailsPaginatedAsync(_emailCurrentPage, _pageSize);
|
||||
var ipTask = StatisticsService.GetServerStatisticsAsync();
|
||||
|
||||
await Task.WhenAll(storageTask, aliasTask, emailTask, ipTask);
|
||||
|
||||
var (storageUsers, storageTotalCount) = await storageTask;
|
||||
var (aliasUsers, aliasTotalCount) = await aliasTask;
|
||||
var (emailUsers, emailTotalCount) = await emailTask;
|
||||
var stats = await ipTask;
|
||||
|
||||
_topUsersByStorage = storageUsers;
|
||||
_storageTotalRecords = storageTotalCount;
|
||||
|
||||
_topUsersByAliases = aliasUsers;
|
||||
_aliasTotalRecords = aliasTotalCount;
|
||||
|
||||
_topUsersByEmails = emailUsers;
|
||||
_emailTotalRecords = emailTotalCount;
|
||||
|
||||
_topUsersByStorage = stats.TopUsersByStorage;
|
||||
_topUsersByAliases = stats.TopUsersByAliases;
|
||||
_topIpAddresses = stats.TopIpAddresses;
|
||||
_ipAddressesLoading = false;
|
||||
|
||||
@@ -335,4 +416,79 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleStoragePageChanged(int newPage)
|
||||
{
|
||||
_storageCurrentPage = newPage;
|
||||
await LoadStoragePageData();
|
||||
}
|
||||
|
||||
private async Task HandleAliasPageChanged(int newPage)
|
||||
{
|
||||
_aliasCurrentPage = newPage;
|
||||
await LoadAliasPageData();
|
||||
}
|
||||
|
||||
private async Task HandleEmailPageChanged(int newPage)
|
||||
{
|
||||
_emailCurrentPage = newPage;
|
||||
await LoadEmailPageData();
|
||||
}
|
||||
|
||||
private async Task LoadStoragePageData()
|
||||
{
|
||||
try
|
||||
{
|
||||
_topUsersByStorage = null;
|
||||
StateHasChanged();
|
||||
|
||||
var (users, totalCount) = await StatisticsService.GetTopUsersByStoragePaginatedAsync(_storageCurrentPage, _pageSize);
|
||||
_topUsersByStorage = users;
|
||||
_storageTotalRecords = totalCount;
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage($"Error loading storage statistics: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAliasPageData()
|
||||
{
|
||||
try
|
||||
{
|
||||
_topUsersByAliases = null;
|
||||
StateHasChanged();
|
||||
|
||||
var (users, totalCount) = await StatisticsService.GetTopUsersByAliasesPaginatedAsync(_aliasCurrentPage, _pageSize);
|
||||
_topUsersByAliases = users;
|
||||
_aliasTotalRecords = totalCount;
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage($"Error loading alias statistics: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadEmailPageData()
|
||||
{
|
||||
try
|
||||
{
|
||||
_topUsersByEmails = null;
|
||||
StateHasChanged();
|
||||
|
||||
var (users, totalCount) = await StatisticsService.GetTopUsersByEmailsPaginatedAsync(_emailCurrentPage, _pageSize);
|
||||
_topUsersByEmails = users;
|
||||
_emailTotalRecords = totalCount;
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage($"Error loading email statistics: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@using AliasVault.RazorComponents.Tables
|
||||
|
||||
<div class="mb-4">
|
||||
<Button Color="danger" OnClick="RevokeAllTokens">Revoke All Tokens</Button>
|
||||
<Button Color="danger" OnClick="RevokeAllTokens">Revoke all sessions</Button>
|
||||
</div>
|
||||
|
||||
<SortableTable Columns="@_refreshTokenTableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<SortableTableRow>
|
||||
<SortableTableColumn IsPrimary="true">@entry.Id</SortableTableColumn>
|
||||
<SortableTableColumn>@entry.CreatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>@entry.UpdatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>@Math.Round((double)entry.FileSize / 1024, 1) MB</SortableTableColumn>
|
||||
<SortableTableColumn>@entry.Version</SortableTableColumn>
|
||||
<SortableTableColumn>@entry.Client</SortableTableColumn>
|
||||
@@ -54,7 +53,6 @@
|
||||
private readonly List<TableColumn> _vaultTableColumns = [
|
||||
new TableColumn { Title = "ID", PropertyName = "Id" },
|
||||
new TableColumn { Title = "Created", PropertyName = "CreatedAt" },
|
||||
new TableColumn { Title = "Updated", PropertyName = "UpdatedAt" },
|
||||
new TableColumn { Title = "Filesize", PropertyName = "FileSize" },
|
||||
new TableColumn { Title = "DB version", PropertyName = "Version" },
|
||||
new TableColumn { Title = "Client", PropertyName = "Client" },
|
||||
|
||||
@@ -75,7 +75,7 @@ else
|
||||
<div class="w-full mb-4 overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<tbody>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<tr class="">
|
||||
<th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">Registered at</th>
|
||||
<td class="px-4 py-3">@User.CreatedAt.ToString("yyyy-MM-dd HH:mm")</td>
|
||||
</tr>
|
||||
@@ -115,6 +115,52 @@ else
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">Email Limits</th>
|
||||
<td class="px-4 py-3">
|
||||
@if (!IsEditingEmailLimits)
|
||||
{
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
<strong>Max Emails:</strong> @(User.MaxEmails == 0 ? "Unlimited" : User.MaxEmails.ToString("N0"))
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
<strong>Max Age:</strong> @(User.MaxEmailAgeDays == 0 ? "Unlimited" : User.MaxEmailAgeDays + " days")
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<Button Color="primary" OnClick="@(() => StartEditingEmailLimits())">Edit Limits</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="space-y-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div>
|
||||
<label class="block mb-1 text-sm font-medium text-gray-900 dark:text-white">Max Emails</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" min="0" @bind="EditMaxEmails" class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-32 p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">(0 = unlimited)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1 text-sm font-medium text-gray-900 dark:text-white">Max Email Age</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" min="0" @bind="EditMaxEmailAgeDays" class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-32 p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">days (0 = unlimited)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2 pt-2">
|
||||
<Button Color="success" OnClick="SaveEmailLimits">Save</Button>
|
||||
<Button Color="secondary" OnClick="CancelEditingEmailLimits">Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -135,7 +181,7 @@ else
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="items-center">
|
||||
<div>
|
||||
<h3 class="mb-1 text-xl font-bold text-gray-900 dark:text-white">UserRefreshTokens (Logged in devices)</h3>
|
||||
<h3 class="mb-1 text-xl font-bold text-gray-900 dark:text-white">Logged in devices</h3>
|
||||
|
||||
<RefreshTokenTable RefreshTokenList="@RefreshTokenList" OnRevokeToken="@RevokeRefreshToken" OnRevokeAllTokens="@RevokeAllTokens" />
|
||||
</div>
|
||||
@@ -185,6 +231,9 @@ else
|
||||
private List<Vault> VaultList { get; set; } = [];
|
||||
private List<AuthLog> AuthLogList { get; set; } = [];
|
||||
private UserUsageStatistics? UserUsageStats { get; set; }
|
||||
private bool IsEditingEmailLimits { get; set; }
|
||||
private int EditMaxEmails { get; set; }
|
||||
private int EditMaxEmailAgeDays { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -427,4 +476,43 @@ Do you want to proceed with the restoration?")) {
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts editing email limits for the user.
|
||||
/// </summary>
|
||||
private void StartEditingEmailLimits()
|
||||
{
|
||||
IsEditingEmailLimits = true;
|
||||
EditMaxEmails = User!.MaxEmails;
|
||||
EditMaxEmailAgeDays = User.MaxEmailAgeDays;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels editing email limits.
|
||||
/// </summary>
|
||||
private void CancelEditingEmailLimits()
|
||||
{
|
||||
IsEditingEmailLimits = false;
|
||||
EditMaxEmails = 0;
|
||||
EditMaxEmailAgeDays = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the email limits for the user.
|
||||
/// </summary>
|
||||
private async Task SaveEmailLimits()
|
||||
{
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
User = await dbContext.AliasVaultUsers.FindAsync(Id);
|
||||
|
||||
if (User != null)
|
||||
{
|
||||
User.MaxEmails = EditMaxEmails;
|
||||
User.MaxEmailAgeDays = EditMaxEmailAgeDays;
|
||||
await dbContext.SaveChangesAsync();
|
||||
IsEditingEmailLimits = false;
|
||||
await RefreshData();
|
||||
GlobalNotificationService.AddSuccessMessage("Email limits updated successfully.", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,9 @@ public class StatisticsService
|
||||
stats.TotalEmailAttachments = results[3];
|
||||
|
||||
// Get top users data
|
||||
stats.TopUsersByStorage = await GetTopUsersByStorageAsync();
|
||||
stats.TopUsersByAliases = await GetTopUsersByAliasesAsync();
|
||||
stats.TopUsersByStorage = await GetTopUsersByStorageAsync(10);
|
||||
stats.TopUsersByAliases = await GetTopUsersByAliasesAsync(10);
|
||||
stats.TopUsersByEmails = await GetTopUsersByEmailsAsync(10);
|
||||
stats.TopIpAddresses = await GetTopIpAddressesAsync();
|
||||
|
||||
return stats;
|
||||
@@ -115,6 +116,126 @@ public class StatisticsService
|
||||
return stats;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets paginated top users by storage size.
|
||||
/// </summary>
|
||||
/// <param name="page">Page number (1-based).</param>
|
||||
/// <param name="pageSize">Number of items per page.</param>
|
||||
/// <returns>Paginated list of top users by storage with total count.</returns>
|
||||
public async Task<(List<TopUserByStorage> Users, int TotalCount)> GetTopUsersByStoragePaginatedAsync(int page, int pageSize)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
// Get total count
|
||||
var totalCount = await context.Vaults
|
||||
.GroupBy(v => v.UserId)
|
||||
.CountAsync();
|
||||
|
||||
// Get paginated data
|
||||
var topUsers = await context.Vaults
|
||||
.GroupBy(v => v.UserId)
|
||||
.Select(g => new
|
||||
{
|
||||
UserId = g.Key,
|
||||
Username = g.First().User.UserName,
|
||||
TotalStorageBytes = g.OrderByDescending(v => v.Version).First().FileSize,
|
||||
})
|
||||
.OrderByDescending(u => u.TotalStorageBytes)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var users = topUsers.Select(u => new TopUserByStorage
|
||||
{
|
||||
UserId = u.UserId,
|
||||
Username = u.Username ?? UnknownUsername,
|
||||
StorageBytes = u.TotalStorageBytes,
|
||||
StorageDisplaySize = FormatKilobytes(u.TotalStorageBytes),
|
||||
}).ToList();
|
||||
|
||||
return (users, totalCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets paginated top users by number of aliases.
|
||||
/// </summary>
|
||||
/// <param name="page">Page number (1-based).</param>
|
||||
/// <param name="pageSize">Number of items per page.</param>
|
||||
/// <returns>Paginated list of top users by aliases with total count.</returns>
|
||||
public async Task<(List<TopUserByAliases> Users, int TotalCount)> GetTopUsersByAliasesPaginatedAsync(int page, int pageSize)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
// Get total count
|
||||
var totalCount = await context.UserEmailClaims
|
||||
.Where(uec => uec.UserId != null)
|
||||
.GroupBy(uec => uec.UserId)
|
||||
.CountAsync();
|
||||
|
||||
// Get paginated data
|
||||
var topUsers = await context.UserEmailClaims
|
||||
.Where(uec => uec.UserId != null)
|
||||
.GroupBy(uec => uec.UserId)
|
||||
.Select(g => new
|
||||
{
|
||||
UserId = g.Key,
|
||||
Username = g.First().User!.UserName,
|
||||
AliasCount = g.Count(),
|
||||
})
|
||||
.OrderByDescending(u => u.AliasCount)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var users = topUsers.Select(u => new TopUserByAliases
|
||||
{
|
||||
UserId = u.UserId!,
|
||||
Username = u.Username ?? UnknownUsername,
|
||||
AliasCount = u.AliasCount,
|
||||
}).ToList();
|
||||
|
||||
return (users, totalCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets paginated top users by number of emails.
|
||||
/// </summary>
|
||||
/// <param name="page">Page number (1-based).</param>
|
||||
/// <param name="pageSize">Number of items per page.</param>
|
||||
/// <returns>Paginated list of top users by emails with total count.</returns>
|
||||
public async Task<(List<TopUserByEmails> Users, int TotalCount)> GetTopUsersByEmailsPaginatedAsync(int page, int pageSize)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
// Get total count
|
||||
var totalCount = await context.Emails
|
||||
.GroupBy(e => e.EncryptionKey.UserId)
|
||||
.CountAsync();
|
||||
|
||||
// Get paginated data
|
||||
var topUsers = await context.Emails
|
||||
.GroupBy(e => e.EncryptionKey.UserId)
|
||||
.Select(g => new
|
||||
{
|
||||
UserId = g.Key,
|
||||
Username = g.First().EncryptionKey.User!.UserName,
|
||||
EmailCount = g.Count(),
|
||||
})
|
||||
.OrderByDescending(u => u.EmailCount)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var users = topUsers.Select(u => new TopUserByEmails
|
||||
{
|
||||
UserId = u.UserId!,
|
||||
Username = u.Username ?? UnknownUsername,
|
||||
EmailCount = u.EmailCount,
|
||||
}).ToList();
|
||||
|
||||
return (users, totalCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets user-specific usage statistics for both all-time and recent periods.
|
||||
/// </summary>
|
||||
@@ -267,10 +388,11 @@ public class StatisticsService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the top 10 users by vault storage size.
|
||||
/// Gets the top users by vault storage size.
|
||||
/// </summary>
|
||||
/// <param name="limit">Number of top users to retrieve.</param>
|
||||
/// <returns>List of top users by storage.</returns>
|
||||
private async Task<List<TopUserByStorage>> GetTopUsersByStorageAsync()
|
||||
private async Task<List<TopUserByStorage>> GetTopUsersByStorageAsync(int limit = 10)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
@@ -284,7 +406,7 @@ public class StatisticsService
|
||||
TotalStorageBytes = g.OrderByDescending(v => v.Version).First().FileSize,
|
||||
})
|
||||
.OrderByDescending(u => u.TotalStorageBytes)
|
||||
.Take(10)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
|
||||
return topUsers.Select(u => new TopUserByStorage
|
||||
@@ -297,10 +419,11 @@ public class StatisticsService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the top 10 users by number of email aliases.
|
||||
/// Gets the top users by number of email aliases.
|
||||
/// </summary>
|
||||
/// <param name="limit">Number of top users to retrieve.</param>
|
||||
/// <returns>List of top users by aliases.</returns>
|
||||
private async Task<List<TopUserByAliases>> GetTopUsersByAliasesAsync()
|
||||
private async Task<List<TopUserByAliases>> GetTopUsersByAliasesAsync(int limit = 10)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync();
|
||||
var topUsers = await context.UserEmailClaims
|
||||
@@ -313,7 +436,7 @@ public class StatisticsService
|
||||
AliasCount = g.Count(),
|
||||
})
|
||||
.OrderByDescending(u => u.AliasCount)
|
||||
.Take(10)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
|
||||
return topUsers.Select(u => new TopUserByAliases
|
||||
@@ -324,6 +447,34 @@ public class StatisticsService
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the top users by number of emails stored.
|
||||
/// </summary>
|
||||
/// <param name="limit">Number of top users to retrieve.</param>
|
||||
/// <returns>List of top users by emails.</returns>
|
||||
private async Task<List<TopUserByEmails>> GetTopUsersByEmailsAsync(int limit = 10)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync();
|
||||
var topUsers = await context.Emails
|
||||
.GroupBy(e => e.EncryptionKey.UserId)
|
||||
.Select(g => new
|
||||
{
|
||||
UserId = g.Key,
|
||||
Username = g.First().EncryptionKey.User!.UserName,
|
||||
EmailCount = g.Count(),
|
||||
})
|
||||
.OrderByDescending(u => u.EmailCount)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
|
||||
return topUsers.Select(u => new TopUserByEmails
|
||||
{
|
||||
UserId = u.UserId!,
|
||||
Username = u.Username ?? UnknownUsername,
|
||||
EmailCount = u.EmailCount,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the top 10 IP address ranges by number of associated user accounts.
|
||||
/// Only includes non-anonymized IPs (not "xxx.xxx.xxx.xxx").
|
||||
|
||||
@@ -759,10 +759,6 @@ video {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mb-8 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.line-clamp-1 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
@@ -882,10 +878,18 @@ video {
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
@@ -922,14 +926,6 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
@@ -1100,6 +1096,12 @@ video {
|
||||
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
@@ -1418,11 +1420,6 @@ video {
|
||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-purple-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 232 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -1439,6 +1436,10 @@ video {
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
.p-3 {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -1578,6 +1579,10 @@ video {
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.pt-6 {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
@@ -1662,10 +1667,6 @@ video {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -1678,6 +1679,11 @@ video {
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.text-blue-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(59 130 246 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(37 99 235 / var(--tw-text-opacity));
|
||||
@@ -1728,9 +1734,9 @@ video {
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
.text-green-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(22 163 74 / var(--tw-text-opacity));
|
||||
color: rgb(34 197 94 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-green-800 {
|
||||
@@ -1748,11 +1754,6 @@ video {
|
||||
color: rgb(184 112 47 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-purple-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(147 51 234 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(248 113 113 / var(--tw-text-opacity));
|
||||
@@ -1788,16 +1789,6 @@ video {
|
||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-green-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(34 197 94 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(59 130 246 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -1957,11 +1948,6 @@ video {
|
||||
color: rgb(154 93 38 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-blue-800:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 64 175 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-red-900:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(127 29 29 / var(--tw-text-opacity));
|
||||
@@ -2192,19 +2178,6 @@ video {
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-900\/50:is(.dark *) {
|
||||
background-color: rgb(17 24 39 / 0.5);
|
||||
}
|
||||
|
||||
.dark\:bg-purple-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(88 28 135 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-800\/10:is(.dark *) {
|
||||
background-color: rgb(153 27 27 / 0.1);
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
@@ -2244,11 +2217,6 @@ video {
|
||||
color: rgb(75 85 99 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-green-200:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(187 247 208 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-green-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(134 239 172 / var(--tw-text-opacity));
|
||||
@@ -2269,11 +2237,6 @@ video {
|
||||
color: rgb(244 149 65 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-purple-400:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(192 132 252 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-100:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 226 226 / var(--tw-text-opacity));
|
||||
@@ -2309,16 +2272,6 @@ video {
|
||||
color: rgb(254 240 138 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-500:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(107 114 128 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-purple-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(216 180 254 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
|
||||
--tw-placeholder-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
|
||||
@@ -2378,21 +2331,16 @@ video {
|
||||
color: rgb(244 149 65 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-white:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-blue-300:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(147 197 253 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-red-300:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(252 165 165 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-white:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-blue-500:focus:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
@@ -2625,16 +2573,16 @@ video {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.lg\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
.lg\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:grid-cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
.lg\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:flex-row {
|
||||
|
||||
@@ -250,7 +250,9 @@
|
||||
else if (Email.MessagePlain is not null)
|
||||
{
|
||||
// No HTML but plain text is available
|
||||
EmailBody = Email.MessagePlain;
|
||||
// Escape HTML entities and wrap in pre tag to preserve formatting
|
||||
var escapedText = System.Net.WebUtility.HtmlEncode(Email.MessagePlain);
|
||||
EmailBody = $"<pre style='font-family: system-ui, -apple-system, sans-serif; white-space: pre-wrap; word-wrap: break-word; margin: 0;'>{escapedText}</pre>";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -270,7 +270,9 @@
|
||||
else if (Email.MessagePlain is not null)
|
||||
{
|
||||
// No HTML but plain text is available
|
||||
EmailBody = Email.MessagePlain;
|
||||
// Escape HTML entities and wrap in pre tag to preserve formatting
|
||||
var escapedText = System.Net.WebUtility.HtmlEncode(Email.MessagePlain);
|
||||
EmailBody = $"<pre style='font-family: system-ui, -apple-system, sans-serif; white-space: pre-wrap; word-wrap: break-word; margin: 0;'>{escapedText}</pre>";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -800,42 +800,25 @@ public sealed class DbService : IDisposable
|
||||
/// <returns>Task.</returns>
|
||||
private async Task VaultCleanupSoftDeletedRecords()
|
||||
{
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-7);
|
||||
var deleteCount = 0;
|
||||
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-7);
|
||||
var softDeletedCredentials = await _dbContext.Credentials
|
||||
// Hard delete soft-deleted Credentials older than 7 days
|
||||
deleteCount += await _dbContext.Credentials
|
||||
.Where(c => c.IsDeleted && c.UpdatedAt <= cutoffDate)
|
||||
.ToListAsync();
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
// Hard delete all soft-deleted credentials that are older than 7 days.
|
||||
foreach (var credential in softDeletedCredentials)
|
||||
{
|
||||
var login = await _dbContext.Credentials
|
||||
.Where(x => x.Id == credential.Id)
|
||||
.FirstAsync();
|
||||
_dbContext.Credentials.Remove(login);
|
||||
|
||||
deleteCount++;
|
||||
}
|
||||
|
||||
// Attachments
|
||||
var softDeletedAttachments = await _dbContext.Attachments
|
||||
// Hard delete soft-deleted Attachments older than 7 days
|
||||
deleteCount += await _dbContext.Attachments
|
||||
.Where(a => a.IsDeleted && a.UpdatedAt <= cutoffDate)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var attachment in softDeletedAttachments)
|
||||
{
|
||||
_dbContext.Attachments.Remove(attachment);
|
||||
deleteCount++;
|
||||
}
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
if (deleteCount > 0)
|
||||
{
|
||||
// Save the database to the server to persist the cleanup.
|
||||
var success = await SaveDatabaseAsync();
|
||||
if (!success)
|
||||
{
|
||||
throw new DataException("Error saving database to server after attachment deletion.");
|
||||
throw new DataException("Error saving database to server after record deletion.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,16 @@ public class AliasVaultUser : IdentityUser
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of emails for all of user's aliases. 0 means unlimited.
|
||||
/// </summary>
|
||||
public int MaxEmails { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum age of emails in days. Emails older than this will be deleted. 0 means unlimited.
|
||||
/// </summary>
|
||||
public int MaxEmailAgeDays { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of vaults.
|
||||
/// </summary>
|
||||
|
||||
926
apps/server/Databases/AliasServerDb/Migrations/20250804182808_AddPerUserEmailLimits.Designer.cs
generated
Normal file
926
apps/server/Databases/AliasServerDb/Migrations/20250804182808_AddPerUserEmailLimits.Designer.cs
generated
Normal file
@@ -0,0 +1,926 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasServerDbContext))]
|
||||
[Migration("20250804182808_AddPerUserEmailLimits")]
|
||||
partial class AddPerUserEmailLimits
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.4")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true)
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AdminRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime?>("LastPasswordChanged")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AdminUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("Blocked")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("MaxEmailAgeDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("MaxEmails")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("PasswordChangedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DeviceIdentifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("ExpireDate")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("character varying(45)");
|
||||
|
||||
b.Property<string>("PreviousTokenValue")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AliasVaultUserRefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AuthLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AdditionalInfo")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Browser")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("DeviceType")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("EventType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("FailureReason")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("IsSuccess")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsSuspiciousActivity")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("OperatingSystem")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("RequestPath")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "EventType" }, "IX_EventType");
|
||||
|
||||
b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress");
|
||||
|
||||
b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp");
|
||||
|
||||
b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp")
|
||||
.IsDescending(false, false, true);
|
||||
|
||||
b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("AuthLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("DateSystem")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("EncryptedSymmetricKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("From")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("FromDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("FromLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessageHtml")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessagePlain")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessagePreview")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessageSource")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PushNotificationSent")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("To")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ToDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ToLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserEncryptionKeyId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Visible")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.HasIndex("DateSystem");
|
||||
|
||||
b.HasIndex("PushNotificationSent");
|
||||
|
||||
b.HasIndex("ToLocal");
|
||||
|
||||
b.HasIndex("UserEncryptionKeyId");
|
||||
|
||||
b.HasIndex("Visible");
|
||||
|
||||
b.ToTable("Emails");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Bytes")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("EmailId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Filesize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmailId");
|
||||
|
||||
b.ToTable("EmailAttachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Log", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Application")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Exception")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Level")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("LogEvent")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("LogEvent");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessageTemplate")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceContext")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("TimeStamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Application");
|
||||
|
||||
b.HasIndex("TimeStamp");
|
||||
|
||||
b.ToTable("Logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsOnDemand")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("RunDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<TimeOnly>("StartTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TaskRunnerJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("AddressDomain")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("AddressLocal")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Address")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEmailClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEncryptionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("CredentialsCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("EmailClaimsCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("EncryptionSettings")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EncryptionType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("FileSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("RevisionNumber")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Salt")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("VaultBlob")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Verifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CurrentStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("DesiredStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("Heartbeat")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkerServiceStatuses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("FriendlyName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Xml")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("DataProtectionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("RoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.ToTable("UserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.ToTable("UserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("UserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey")
|
||||
.WithMany("Emails")
|
||||
.HasForeignKey("UserEncryptionKeyId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("EncryptionKey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.Email", "Email")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("EmailId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Email");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("EmailClaims")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("EncryptionKeys")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("Vaults")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Navigation("EmailClaims");
|
||||
|
||||
b.Navigation("EncryptionKeys");
|
||||
|
||||
b.Navigation("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Navigation("Emails");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPerUserEmailLimits : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaxEmailAgeDays",
|
||||
table: "AliasVaultUsers",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaxEmails",
|
||||
table: "AliasVaultUsers",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxEmailAgeDays",
|
||||
table: "AliasVaultUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxEmails",
|
||||
table: "AliasVaultUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace AliasServerDb.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("ProductVersion", "9.0.4")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true)
|
||||
@@ -147,6 +147,12 @@ namespace AliasServerDb.Migrations
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("MaxEmailAgeDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("MaxEmails")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("text");
|
||||
|
||||
@@ -1,9 +1,61 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Usage: ./sendEmailCLI.sh [port]
|
||||
# Example: ./sendEmailCLI.sh 2525
|
||||
# Default port is 25 if not specified
|
||||
|
||||
generate_random_string() {
|
||||
LC_ALL=C tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w ${1:-10} | head -n 1
|
||||
}
|
||||
|
||||
generate_random_special_chars() {
|
||||
# Generate random special characters and symbols
|
||||
local length=${1:-10}
|
||||
local special_chars="!@#$%^&*()_+-=[]{}|;':,./<>?~¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"
|
||||
local result=""
|
||||
for ((i=0; i<length; i++)); do
|
||||
local random_index=$((RANDOM % ${#special_chars}))
|
||||
result+="${special_chars:$random_index:1}"
|
||||
done
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
generate_random_emoji() {
|
||||
# Generate random emoji characters
|
||||
local emojis="😀😃😄😁😆😅😂🤣😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶😱😨😰😥😓🤗🤔🤭🤫🤥😶😐😑😯😦😧😮😲🥱😴🤤😪😵🤐🥴🤢🤮🤧😷🤒🤕🤑🤠"
|
||||
local length=${1:-3}
|
||||
local result=""
|
||||
for ((i=0; i<length; i++)); do
|
||||
local random_index=$((RANDOM % ${#emojis}))
|
||||
result+="${emojis:$random_index:1}"
|
||||
done
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
generate_random_unicode_subject() {
|
||||
# Generate random Unicode characters for subject line
|
||||
local length=${1:-4}
|
||||
local unicode_chars="你好世界🌍🚀🎉🎊🎈🎂🎁∑∏∫√∞≠≈≤≥€£¥₹₿éèêëàáâäùúûüçñ"
|
||||
local result=""
|
||||
for ((i=0; i<length; i++)); do
|
||||
local random_index=$((RANDOM % ${#unicode_chars}))
|
||||
result+="${unicode_chars:$random_index:1}"
|
||||
done
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
generate_random_chinese() {
|
||||
# Generate random Chinese characters
|
||||
local length=${1:-8}
|
||||
local chinese_chars="你好世界测试文字中文邮件内容随机字符"
|
||||
local result=""
|
||||
for ((i=0; i<length; i++)); do
|
||||
local random_index=$((RANDOM % ${#chinese_chars}))
|
||||
result+="${chinese_chars:$random_index:1}"
|
||||
done
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
generate_random_attachment() {
|
||||
local temp_file="/tmp/test_attachment_$(generate_random_string 8).txt"
|
||||
echo "This is a test attachment content - $(generate_random_string 32)" > "$temp_file"
|
||||
@@ -11,6 +63,7 @@ generate_random_attachment() {
|
||||
}
|
||||
|
||||
print_logo() {
|
||||
local port="${1:-25}"
|
||||
printf "${MAGENTA}\n"
|
||||
printf "=========================================================\n"
|
||||
printf " _ _ __ __ _ _ \n"
|
||||
@@ -23,71 +76,287 @@ print_logo() {
|
||||
printf " Email sender DevTool\n"
|
||||
printf "=========================================================\n"
|
||||
printf "This tool sends an email to the recipient of your choice\n"
|
||||
printf "and delivers it to the local SMTP server running on localhost:25.\n"
|
||||
printf "and delivers it to the local SMTP server running on localhost:$port.\n"
|
||||
printf "${NC}\n"
|
||||
}
|
||||
|
||||
generate_plain_body() {
|
||||
local email_number="$1"
|
||||
local content_suffix="$2"
|
||||
local chinese_text="$3"
|
||||
local special_chars="$4"
|
||||
local emoji_text="$5"
|
||||
local random_unicode="$6"
|
||||
local with_attachment="$7"
|
||||
|
||||
local opening_text="This is test email #$email_number"
|
||||
if [[ "$with_attachment" == "true" ]]; then
|
||||
opening_text="This is test email #$email_number with attachment"
|
||||
fi
|
||||
|
||||
# Use printf for consistent newline handling
|
||||
printf "%s\r\n" "$opening_text"
|
||||
printf "\r\n"
|
||||
printf "Random content: %s\r\n" "$content_suffix"
|
||||
printf "\r\n"
|
||||
printf "=== Special Character Testing ===\r\n"
|
||||
printf "Special symbols: %s\r\n" "$special_chars"
|
||||
printf "\r\n"
|
||||
printf "Emoji test: %s\r\n" "$emoji_text"
|
||||
printf "\r\n"
|
||||
printf "Mixed Unicode: %s\r\n" "$random_unicode"
|
||||
printf "\r\n"
|
||||
printf "Testing various character encodings:\r\n"
|
||||
printf "• Latin: Hello World\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Chinese: 你好世界\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Japanese: こんにちは世界\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Korean: 안녕하세요 세계\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Arabic: مرحبا بالعالم\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Cyrillic: Привет мир\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Greek: Γεια σου κόσμε\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Thai: สวัสดีชาวโลก\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Emoji: 🎉🎊🎈🎂🎁\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Math symbols: ∑∏∫√∞≠≈≤≥\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Currency: €£¥₹₿\r\n"
|
||||
printf "\r\n"
|
||||
printf "• Accents: éèêëàáâäùúûüçñ\r\n"
|
||||
}
|
||||
|
||||
generate_html_body() {
|
||||
local email_number="$1"
|
||||
local content_suffix="$2"
|
||||
local chinese_text="$3"
|
||||
local special_chars="$4"
|
||||
local emoji_text="$5"
|
||||
local random_unicode="$6"
|
||||
local with_attachment="$7"
|
||||
|
||||
local opening_text="This is test email #$email_number"
|
||||
if [[ "$with_attachment" == "true" ]]; then
|
||||
opening_text="This is test email #$email_number with attachment"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
h2 { color: #0066cc; }
|
||||
.section { margin: 20px 0; padding: 15px; background-color: #f4f4f4; border-radius: 5px; }
|
||||
.emoji { font-size: 24px; }
|
||||
.special-chars { background-color: #ffffcc; padding: 5px; }
|
||||
ul { list-style-type: none; }
|
||||
li { margin: 5px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>$opening_text</h1>
|
||||
|
||||
<div class="section">
|
||||
<p><strong>Random content:</strong> $content_suffix</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Special Character Testing</h2>
|
||||
<p class="special-chars"><strong>Special symbols:</strong> $special_chars</p>
|
||||
<p class="emoji"><strong>Emoji test:</strong> $emoji_text</p>
|
||||
<p><strong>Mixed Unicode:</strong> $random_unicode</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Testing various character encodings:</h2>
|
||||
<ul>
|
||||
<li>• <strong>Latin:</strong> Hello World</li>
|
||||
<li>• <strong>Chinese:</strong> 你好世界</li>
|
||||
<li>• <strong>Japanese:</strong> こんにちは世界</li>
|
||||
<li>• <strong>Korean:</strong> 안녕하세요 세계</li>
|
||||
<li>• <strong>Arabic:</strong> مرحبا بالعالم</li>
|
||||
<li>• <strong>Cyrillic:</strong> Привет мир</li>
|
||||
<li>• <strong>Greek:</strong> Γεια σου κόσμε</li>
|
||||
<li>• <strong>Thai:</strong> สวัสดีชาวโลก</li>
|
||||
<li>• <strong>Emoji:</strong> <span class="emoji">🎉🎊🎈🎂🎁</span></li>
|
||||
<li>• <strong>Math symbols:</strong> ∑∏∫√∞≠≈≤≥</li>
|
||||
<li>• <strong>Currency:</strong> €£¥₹₿</li>
|
||||
<li>• <strong>Accents:</strong> éèêëàáâäùúûüçñ</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
}
|
||||
|
||||
# Generate email headers
|
||||
generate_headers() {
|
||||
local recipient="$1"
|
||||
local subject="$2"
|
||||
local content_type="$3"
|
||||
local boundary="$4"
|
||||
|
||||
printf "From: sender@example.com\r\n"
|
||||
printf "To: %s\r\n" "$recipient"
|
||||
printf "Subject: %s\r\n" "$subject"
|
||||
printf "MIME-Version: 1.0\r\n"
|
||||
|
||||
if [[ -n "$boundary" ]]; then
|
||||
printf "Content-Type: multipart/mixed; boundary=%s\r\n" "$boundary"
|
||||
else
|
||||
printf "Content-Type: %s; charset=utf-8\r\n" "$content_type"
|
||||
printf "Content-Transfer-Encoding: 8bit\r\n"
|
||||
fi
|
||||
printf "\r\n"
|
||||
}
|
||||
|
||||
# Generate random content for email
|
||||
generate_random_content() {
|
||||
echo "$(generate_random_string 20)"
|
||||
}
|
||||
|
||||
# Send email with configurable options
|
||||
send_email() {
|
||||
local recipient="$1"
|
||||
local with_attachment="$2"
|
||||
local email_type="$2"
|
||||
local smtp_port="$3"
|
||||
local email_number="$4"
|
||||
|
||||
# Generate common random elements
|
||||
local subject_suffix=$(generate_random_string 8)
|
||||
local content_suffix=$(generate_random_string 20)
|
||||
local boundary="boundary-$(generate_random_string 16)"
|
||||
local attachment_content="This is a test attachment content - $(generate_random_string 32)"
|
||||
local attachment_name="test_attachment_$(generate_random_string 8).txt"
|
||||
|
||||
if [[ "$with_attachment" =~ ^[Yy]$ ]]; then
|
||||
local content_suffix=$(generate_random_content)
|
||||
local special_chars=$(generate_random_special_chars 15)
|
||||
local emoji_text=$(generate_random_emoji 4)
|
||||
local random_unicode="Unicode test: 你好世界 🌍 测试文字 🚀"
|
||||
local subject_unicode=$(generate_random_unicode_subject 6)
|
||||
local chinese_text=$(generate_random_chinese 8)
|
||||
|
||||
# Determine email properties based on type
|
||||
local with_attachment="false"
|
||||
local is_html="false"
|
||||
local content_type="text/plain"
|
||||
|
||||
case "$email_type" in
|
||||
2) with_attachment="true" ;;
|
||||
3) is_html="true"; content_type="text/html" ;;
|
||||
4) with_attachment="true"; is_html="true"; content_type="text/html" ;;
|
||||
esac
|
||||
|
||||
# Build subject line
|
||||
local subject="Test Email #$email_number"
|
||||
[[ "$with_attachment" == "true" ]] && subject="$subject with Attachment"
|
||||
subject="$subject $subject_unicode - $subject_suffix"
|
||||
|
||||
# Handle emails with attachments
|
||||
if [[ "$with_attachment" == "true" ]]; then
|
||||
local boundary="boundary-$(generate_random_string 16)"
|
||||
local attachment_content="This is a test attachment content - $(generate_random_string 32)"
|
||||
local attachment_name="test_attachment_$(generate_random_string 8).txt"
|
||||
|
||||
{
|
||||
echo "From: sender@example.com"
|
||||
echo "To: $recipient"
|
||||
echo "Subject: Test Email with Attachment - $subject_suffix"
|
||||
echo "MIME-Version: 1.0"
|
||||
echo "Content-Type: multipart/mixed; boundary=$boundary"
|
||||
echo ""
|
||||
echo "--$boundary"
|
||||
echo "Content-Type: text/plain; charset=utf-8"
|
||||
echo ""
|
||||
echo "This is a test email with attachment."
|
||||
echo ""
|
||||
echo "Random content: $content_suffix"
|
||||
echo ""
|
||||
echo "--$boundary"
|
||||
echo "Content-Type: application/octet-stream"
|
||||
echo "Content-Transfer-Encoding: base64"
|
||||
echo "Content-Disposition: attachment; filename=\"$attachment_name\""
|
||||
echo ""
|
||||
generate_headers "$recipient" "$subject" "" "$boundary"
|
||||
|
||||
# Email body part
|
||||
printf -- "--%s\r\n" "$boundary"
|
||||
printf "Content-Type: %s; charset=utf-8\r\n" "$content_type"
|
||||
printf "Content-Transfer-Encoding: 8bit\r\n"
|
||||
printf "\r\n"
|
||||
|
||||
if [[ "$is_html" == "true" ]]; then
|
||||
generate_html_body "$email_number" "$content_suffix" "$chinese_text" "$special_chars" "$emoji_text" "$random_unicode" "$with_attachment"
|
||||
else
|
||||
generate_plain_body "$email_number" "$content_suffix" "$chinese_text" "$special_chars" "$emoji_text" "$random_unicode" "$with_attachment"
|
||||
fi
|
||||
|
||||
printf "\r\n"
|
||||
|
||||
# Attachment part
|
||||
printf -- "--%s\r\n" "$boundary"
|
||||
printf "Content-Type: application/octet-stream\r\n"
|
||||
printf "Content-Transfer-Encoding: base64\r\n"
|
||||
printf "Content-Disposition: attachment; filename=\"%s\"\r\n" "$attachment_name"
|
||||
printf "\r\n"
|
||||
echo "$attachment_content" | base64
|
||||
echo ""
|
||||
echo "--$boundary--"
|
||||
} | curl --url "smtp://localhost:25" \
|
||||
printf "\r\n"
|
||||
printf -- "--%s--\r\n" "$boundary"
|
||||
} | curl --url "smtp://localhost:$smtp_port" \
|
||||
--mail-from "sender@example.com" \
|
||||
--mail-rcpt "$recipient" \
|
||||
--upload-file -
|
||||
else
|
||||
# Handle emails without attachments
|
||||
{
|
||||
echo "From: sender@example.com"
|
||||
echo "To: $recipient"
|
||||
echo "Subject: Test Email - $subject_suffix"
|
||||
echo ""
|
||||
echo "This is a test email."
|
||||
echo ""
|
||||
echo "Random content: $content_suffix"
|
||||
} | curl --url "smtp://localhost:25" \
|
||||
generate_headers "$recipient" "$subject" "$content_type" ""
|
||||
|
||||
if [[ "$is_html" == "true" ]]; then
|
||||
generate_html_body "$email_number" "$content_suffix" "$chinese_text" "$special_chars" "$emoji_text" "$random_unicode" "$with_attachment"
|
||||
else
|
||||
generate_plain_body "$email_number" "$content_suffix" "$chinese_text" "$special_chars" "$emoji_text" "$random_unicode" "$with_attachment"
|
||||
fi
|
||||
} | curl --url "smtp://localhost:$smtp_port" \
|
||||
--mail-from "sender@example.com" \
|
||||
--mail-rcpt "$recipient" \
|
||||
--upload-file -
|
||||
fi
|
||||
}
|
||||
|
||||
print_logo
|
||||
# Check for command line arguments
|
||||
smtp_port="${1:-25}"
|
||||
|
||||
# Validate port number
|
||||
if ! [[ "$smtp_port" =~ ^[0-9]+$ ]] || [ "$smtp_port" -lt 1 ] || [ "$smtp_port" -gt 65535 ]; then
|
||||
echo "Error: Invalid port number. Using default port 25."
|
||||
smtp_port="25"
|
||||
fi
|
||||
|
||||
# Initialize email counter
|
||||
email_counter=1
|
||||
|
||||
print_logo "$smtp_port"
|
||||
|
||||
# Function to display email type menu
|
||||
select_email_type() {
|
||||
echo "" >&2
|
||||
echo "Select email type:" >&2
|
||||
echo "1) Plain text" >&2
|
||||
echo "2) Plain text with attachment" >&2
|
||||
echo "3) HTML" >&2
|
||||
echo "4) HTML with attachment" >&2
|
||||
echo "" >&2
|
||||
|
||||
local email_type
|
||||
while true; do
|
||||
read -p "Enter your choice (1-4): " email_type
|
||||
if [[ "$email_type" =~ ^[1-4]$ ]]; then
|
||||
echo "$email_type"
|
||||
return
|
||||
else
|
||||
echo "Invalid choice. Please enter a number between 1 and 4." >&2
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
while true; do
|
||||
if [[ -z "$recipient" ]]; then
|
||||
read -p "Enter the recipient's email address: " recipient
|
||||
read -p "Do you want to send emails with attachments? (y/N): " with_attachment
|
||||
fi
|
||||
|
||||
if [[ -z "$email_type" ]]; then
|
||||
email_type=$(select_email_type)
|
||||
fi
|
||||
|
||||
send_email "$recipient" "$with_attachment"
|
||||
send_email "$recipient" "$email_type" "$smtp_port" "$email_counter"
|
||||
|
||||
# Increment email counter
|
||||
((email_counter++))
|
||||
|
||||
read -p "Send another email? (Press Enter for same recipient/settings, or type a new email, or 'q' to quit): " next_action
|
||||
|
||||
@@ -96,6 +365,6 @@ while true; do
|
||||
exit 0
|
||||
elif [[ -n "$next_action" ]]; then
|
||||
recipient="$next_action"
|
||||
read -p "Do you want to send emails with attachments? (y/N): " with_attachment
|
||||
email_type=$(select_email_type)
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -43,22 +43,70 @@ public class EmailCleanupTask : IMaintenanceTask
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _settingsService.GetAllSettingsAsync();
|
||||
if (settings.EmailRetentionDays <= 0)
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var totalEmailsDeleted = 0;
|
||||
|
||||
// First handle global retention settings
|
||||
if (settings.EmailRetentionDays > 0)
|
||||
{
|
||||
return;
|
||||
var globalCutoffDate = DateTime.UtcNow.AddDays(-settings.EmailRetentionDays);
|
||||
|
||||
// Delete the emails based on global settings
|
||||
var globalEmailsDeleted = await dbContext.Emails
|
||||
.Where(x => x.DateSystem < globalCutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (globalEmailsDeleted > 0)
|
||||
{
|
||||
totalEmailsDeleted += globalEmailsDeleted;
|
||||
_logger.LogWarning(
|
||||
"Deleted {EmailCount} emails older than {Days} days (global setting)",
|
||||
globalEmailsDeleted,
|
||||
settings.EmailRetentionDays);
|
||||
}
|
||||
}
|
||||
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-settings.EmailRetentionDays);
|
||||
// Now handle per-user age limits
|
||||
var usersWithAgeLimits = await dbContext.AliasVaultUsers
|
||||
.Where(u => u.MaxEmailAgeDays > 0)
|
||||
.Select(u => new { u.Id, u.UserName, u.MaxEmailAgeDays })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Delete the emails
|
||||
var emailsDeleted = await dbContext.Emails
|
||||
.Where(x => x.DateSystem < cutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
foreach (var user in usersWithAgeLimits)
|
||||
{
|
||||
var userCutoffDate = DateTime.UtcNow.AddDays(-user.MaxEmailAgeDays);
|
||||
|
||||
_logger.LogWarning(
|
||||
"Deleted {EmailCount} emails older than {Days} days",
|
||||
emailsDeleted,
|
||||
settings.EmailRetentionDays);
|
||||
// Get all email addresses for this user
|
||||
var userAddresses = await dbContext.UserEmailClaims
|
||||
.Where(c => c.UserId == user.Id)
|
||||
.Select(c => c.Address)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (userAddresses.Count > 0)
|
||||
{
|
||||
// Delete emails older than user's limit
|
||||
var userEmailsDeleted = await dbContext.Emails
|
||||
.Where(e => userAddresses.Contains(e.To) && e.DateSystem < userCutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (userEmailsDeleted > 0)
|
||||
{
|
||||
totalEmailsDeleted += userEmailsDeleted;
|
||||
_logger.LogWarning(
|
||||
"Deleted {EmailCount} emails older than {Days} days for user {UserName} (user-specific setting)",
|
||||
userEmailsDeleted,
|
||||
user.MaxEmailAgeDays,
|
||||
user.UserName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalEmailsDeleted > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Total emails deleted by age cleanup: {TotalEmails}",
|
||||
totalEmailsDeleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,35 +43,53 @@ public class EmailQuotaCleanupTask : IMaintenanceTask
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _settingsService.GetAllSettingsAsync();
|
||||
if (settings.MaxEmailsPerUser <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
// Get all users with their email claims
|
||||
var userEmailClaims = await dbContext.UserEmailClaims
|
||||
.Select(c => new { c.UserId, c.Address })
|
||||
// Get all users with their email claims and limits
|
||||
var usersWithClaims = await (from u in dbContext.AliasVaultUsers
|
||||
join c in dbContext.UserEmailClaims on u.Id equals c.UserId
|
||||
select new { u.Id, u.UserName, u.MaxEmails, c.Address })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var totalEmailsDeleted = 0;
|
||||
var usersProcessed = 0;
|
||||
|
||||
// Group email claims by user
|
||||
foreach (var userGroup in userEmailClaims.GroupBy(c => c.UserId))
|
||||
// Group by user
|
||||
foreach (var userGroup in usersWithClaims.GroupBy(x => new { x.Id, x.UserName, x.MaxEmails }))
|
||||
{
|
||||
var userAddresses = userGroup.Select(c => c.Address).ToList();
|
||||
// Determine the effective limit for this user
|
||||
int effectiveLimit;
|
||||
string limitSource;
|
||||
|
||||
if (userGroup.Key.MaxEmails > 0)
|
||||
{
|
||||
// User has a specific limit
|
||||
effectiveLimit = userGroup.Key.MaxEmails;
|
||||
limitSource = "user-specific";
|
||||
}
|
||||
else if (settings.MaxEmailsPerUser > 0)
|
||||
{
|
||||
// Use global limit
|
||||
effectiveLimit = settings.MaxEmailsPerUser;
|
||||
limitSource = "global";
|
||||
}
|
||||
else
|
||||
{
|
||||
// No limits apply
|
||||
continue;
|
||||
}
|
||||
|
||||
var userAddresses = userGroup.Select(x => x.Address).ToList();
|
||||
|
||||
// Get total email count for this user
|
||||
var emailCount = await dbContext.Emails
|
||||
.Where(e => userAddresses.Contains(e.To))
|
||||
.CountAsync(cancellationToken);
|
||||
|
||||
if (emailCount > settings.MaxEmailsPerUser)
|
||||
if (emailCount > effectiveLimit)
|
||||
{
|
||||
// Calculate how many emails need to be deleted
|
||||
var deleteCount = emailCount - settings.MaxEmailsPerUser;
|
||||
var deleteCount = emailCount - effectiveLimit;
|
||||
|
||||
// Delete the oldest emails - attachments will be cascade deleted
|
||||
var emailsDeleted = await dbContext.Emails
|
||||
@@ -85,18 +103,21 @@ public class EmailQuotaCleanupTask : IMaintenanceTask
|
||||
totalEmailsDeleted += emailsDeleted;
|
||||
usersProcessed++;
|
||||
_logger.LogWarning(
|
||||
"Deleted {EmailCount} emails for user {UserId} to maintain quota of {MaxEmails}",
|
||||
"Deleted {EmailCount} emails for user {Username} to maintain quota of {MaxEmails} ({LimitSource} setting)",
|
||||
emailsDeleted,
|
||||
userGroup.Key,
|
||||
settings.MaxEmailsPerUser);
|
||||
userGroup.Key.UserName,
|
||||
effectiveLimit,
|
||||
limitSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Deleted {TotalEmails} emails across {UserCount} users to maintain quota of {MaxEmails} max emails per user",
|
||||
totalEmailsDeleted,
|
||||
usersProcessed,
|
||||
settings.MaxEmailsPerUser);
|
||||
if (totalEmailsDeleted > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Total emails deleted by quota cleanup: {TotalEmails} across {UserCount} users",
|
||||
totalEmailsDeleted,
|
||||
usersProcessed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public static class AppInfo
|
||||
/// <summary>
|
||||
/// Gets the patch version number.
|
||||
/// </summary>
|
||||
public const int VersionPatch = 0;
|
||||
public const int VersionPatch = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum supported AliasVault client version. Normally the minimum client version is the same
|
||||
|
||||
@@ -241,51 +241,109 @@ public class TaskRunnerTests
|
||||
Assert.That(currentEmailCount, Is.EqualTo(initialEmailCount), "Email count changed despite current day being excluded from maintenance days. Check if TaskRunner is respecting the task runner days setting.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the test with test data.
|
||||
/// <summary>
|
||||
/// Test that per-user email limits are enforced when specified.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
protected async Task InitializeWithTestData()
|
||||
[Test]
|
||||
public async Task PerUserEmailLimits_EnforcesUserSpecificLimits()
|
||||
{
|
||||
await using var dbContext = await _testHostBuilder.GetDbContextAsync();
|
||||
await SeedData.SeedDatabase(dbContext);
|
||||
await _testHost.StartAsync();
|
||||
|
||||
// Wait for the maintenance job to complete instead of using a fixed delay
|
||||
// Create two users with different email limits
|
||||
await SetupPerUserEmailLimitsTest();
|
||||
|
||||
await _testHost.StartAsync();
|
||||
await WaitForMaintenanceJobCompletion();
|
||||
|
||||
// Check that user1 (limit: 5) has exactly 5 emails
|
||||
var user1EmailCount = await dbContext.Emails
|
||||
.Where(e => e.To == "user1@test.com")
|
||||
.CountAsync();
|
||||
Assert.That(user1EmailCount, Is.EqualTo(5), "User1 should have exactly 5 emails after cleanup");
|
||||
|
||||
// Check that user2 (limit: 10) has exactly 10 emails
|
||||
var user2EmailCount = await dbContext.Emails
|
||||
.Where(e => e.To == "user2@test.com")
|
||||
.CountAsync();
|
||||
Assert.That(user2EmailCount, Is.EqualTo(10), "User2 should have exactly 10 emails after cleanup");
|
||||
|
||||
// Check that user3 (no limit) has all 15 emails
|
||||
var user3EmailCount = await dbContext.Emails
|
||||
.Where(e => e.To == "user3@test.com")
|
||||
.CountAsync();
|
||||
Assert.That(user3EmailCount, Is.EqualTo(15), "User3 should have all 15 emails (no limit)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the maintenance job to complete.
|
||||
/// Test that per-user email age limits are enforced when specified.
|
||||
/// </summary>
|
||||
/// <param name="timeoutSeconds">The timeout in seconds.</param>
|
||||
/// <returns>Task.</returns>
|
||||
protected async Task WaitForMaintenanceJobCompletion(int timeoutSeconds = 10)
|
||||
[Test]
|
||||
public async Task PerUserEmailAgeLimits_EnforcesUserSpecificAgeLimits()
|
||||
{
|
||||
var startTime = DateTime.Now;
|
||||
var timeout = startTime.AddSeconds(timeoutSeconds);
|
||||
await using var dbContext = await _testHostBuilder.GetDbContextAsync();
|
||||
|
||||
while (DateTime.Now < timeout)
|
||||
// Create users with different email age limits
|
||||
await SetupPerUserEmailAgeLimitsTest();
|
||||
|
||||
await _testHost.StartAsync();
|
||||
await WaitForMaintenanceJobCompletion();
|
||||
|
||||
// Check that user1 (7 days limit) has no emails older than 7 days
|
||||
var user1OldEmails = await dbContext.Emails
|
||||
.Where(e => e.To == "user1@test.com" && e.DateSystem < DateTime.UtcNow.AddDays(-7))
|
||||
.CountAsync();
|
||||
Assert.That(user1OldEmails, Is.EqualTo(0), "User1 should have no emails older than 7 days");
|
||||
|
||||
// Check that user2 (30 days limit) has no emails older than 30 days
|
||||
var user2OldEmails = await dbContext.Emails
|
||||
.Where(e => e.To == "user2@test.com" && e.DateSystem < DateTime.UtcNow.AddDays(-30))
|
||||
.CountAsync();
|
||||
Assert.That(user2OldEmails, Is.EqualTo(0), "User2 should have no emails older than 30 days");
|
||||
|
||||
// Check that user3 (no age limit) has all emails including old ones
|
||||
var user3OldEmails = await dbContext.Emails
|
||||
.Where(e => e.To == "user3@test.com" && e.DateSystem < DateTime.UtcNow.AddDays(-50))
|
||||
.CountAsync();
|
||||
Assert.That(user3OldEmails, Is.GreaterThan(0), "User3 should have old emails (no age limit)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that user-specific limits take priority over global limits.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
[Test]
|
||||
public async Task PerUserLimits_TakePriorityOverGlobalLimits()
|
||||
{
|
||||
await using var dbContext = await _testHostBuilder.GetDbContextAsync();
|
||||
|
||||
// Set global email limit to 20
|
||||
var globalSetting = new ServerSetting
|
||||
{
|
||||
await using var dbContext = await _testHostBuilder.GetDbContextAsync();
|
||||
var job = await dbContext.TaskRunnerJobs
|
||||
.OrderByDescending(j => j.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
Key = "MaxEmailsPerUser",
|
||||
Value = "20",
|
||||
};
|
||||
dbContext.ServerSettings.Add(globalSetting);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
if (job != null && (job.Status == TaskRunnerJobStatus.Finished || job.Status == TaskRunnerJobStatus.Error))
|
||||
{
|
||||
if (job.Status == TaskRunnerJobStatus.Error)
|
||||
{
|
||||
Assert.Fail($"Maintenance job failed with error: {job.ErrorMessage}");
|
||||
}
|
||||
// Create user with specific limit that overrides global
|
||||
await SetupUserSpecificVsGlobalLimitsTest();
|
||||
|
||||
return;
|
||||
}
|
||||
await _testHost.StartAsync();
|
||||
await WaitForMaintenanceJobCompletion();
|
||||
|
||||
await Task.Delay(500); // Poll every 500ms
|
||||
}
|
||||
// User with specific limit (5) should have 5 emails, not 20
|
||||
var userWithLimitCount = await dbContext.Emails
|
||||
.Where(e => e.To == "userwithLimit@test.com")
|
||||
.CountAsync();
|
||||
Assert.That(userWithLimitCount, Is.EqualTo(5), "User with specific limit should have 5 emails, not global limit");
|
||||
|
||||
Assert.Fail($"Maintenance job did not complete within {timeoutSeconds} seconds");
|
||||
// User without specific limit should use global limit (20)
|
||||
var userWithoutLimitCount = await dbContext.Emails
|
||||
.Where(e => e.To == "userwithoutLimit@test.com")
|
||||
.CountAsync();
|
||||
Assert.That(userWithoutLimitCount, Is.EqualTo(20), "User without specific limit should use global limit");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -318,6 +376,53 @@ public class TaskRunnerTests
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the test with test data.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task InitializeWithTestData()
|
||||
{
|
||||
await using var dbContext = await _testHostBuilder.GetDbContextAsync();
|
||||
await SeedData.SeedDatabase(dbContext);
|
||||
await _testHost.StartAsync();
|
||||
|
||||
// Wait for the maintenance job to complete instead of using a fixed delay
|
||||
await WaitForMaintenanceJobCompletion();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the maintenance job to complete.
|
||||
/// </summary>
|
||||
/// <param name="timeoutSeconds">The timeout in seconds.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task WaitForMaintenanceJobCompletion(int timeoutSeconds = 10)
|
||||
{
|
||||
var startTime = DateTime.Now;
|
||||
var timeout = startTime.AddSeconds(timeoutSeconds);
|
||||
|
||||
while (DateTime.Now < timeout)
|
||||
{
|
||||
await using var dbContext = await _testHostBuilder.GetDbContextAsync();
|
||||
var job = await dbContext.TaskRunnerJobs
|
||||
.OrderByDescending(j => j.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (job != null && (job.Status == TaskRunnerJobStatus.Finished || job.Status == TaskRunnerJobStatus.Error))
|
||||
{
|
||||
if (job.Status == TaskRunnerJobStatus.Error)
|
||||
{
|
||||
Assert.Fail($"Maintenance job failed with error: {job.ErrorMessage}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(500); // Poll every 500ms
|
||||
}
|
||||
|
||||
Assert.Fail($"Maintenance job did not complete within {timeoutSeconds} seconds");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up test data for disabled email cleanup tests.
|
||||
/// </summary>
|
||||
@@ -421,4 +526,201 @@ public class TaskRunnerTests
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up test data for per-user email limits testing.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task SetupPerUserEmailLimitsTest()
|
||||
{
|
||||
await using var dbContext = await _testHostBuilder.GetDbContextAsync();
|
||||
|
||||
// Create user1 with 5 email limit
|
||||
var user1 = new AliasVaultUser
|
||||
{
|
||||
UserName = "user1",
|
||||
Email = "user1@test.com",
|
||||
MaxEmails = 5,
|
||||
};
|
||||
dbContext.AliasVaultUsers.Add(user1);
|
||||
|
||||
// Create user2 with 10 email limit
|
||||
var user2 = new AliasVaultUser
|
||||
{
|
||||
UserName = "user2",
|
||||
Email = "user2@test.com",
|
||||
MaxEmails = 10,
|
||||
};
|
||||
dbContext.AliasVaultUsers.Add(user2);
|
||||
|
||||
// Create user3 with no limit (0 = unlimited)
|
||||
var user3 = new AliasVaultUser
|
||||
{
|
||||
UserName = "user3",
|
||||
Email = "user3@test.com",
|
||||
MaxEmails = 0,
|
||||
};
|
||||
dbContext.AliasVaultUsers.Add(user3);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Create encryption key
|
||||
var encryptionKey = new UserEncryptionKey
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = user1.Id,
|
||||
PublicKey = "test-encryption-key",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
dbContext.UserEncryptionKeys.Add(encryptionKey);
|
||||
|
||||
// Create email claims for each user
|
||||
dbContext.UserEmailClaims.Add(new UserEmailClaim { UserId = user1.Id, Address = "user1@test.com", AddressLocal = "user1", AddressDomain = "test.com" });
|
||||
dbContext.UserEmailClaims.Add(new UserEmailClaim { UserId = user2.Id, Address = "user2@test.com", AddressLocal = "user2", AddressDomain = "test.com" });
|
||||
dbContext.UserEmailClaims.Add(new UserEmailClaim { UserId = user3.Id, Address = "user3@test.com", AddressLocal = "user3", AddressDomain = "test.com" });
|
||||
|
||||
// Create 15 emails for each user (all will exceed user1 and user2 limits)
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
var dateCreated = DateTime.UtcNow.AddDays(-i); // Different ages for realistic testing
|
||||
|
||||
dbContext.Emails.Add(CreateTestEmail("user1@test.com", encryptionKey, $"User1 Email {i}", dateCreated));
|
||||
dbContext.Emails.Add(CreateTestEmail("user2@test.com", encryptionKey, $"User2 Email {i}", dateCreated));
|
||||
dbContext.Emails.Add(CreateTestEmail("user3@test.com", encryptionKey, $"User3 Email {i}", dateCreated));
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up test data for per-user email age limits testing.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task SetupPerUserEmailAgeLimitsTest()
|
||||
{
|
||||
await using var dbContext = await _testHostBuilder.GetDbContextAsync();
|
||||
|
||||
// Create user1 with 7 days age limit
|
||||
var user1 = new AliasVaultUser
|
||||
{
|
||||
UserName = "user1",
|
||||
Email = "user1@test.com",
|
||||
MaxEmailAgeDays = 7,
|
||||
};
|
||||
dbContext.AliasVaultUsers.Add(user1);
|
||||
|
||||
// Create user2 with 30 days age limit
|
||||
var user2 = new AliasVaultUser
|
||||
{
|
||||
UserName = "user2",
|
||||
Email = "user2@test.com",
|
||||
MaxEmailAgeDays = 30,
|
||||
};
|
||||
dbContext.AliasVaultUsers.Add(user2);
|
||||
|
||||
// Create user3 with no age limit (0 = unlimited)
|
||||
var user3 = new AliasVaultUser
|
||||
{
|
||||
UserName = "user3",
|
||||
Email = "user3@test.com",
|
||||
MaxEmailAgeDays = 0,
|
||||
};
|
||||
dbContext.AliasVaultUsers.Add(user3);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Create encryption key
|
||||
var encryptionKey = new UserEncryptionKey
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = user1.Id,
|
||||
PublicKey = "test-encryption-key",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
dbContext.UserEncryptionKeys.Add(encryptionKey);
|
||||
|
||||
// Create email claims for each user
|
||||
dbContext.UserEmailClaims.Add(new UserEmailClaim { UserId = user1.Id, Address = "user1@test.com", AddressLocal = "user1", AddressDomain = "test.com" });
|
||||
dbContext.UserEmailClaims.Add(new UserEmailClaim { UserId = user2.Id, Address = "user2@test.com", AddressLocal = "user2", AddressDomain = "test.com" });
|
||||
dbContext.UserEmailClaims.Add(new UserEmailClaim { UserId = user3.Id, Address = "user3@test.com", AddressLocal = "user3", AddressDomain = "test.com" });
|
||||
|
||||
// Create emails with various ages for each user
|
||||
var testDates = new[]
|
||||
{
|
||||
DateTime.UtcNow.AddDays(-1), // 1 day old
|
||||
DateTime.UtcNow.AddDays(-5), // 5 days old
|
||||
DateTime.UtcNow.AddDays(-10), // 10 days old (should be deleted for user1)
|
||||
DateTime.UtcNow.AddDays(-15), // 15 days old (should be deleted for user1)
|
||||
DateTime.UtcNow.AddDays(-25), // 25 days old (should be deleted for user1)
|
||||
DateTime.UtcNow.AddDays(-35), // 35 days old (should be deleted for user1 and user2)
|
||||
DateTime.UtcNow.AddDays(-45), // 45 days old (should be deleted for user1 and user2)
|
||||
DateTime.UtcNow.AddDays(-60), // 60 days old (should be deleted for user1 and user2)
|
||||
};
|
||||
|
||||
foreach (var date in testDates)
|
||||
{
|
||||
dbContext.Emails.Add(CreateTestEmail("user1@test.com", encryptionKey, $"User1 Email {date:yyyy-MM-dd}", date));
|
||||
dbContext.Emails.Add(CreateTestEmail("user2@test.com", encryptionKey, $"User2 Email {date:yyyy-MM-dd}", date));
|
||||
dbContext.Emails.Add(CreateTestEmail("user3@test.com", encryptionKey, $"User3 Email {date:yyyy-MM-dd}", date));
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up test data for testing user-specific vs global limits.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task SetupUserSpecificVsGlobalLimitsTest()
|
||||
{
|
||||
await using var dbContext = await _testHostBuilder.GetDbContextAsync();
|
||||
|
||||
// Create user with specific limit that overrides global
|
||||
var userWithLimit = new AliasVaultUser
|
||||
{
|
||||
UserName = "userwithLimit",
|
||||
Email = "userwithLimit@test.com",
|
||||
MaxEmails = 5, // Lower than global limit
|
||||
};
|
||||
dbContext.AliasVaultUsers.Add(userWithLimit);
|
||||
|
||||
// Create user without specific limit (should use global)
|
||||
var userWithoutLimit = new AliasVaultUser
|
||||
{
|
||||
UserName = "userwithoutLimit",
|
||||
Email = "userwithoutLimit@test.com",
|
||||
MaxEmails = 0, // Use global limit
|
||||
};
|
||||
dbContext.AliasVaultUsers.Add(userWithoutLimit);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Create encryption key
|
||||
var encryptionKey = new UserEncryptionKey
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userWithLimit.Id,
|
||||
PublicKey = "test-encryption-key",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
dbContext.UserEncryptionKeys.Add(encryptionKey);
|
||||
|
||||
// Create email claims
|
||||
dbContext.UserEmailClaims.Add(new UserEmailClaim { UserId = userWithLimit.Id, Address = "userwithLimit@test.com", AddressLocal = "userwithLimit", AddressDomain = "test.com" });
|
||||
dbContext.UserEmailClaims.Add(new UserEmailClaim { UserId = userWithoutLimit.Id, Address = "userwithoutLimit@test.com", AddressLocal = "userwithoutLimit", AddressDomain = "test.com" });
|
||||
|
||||
// Create 25 emails for each user (both exceed their limits)
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
var dateCreated = DateTime.UtcNow.AddDays(-i);
|
||||
|
||||
dbContext.Emails.Add(CreateTestEmail("userwithLimit@test.com", encryptionKey, $"Limited User Email {i}", dateCreated));
|
||||
dbContext.Emails.Add(CreateTestEmail("userwithoutLimit@test.com", encryptionKey, $"Unlimited User Email {i}", dateCreated));
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,8 @@ nav_order: 6
|
||||
|
||||
# Upgrade the AliasServerDb EF model
|
||||
|
||||
The AliasServerDb EF model has migrations for both the SQLite and PostgreSQL databases. This means
|
||||
that when you make changes to the EF model, you need to create migrations for both databases.
|
||||
The below command allows you to create a new EF migration based on the existing database structure as defined in the EF mode classes.
|
||||
|
||||
1. Make migration for PostgreSQL database:
|
||||
```bash
|
||||
dotnet ef migrations add InitialMigration --context AliasServerDbContextPostgresql --output-dir Migrations/PostgresqlMigrations
|
||||
```
|
||||
|
||||
2. Make migration for SQLite database:
|
||||
```bash
|
||||
dotnet ef migrations add InitialMigration --context AliasServerDbContextSqlite --output-dir Migrations/SqliteMigrations
|
||||
dotnet ef migrations add InitialMigration --output-dir Migrations/PostgresqlMigrations
|
||||
```
|
||||
|
||||
3
fastlane/metadata/android/en-US/changelogs/12.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/12.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Generated email aliases are now more unique
|
||||
- Fixed problem where app sometimes showed unlock sequence twice
|
||||
- Fix issue with save button on Android not always detecting presses
|
||||
3
fastlane/metadata/android/nl-NL/changelogs/12.txt
Normal file
3
fastlane/metadata/android/nl-NL/changelogs/12.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Gegenereerde e-mailaliassen unieker gemaakt
|
||||
- Probleem opgelost waarbij app soms twee keer de unlock procedure toonde
|
||||
- Probleem opgelost waarbij "opslaan" knop op Android soms niet goed werkte
|
||||
@@ -0,0 +1 @@
|
||||
- Fix bug where extension could crash on startup in certain browsers on Windows due to new translations
|
||||
@@ -0,0 +1 @@
|
||||
- Fix bug where plain text emails would not be properly readable in dark mode
|
||||
2
fastlane/metadata/ios/en-US/changelogs/12.txt
Normal file
2
fastlane/metadata/ios/en-US/changelogs/12.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Generated email aliases are now more unique
|
||||
- Fixed problem where app sometimes showed unlock sequence twice
|
||||
2
fastlane/metadata/ios/nl-NL/changelogs/12.txt
Normal file
2
fastlane/metadata/ios/nl-NL/changelogs/12.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Gegenereerde e-mailaliassen unieker gemaakt
|
||||
- Probleem opgelost waarbij app soms twee keer de unlock procedure toonde
|
||||
@@ -294,14 +294,23 @@ update_version "../apps/browser-extension/safari-xcode/AliasVault/AliasVault.xco
|
||||
"CURRENT_PROJECT_VERSION = $new_safari_build;" \
|
||||
"Safari Extension"
|
||||
|
||||
# Update install.sh version if it exists
|
||||
if [ -f "../install.sh" ]; then
|
||||
echo -e "\n"
|
||||
echo "--------------------------------"
|
||||
echo "Reminder: if you've made changes to install.sh since the last release, remember to update its @version in the header to match this release version ($version)."
|
||||
echo "--------------------------------"
|
||||
read -p "Press Enter to continue..."
|
||||
fi
|
||||
# Show reminders
|
||||
echo -e "\n"
|
||||
echo "--------------------------------"
|
||||
echo "IMPORTANT REMINDERS:"
|
||||
echo "--------------------------------"
|
||||
|
||||
# Install.sh reminder
|
||||
echo "• If you've made changes to install.sh since the last release, remember to update its @version in the header to match this release version ($version)."
|
||||
|
||||
# Changelog reminder
|
||||
echo "• Add changelogs for new version $version to the /fastlane directory:"
|
||||
echo " - iOS: fastlane/metadata/ios/[lang]/changelogs/$(echo $new_ios_build | cut -d. -f1-2).txt"
|
||||
echo " - Android: fastlane/metadata/android/[lang]/changelogs/$(echo $new_android_build | cut -d. -f1-2).txt"
|
||||
echo " - Browser Extension: fastlane/metadata/browser-extension/[lang]/changelogs/$version.txt"
|
||||
|
||||
echo "--------------------------------"
|
||||
read -p "Press Enter to continue..."
|
||||
|
||||
echo -e "\nAll build numbers have been updated successfully!"
|
||||
echo "New version: $version"
|
||||
|
||||
@@ -1,261 +1,279 @@
|
||||
export default [
|
||||
"Aaliyah",
|
||||
"Abigail",
|
||||
"Ada",
|
||||
"Adalyn",
|
||||
"Adalynn",
|
||||
"Addison",
|
||||
"Adeline",
|
||||
"Adriana",
|
||||
"Alexa",
|
||||
"Alexandra",
|
||||
"Alice",
|
||||
"Alina",
|
||||
"Allison",
|
||||
"Alyssa",
|
||||
"Amara",
|
||||
"Amaya",
|
||||
"Amelia",
|
||||
"Anastasia",
|
||||
"Andrea",
|
||||
"Anna",
|
||||
"Annabelle",
|
||||
"Aria",
|
||||
"Ariana",
|
||||
"Arianna",
|
||||
"Arielle",
|
||||
"Arya",
|
||||
"Ashley",
|
||||
"Aspen",
|
||||
"Athena",
|
||||
"Aubree",
|
||||
"Aubrey",
|
||||
"Audrey",
|
||||
"Aurora",
|
||||
"Autumn",
|
||||
"Ava",
|
||||
"Avery",
|
||||
"Ayla",
|
||||
"Bailey",
|
||||
"Beatrice",
|
||||
"Bella",
|
||||
"Bianca",
|
||||
"Blair",
|
||||
"Brianna",
|
||||
"Brielle",
|
||||
"Brooklyn",
|
||||
"Brynn",
|
||||
"Calliope",
|
||||
"Camila",
|
||||
"Camille",
|
||||
"Carmen",
|
||||
"Caroline",
|
||||
"Cassidy",
|
||||
"Catalina",
|
||||
"Cecilia",
|
||||
"Celeste",
|
||||
"Charlie",
|
||||
"Charlotte",
|
||||
"Chloe",
|
||||
"Claire",
|
||||
"Clara",
|
||||
"Cora",
|
||||
"Coraline",
|
||||
"Dahlia",
|
||||
"Daisy",
|
||||
"Dakota",
|
||||
"Daphne",
|
||||
"Dawn",
|
||||
"Delilah",
|
||||
"Diana",
|
||||
"Eden",
|
||||
"Eleanor",
|
||||
"Elena",
|
||||
"Eliana",
|
||||
"Elise",
|
||||
"Eliza",
|
||||
"Elizabeth",
|
||||
"Ella",
|
||||
"Ellie",
|
||||
"Eloise",
|
||||
"Ember",
|
||||
"Emerson",
|
||||
"Emersyn",
|
||||
"Emery",
|
||||
"Emilia",
|
||||
"Emily",
|
||||
"Emma",
|
||||
"Olivia",
|
||||
"Ava",
|
||||
"Sophia",
|
||||
"Isabella",
|
||||
"Mia",
|
||||
"Charlotte",
|
||||
"Amelia",
|
||||
"Harper",
|
||||
"Evelyn",
|
||||
"Abigail",
|
||||
"Elizabeth",
|
||||
"Sofia",
|
||||
"Avery",
|
||||
"Ella",
|
||||
"Madison",
|
||||
"Scarlett",
|
||||
"Victoria",
|
||||
"Aria",
|
||||
"Grace",
|
||||
"Chloe",
|
||||
"Camila",
|
||||
"Penelope",
|
||||
"Riley",
|
||||
"Layla",
|
||||
"Zoey",
|
||||
"Nora",
|
||||
"Lily",
|
||||
"Eleanor",
|
||||
"Hannah",
|
||||
"Lillian",
|
||||
"Addison",
|
||||
"Aubrey",
|
||||
"Ellie",
|
||||
"Stella",
|
||||
"Natalie",
|
||||
"Zoe",
|
||||
"Leah",
|
||||
"Hazel",
|
||||
"Violet",
|
||||
"Aurora",
|
||||
"Savannah",
|
||||
"Audrey",
|
||||
"Brooklyn",
|
||||
"Bella",
|
||||
"Claire",
|
||||
"Skylar",
|
||||
"Lucy",
|
||||
"Paisley",
|
||||
"Everly",
|
||||
"Anna",
|
||||
"Caroline",
|
||||
"Nova",
|
||||
"Genesis",
|
||||
"Emilia",
|
||||
"Kennedy",
|
||||
"Samantha",
|
||||
"Maya",
|
||||
"Willow",
|
||||
"Kinsley",
|
||||
"Naomi",
|
||||
"Aaliyah",
|
||||
"Elena",
|
||||
"Sarah",
|
||||
"Ariana",
|
||||
"Allison",
|
||||
"Gabriella",
|
||||
"Alice",
|
||||
"Madelyn",
|
||||
"Cora",
|
||||
"Ruby",
|
||||
"Eva",
|
||||
"Serenity",
|
||||
"Autumn",
|
||||
"Adeline",
|
||||
"Hailey",
|
||||
"Gianna",
|
||||
"Valentina",
|
||||
"Isla",
|
||||
"Eliana",
|
||||
"Quinn",
|
||||
"Nevaeh",
|
||||
"Ivy",
|
||||
"Sadie",
|
||||
"Piper",
|
||||
"Lydia",
|
||||
"Alexa",
|
||||
"Josephine",
|
||||
"Emery",
|
||||
"Julia",
|
||||
"Delilah",
|
||||
"Arianna",
|
||||
"Vivian",
|
||||
"Kaylee",
|
||||
"Sophie",
|
||||
"Brielle",
|
||||
"Madeline",
|
||||
"Peyton",
|
||||
"Rylee",
|
||||
"Clara",
|
||||
"Hadley",
|
||||
"Melanie",
|
||||
"Mackenzie",
|
||||
"Reagan",
|
||||
"Adalyn",
|
||||
"Liliana",
|
||||
"Aubree",
|
||||
"Jade",
|
||||
"Katherine",
|
||||
"Isabelle",
|
||||
"Natalia",
|
||||
"Raelynn",
|
||||
"Maria",
|
||||
"Athena",
|
||||
"Ximena",
|
||||
"Arya",
|
||||
"Leilani",
|
||||
"Taylor",
|
||||
"Faith",
|
||||
"Rose",
|
||||
"Kylie",
|
||||
"Alexandra",
|
||||
"Mary",
|
||||
"Margaret",
|
||||
"Lyla",
|
||||
"Ashley",
|
||||
"Amaya",
|
||||
"Eliza",
|
||||
"Brianna",
|
||||
"Bailey",
|
||||
"Andrea",
|
||||
"Khloe",
|
||||
"Jasmine",
|
||||
"Melody",
|
||||
"Iris",
|
||||
"Isabel",
|
||||
"Norah",
|
||||
"Annabelle",
|
||||
"Valeria",
|
||||
"Emerson",
|
||||
"Adalynn",
|
||||
"Ryleigh",
|
||||
"Eden",
|
||||
"Emersyn",
|
||||
"Anastasia",
|
||||
"Kayla",
|
||||
"Alyssa",
|
||||
"Anna",
|
||||
"Juliana",
|
||||
"Charlie",
|
||||
"Lucia",
|
||||
"Stella",
|
||||
"Adriana",
|
||||
"Beatrice",
|
||||
"Bianca",
|
||||
"Calliope",
|
||||
"Carmen",
|
||||
"Celeste",
|
||||
"Dakota",
|
||||
"Diana",
|
||||
"Esme",
|
||||
"Esther",
|
||||
"Eva",
|
||||
"Evangeline",
|
||||
"Evelyn",
|
||||
"Everly",
|
||||
"Faith",
|
||||
"Fiona",
|
||||
"Flora",
|
||||
"Florence",
|
||||
"Francesca",
|
||||
"Freya",
|
||||
"Gabriella",
|
||||
"Gemma",
|
||||
"Genesis",
|
||||
"Georgia",
|
||||
"Gianna",
|
||||
"Giselle",
|
||||
"Grace",
|
||||
"Gwendolyn",
|
||||
"Hadley",
|
||||
"Hailey",
|
||||
"Hannah",
|
||||
"Harlow",
|
||||
"Harmony",
|
||||
"Harper",
|
||||
"Haven",
|
||||
"Hazel",
|
||||
"Heidi",
|
||||
"Helena",
|
||||
"Holly",
|
||||
"Hope",
|
||||
"Imogen",
|
||||
"India",
|
||||
"Indie",
|
||||
"Iris",
|
||||
"Isabel",
|
||||
"Isabella",
|
||||
"Isabelle",
|
||||
"Isla",
|
||||
"Ivy",
|
||||
"Jade",
|
||||
"Jasmine",
|
||||
"Jessie",
|
||||
"Jocelyn",
|
||||
"Josephine",
|
||||
"Josie",
|
||||
"Julia",
|
||||
"Juliana",
|
||||
"Juliet",
|
||||
"June",
|
||||
"Juniper",
|
||||
"Kaia",
|
||||
"Katherine",
|
||||
"Kayla",
|
||||
"Kaylee",
|
||||
"Keira",
|
||||
"Kennedy",
|
||||
"Khloe",
|
||||
"Kinsley",
|
||||
"Kylie",
|
||||
"Lara",
|
||||
"Laura",
|
||||
"Laurel",
|
||||
"Layla",
|
||||
"Leah",
|
||||
"Leilani",
|
||||
"Lena",
|
||||
"Liliana",
|
||||
"Lillian",
|
||||
"Lily",
|
||||
"Lola",
|
||||
"Lorelei",
|
||||
"Lucia",
|
||||
"Lucy",
|
||||
"Luna",
|
||||
"Magnolia",
|
||||
"Lydia",
|
||||
"Lyla",
|
||||
"Mabel",
|
||||
"Mackenzie",
|
||||
"Madeline",
|
||||
"Madelyn",
|
||||
"Madison",
|
||||
"Maeve",
|
||||
"Magnolia",
|
||||
"Maisie",
|
||||
"Malia",
|
||||
"Margaret",
|
||||
"Margot",
|
||||
"Maria",
|
||||
"Marina",
|
||||
"Marlowe",
|
||||
"Mary",
|
||||
"Matilda",
|
||||
"Maya",
|
||||
"Melanie",
|
||||
"Melody",
|
||||
"Mia",
|
||||
"Mira",
|
||||
"Miranda",
|
||||
"Morgan",
|
||||
"Nadia",
|
||||
"Naomi",
|
||||
"Natalia",
|
||||
"Natalie",
|
||||
"Nell",
|
||||
"Nevaeh",
|
||||
"Nina",
|
||||
"Noelle",
|
||||
"Nora",
|
||||
"Norah",
|
||||
"Nova",
|
||||
"Octavia",
|
||||
"Odette",
|
||||
"Olive",
|
||||
"Olivia",
|
||||
"Opal",
|
||||
"Ophelia",
|
||||
"Paisley",
|
||||
"Pearl",
|
||||
"Penelope",
|
||||
"Peyton",
|
||||
"Phoebe",
|
||||
"Phoenix",
|
||||
"Piper",
|
||||
"Poppy",
|
||||
"Primrose",
|
||||
"Quinn",
|
||||
"Raelynn",
|
||||
"Ramona",
|
||||
"Raven",
|
||||
"Reagan",
|
||||
"Reese",
|
||||
"Riley",
|
||||
"River",
|
||||
"Robin",
|
||||
"Rosalie",
|
||||
"Rose",
|
||||
"Rosemary",
|
||||
"Rowan",
|
||||
"Ruby",
|
||||
"Ruth",
|
||||
"Rylee",
|
||||
"Ryleigh",
|
||||
"Sabrina",
|
||||
"Sadie",
|
||||
"Sage",
|
||||
"Salem",
|
||||
"Selena",
|
||||
"Sienna",
|
||||
"Summer",
|
||||
"Sylvie",
|
||||
"Thea",
|
||||
"Tessa",
|
||||
"Wren",
|
||||
"Winter",
|
||||
"Willa",
|
||||
"Ada",
|
||||
"Aspen",
|
||||
"Blair",
|
||||
"Brynn",
|
||||
"Cassidy",
|
||||
"Cecilia",
|
||||
"Daisy",
|
||||
"Dawn",
|
||||
"Daphne",
|
||||
"Ember",
|
||||
"Fiona",
|
||||
"Flora",
|
||||
"Freya",
|
||||
"Gemma",
|
||||
"Giselle",
|
||||
"Harmony",
|
||||
"Heidi",
|
||||
"Imogen",
|
||||
"Indie",
|
||||
"Jessie",
|
||||
"June",
|
||||
"Kaia",
|
||||
"Lena",
|
||||
"Lola",
|
||||
"Mabel",
|
||||
"Maisie",
|
||||
"Margot",
|
||||
"Matilda",
|
||||
"Mira",
|
||||
"Morgan",
|
||||
"Nell",
|
||||
"Nadia",
|
||||
"Odette",
|
||||
"Opal",
|
||||
"Pearl",
|
||||
"Phoebe",
|
||||
"Raven",
|
||||
"Reese",
|
||||
"Robin",
|
||||
"Rowan",
|
||||
"Ruth",
|
||||
"Sabrina",
|
||||
"Samantha",
|
||||
"Sarah",
|
||||
"Sasha",
|
||||
"Savannah",
|
||||
"Scarlett",
|
||||
"Selena",
|
||||
"Serena",
|
||||
"Serenity",
|
||||
"Sienna",
|
||||
"Sierra",
|
||||
"Simone",
|
||||
"Skye",
|
||||
"Skylar",
|
||||
"Sloane",
|
||||
"Sofia",
|
||||
"Sophia",
|
||||
"Sophie",
|
||||
"Stella",
|
||||
"Summer",
|
||||
"Sylvia",
|
||||
"Sylvie",
|
||||
"Talia",
|
||||
"Taylor",
|
||||
"Tessa",
|
||||
"Thea",
|
||||
"Thora",
|
||||
"Valentina",
|
||||
"Valeria",
|
||||
"Vera",
|
||||
"Victoria",
|
||||
"Violet",
|
||||
"Vivian",
|
||||
"Vivienne",
|
||||
"Willa",
|
||||
"Willow",
|
||||
"Winnie",
|
||||
"Winter",
|
||||
"Wren",
|
||||
"Ximena",
|
||||
"Yara",
|
||||
"Zara"
|
||||
"Zara",
|
||||
"Zoe",
|
||||
"Zoey"
|
||||
];
|
||||
|
||||
@@ -1,249 +1,299 @@
|
||||
export default [
|
||||
"Michael",
|
||||
"Christopher",
|
||||
"Matthew",
|
||||
"Joshua",
|
||||
"Daniel",
|
||||
"David",
|
||||
"Andrew",
|
||||
"Joseph",
|
||||
"James",
|
||||
"John",
|
||||
"Robert",
|
||||
"William",
|
||||
"Ryan",
|
||||
"Jason",
|
||||
"Nicholas",
|
||||
"Jonathan",
|
||||
"Jacob",
|
||||
"Brandon",
|
||||
"Tyler",
|
||||
"Zachary",
|
||||
"Kevin",
|
||||
"Justin",
|
||||
"Benjamin",
|
||||
"Anthony",
|
||||
"Samuel",
|
||||
"Thomas",
|
||||
"Alexander",
|
||||
"Ethan",
|
||||
"Noah",
|
||||
"Dylan",
|
||||
"Nathan",
|
||||
"Christian",
|
||||
"Austin",
|
||||
"Adam",
|
||||
"Caleb",
|
||||
"Cody",
|
||||
"Jordan",
|
||||
"Logan",
|
||||
"Aaron",
|
||||
"Kyle",
|
||||
"Jose",
|
||||
"Brian",
|
||||
"Gabriel",
|
||||
"Timothy",
|
||||
"Luke",
|
||||
"Jared",
|
||||
"Connor",
|
||||
"Sean",
|
||||
"Evan",
|
||||
"Isaac",
|
||||
"Jack",
|
||||
"Cameron",
|
||||
"Hunter",
|
||||
"Jackson",
|
||||
"Charles",
|
||||
"Devin",
|
||||
"Stephen",
|
||||
"Patrick",
|
||||
"Steven",
|
||||
"Elijah",
|
||||
"Scott",
|
||||
"Mark",
|
||||
"Jeffrey",
|
||||
"Corey",
|
||||
"Juan",
|
||||
"Luis",
|
||||
"Derek",
|
||||
"Chase",
|
||||
"Travis",
|
||||
"Alex",
|
||||
"Spencer",
|
||||
"Ian",
|
||||
"Trevor",
|
||||
"Bryan",
|
||||
"Tanner",
|
||||
"Marcus",
|
||||
"Jeremy",
|
||||
"Eric",
|
||||
"Jaden",
|
||||
"Garrett",
|
||||
"Isaiah",
|
||||
"Dustin",
|
||||
"Jesse",
|
||||
"Seth",
|
||||
"Blake",
|
||||
"Nathaniel",
|
||||
"Mason",
|
||||
"Liam",
|
||||
"Paul",
|
||||
"Carlos",
|
||||
"Mitchell",
|
||||
"Parker",
|
||||
"Lucas",
|
||||
"Richard",
|
||||
"Cole",
|
||||
"Adam",
|
||||
"Adrian",
|
||||
"Colin",
|
||||
"Bradley",
|
||||
"Jesus",
|
||||
"Peter",
|
||||
"Kenneth",
|
||||
"Joel",
|
||||
"Victor",
|
||||
"Bryce",
|
||||
"Casey",
|
||||
"Vincent",
|
||||
"Edward",
|
||||
"Henry",
|
||||
"Dominic",
|
||||
"Riley",
|
||||
"Shane",
|
||||
"Dalton",
|
||||
"Grant",
|
||||
"Shawn",
|
||||
"Braden",
|
||||
"Caden",
|
||||
"Max",
|
||||
"Hayden",
|
||||
"Owen",
|
||||
"Brett",
|
||||
"Trevor",
|
||||
"Philip",
|
||||
"Brendan",
|
||||
"Wesley",
|
||||
"Aidan",
|
||||
"Brady",
|
||||
"Colton",
|
||||
"Tristan",
|
||||
"George",
|
||||
"Gavin",
|
||||
"Dawson",
|
||||
"Miguel",
|
||||
"Antonio",
|
||||
"Nolan",
|
||||
"Dakota",
|
||||
"Jace",
|
||||
"Collin",
|
||||
"Preston",
|
||||
"Levi",
|
||||
"Aiden",
|
||||
"Alan",
|
||||
"Jorge",
|
||||
"Carson",
|
||||
"Felix",
|
||||
"Oliver",
|
||||
"Theodore",
|
||||
"Harrison",
|
||||
"Maxwell",
|
||||
"Sebastian",
|
||||
"Xavier",
|
||||
"Dominick",
|
||||
"Lincoln",
|
||||
"Elliott",
|
||||
"Walter",
|
||||
"Simon",
|
||||
"Dean",
|
||||
"Hugo",
|
||||
"Malcolm",
|
||||
"Leon",
|
||||
"Oscar",
|
||||
"Calvin",
|
||||
"Raymond",
|
||||
"Edgar",
|
||||
"Franklin",
|
||||
"Arthur",
|
||||
"Lawrence",
|
||||
"Dennis",
|
||||
"Russell",
|
||||
"Douglas",
|
||||
"Leonard",
|
||||
"Gregory",
|
||||
"Harold",
|
||||
"Frederick",
|
||||
"Martin",
|
||||
"Curtis",
|
||||
"Stanley",
|
||||
"Gilbert",
|
||||
"Harvey",
|
||||
"Francis",
|
||||
"Eugene",
|
||||
"Ralph",
|
||||
"Roy",
|
||||
"Albert",
|
||||
"Bruce",
|
||||
"Ronald",
|
||||
"Keith",
|
||||
"Craig",
|
||||
"Roger",
|
||||
"Randy",
|
||||
"Gary",
|
||||
"Dennis",
|
||||
"Edwin",
|
||||
"Don",
|
||||
"Glen",
|
||||
"Gordon",
|
||||
"Howard",
|
||||
"Earl",
|
||||
"Leo",
|
||||
"Lloyd",
|
||||
"Milton",
|
||||
"Norman",
|
||||
"Roland",
|
||||
"Vernon",
|
||||
"Warren",
|
||||
"Alex",
|
||||
"Alexander",
|
||||
"Alfred",
|
||||
"Amir",
|
||||
"Andrew",
|
||||
"Anthony",
|
||||
"Antonio",
|
||||
"Archer",
|
||||
"Arthur",
|
||||
"Asher",
|
||||
"Austin",
|
||||
"Axel",
|
||||
"Barry",
|
||||
"Beau",
|
||||
"Beckett",
|
||||
"Benjamin",
|
||||
"Bennett",
|
||||
"Bernard",
|
||||
"Blake",
|
||||
"Braden",
|
||||
"Bradley",
|
||||
"Brady",
|
||||
"Brandon",
|
||||
"Braxton",
|
||||
"Brayden",
|
||||
"Brendan",
|
||||
"Brett",
|
||||
"Brian",
|
||||
"Bruce",
|
||||
"Bryan",
|
||||
"Bryce",
|
||||
"Bryson",
|
||||
"Caden",
|
||||
"Caleb",
|
||||
"Calvin",
|
||||
"Camden",
|
||||
"Cameron",
|
||||
"Carlos",
|
||||
"Carson",
|
||||
"Carter",
|
||||
"Casey",
|
||||
"Cecil",
|
||||
"Charles",
|
||||
"Chase",
|
||||
"Chester",
|
||||
"Christian",
|
||||
"Christopher",
|
||||
"Clarence",
|
||||
"Claude",
|
||||
"Clifford",
|
||||
"Clyde",
|
||||
"Cody",
|
||||
"Cole",
|
||||
"Colin",
|
||||
"Collin",
|
||||
"Colton",
|
||||
"Connor",
|
||||
"Cooper",
|
||||
"Corey",
|
||||
"Craig",
|
||||
"Curtis",
|
||||
"Dakota",
|
||||
"Dale",
|
||||
"Dalton",
|
||||
"Damian",
|
||||
"Dan",
|
||||
"Daniel",
|
||||
"Darrell",
|
||||
"Daryl",
|
||||
"David",
|
||||
"Dawson",
|
||||
"Dean",
|
||||
"Declan",
|
||||
"Dennis",
|
||||
"Derek",
|
||||
"Devin",
|
||||
"Dominic",
|
||||
"Dominick",
|
||||
"Don",
|
||||
"Douglas",
|
||||
"Dustin",
|
||||
"Dylan",
|
||||
"Earl",
|
||||
"Easton",
|
||||
"Edgar",
|
||||
"Edmund",
|
||||
"Edward",
|
||||
"Edwin",
|
||||
"Eli",
|
||||
"Elias",
|
||||
"Elijah",
|
||||
"Elliot",
|
||||
"Elliott",
|
||||
"Emilio",
|
||||
"Emmett",
|
||||
"Eric",
|
||||
"Ethan",
|
||||
"Eugene",
|
||||
"Evan",
|
||||
"Everett",
|
||||
"Felix",
|
||||
"Ferdinand",
|
||||
"Floyd",
|
||||
"Forrest",
|
||||
"Francis",
|
||||
"Franklin",
|
||||
"Frederick",
|
||||
"Gabriel",
|
||||
"Gael",
|
||||
"Garrett",
|
||||
"Gary",
|
||||
"Gavin",
|
||||
"George",
|
||||
"Gerald",
|
||||
"Gilbert",
|
||||
"Glen",
|
||||
"Gordon",
|
||||
"Graham",
|
||||
"Grant",
|
||||
"Grayson",
|
||||
"Gregory",
|
||||
"Greyson",
|
||||
"Harold",
|
||||
"Harrison",
|
||||
"Harvey",
|
||||
"Hayden",
|
||||
"Henry",
|
||||
"Herman",
|
||||
"Howard",
|
||||
"Hudson",
|
||||
"Hugh",
|
||||
"Hugo",
|
||||
"Hunter",
|
||||
"Ian",
|
||||
"Irving",
|
||||
"Isaac",
|
||||
"Isaiah",
|
||||
"Ivan",
|
||||
"Jace",
|
||||
"Jack",
|
||||
"Jackson",
|
||||
"Jacob",
|
||||
"Jaden",
|
||||
"James",
|
||||
"Jared",
|
||||
"Jason",
|
||||
"Jasper",
|
||||
"Jayden",
|
||||
"Jeffrey",
|
||||
"Jeremiah",
|
||||
"Jeremy",
|
||||
"Jerome",
|
||||
"Jesse",
|
||||
"Jesus",
|
||||
"Joel",
|
||||
"John",
|
||||
"Jonathan",
|
||||
"Jordan",
|
||||
"Jorge",
|
||||
"Jose",
|
||||
"Joseph",
|
||||
"Joshua",
|
||||
"Josiah",
|
||||
"Juan",
|
||||
"Justin",
|
||||
"Kai",
|
||||
"Kayden",
|
||||
"Keith",
|
||||
"Kenneth",
|
||||
"Kevin",
|
||||
"Kingston",
|
||||
"Knox",
|
||||
"Kyle",
|
||||
"Landon",
|
||||
"Lawrence",
|
||||
"Leo",
|
||||
"Leon",
|
||||
"Leonard",
|
||||
"Leonardo",
|
||||
"Leslie",
|
||||
"Levi",
|
||||
"Liam",
|
||||
"Lincoln",
|
||||
"Lloyd",
|
||||
"Logan",
|
||||
"Luca",
|
||||
"Lucas",
|
||||
"Luis",
|
||||
"Luke",
|
||||
"Maddox",
|
||||
"Malachi",
|
||||
"Malcolm",
|
||||
"Marcus",
|
||||
"Mark",
|
||||
"Martin",
|
||||
"Marvin",
|
||||
"Mason",
|
||||
"Mateo",
|
||||
"Matthew",
|
||||
"Maurice",
|
||||
"Max",
|
||||
"Maxwell",
|
||||
"Micah",
|
||||
"Michael",
|
||||
"Miguel",
|
||||
"Miles",
|
||||
"Milton",
|
||||
"Mitchell",
|
||||
"Morris",
|
||||
"Nathan",
|
||||
"Nathaniel",
|
||||
"Neil",
|
||||
"Nelson",
|
||||
"Nicholas",
|
||||
"Noah",
|
||||
"Nolan",
|
||||
"Norman",
|
||||
"Oliver",
|
||||
"Oscar",
|
||||
"Owen",
|
||||
"Parker",
|
||||
"Patrick",
|
||||
"Paul",
|
||||
"Perry",
|
||||
"Peter",
|
||||
"Philip",
|
||||
"Phillip",
|
||||
"Preston",
|
||||
"Ralph",
|
||||
"Randy",
|
||||
"Ray",
|
||||
"Raymond",
|
||||
"Rhett",
|
||||
"Richard",
|
||||
"Riley",
|
||||
"Robert",
|
||||
"Roderick",
|
||||
"Rodney",
|
||||
"Roger",
|
||||
"Roland",
|
||||
"Roman",
|
||||
"Ronald",
|
||||
"Ross",
|
||||
"Roy",
|
||||
"Russell",
|
||||
"Ryan",
|
||||
"Ryder",
|
||||
"Ryker",
|
||||
"Samuel",
|
||||
"Sawyer",
|
||||
"Scott",
|
||||
"Sean",
|
||||
"Sebastian",
|
||||
"Seth",
|
||||
"Shane",
|
||||
"Shawn",
|
||||
"Silas",
|
||||
"Simon",
|
||||
"Spencer",
|
||||
"Stanley",
|
||||
"Stephen",
|
||||
"Steven",
|
||||
"Stuart",
|
||||
"Tanner",
|
||||
"Terrence",
|
||||
"Theodore",
|
||||
"Thomas",
|
||||
"Timothy",
|
||||
"Travis",
|
||||
"Trevor",
|
||||
"Tristan",
|
||||
"Tyler",
|
||||
"Vernon",
|
||||
"Victor",
|
||||
"Vincent",
|
||||
"Wade",
|
||||
"Wallace",
|
||||
"Walter",
|
||||
"Warren",
|
||||
"Wayne",
|
||||
"Wendell",
|
||||
"Barry",
|
||||
"Cecil",
|
||||
"Claude",
|
||||
"Daryl",
|
||||
"Edmund",
|
||||
"Everett",
|
||||
"Ferdinand",
|
||||
"Forrest",
|
||||
"Gerald",
|
||||
"Hugh",
|
||||
"Irving",
|
||||
"Leslie",
|
||||
"Marvin",
|
||||
"Morris",
|
||||
"Nelson",
|
||||
"Perry",
|
||||
"Phillip",
|
||||
"Roderick",
|
||||
"Ross",
|
||||
"Terrence",
|
||||
"Wade",
|
||||
"Wesley",
|
||||
"Weston",
|
||||
"William",
|
||||
"Winston",
|
||||
"Zachariah"
|
||||
"Wyatt",
|
||||
"Xavier",
|
||||
"Zachariah",
|
||||
"Zachary",
|
||||
"Zane"
|
||||
];
|
||||
|
||||
@@ -1,273 +1,369 @@
|
||||
export default [
|
||||
"Smith",
|
||||
"Johnson",
|
||||
"Williams",
|
||||
"Brown",
|
||||
"Jones",
|
||||
"Garcia",
|
||||
"Miller",
|
||||
"Davis",
|
||||
"Rodriguez",
|
||||
"Martinez",
|
||||
"Hernandez",
|
||||
"Lopez",
|
||||
"Gonzalez",
|
||||
"Wilson",
|
||||
"Anderson",
|
||||
"Thomas",
|
||||
"Taylor",
|
||||
"Moore",
|
||||
"Jackson",
|
||||
"Martin",
|
||||
"Lee",
|
||||
"Perez",
|
||||
"Thompson",
|
||||
"White",
|
||||
"Harris",
|
||||
"Sanchez",
|
||||
"Clark",
|
||||
"Ramirez",
|
||||
"Lewis",
|
||||
"Robinson",
|
||||
"Walker",
|
||||
"Young",
|
||||
"Allen",
|
||||
"King",
|
||||
"Wright",
|
||||
"Scott",
|
||||
"Torres",
|
||||
"Nguyen",
|
||||
"Hill",
|
||||
"Flores",
|
||||
"Green",
|
||||
"Abbott",
|
||||
"Adams",
|
||||
"Nelson",
|
||||
"Baker",
|
||||
"Hall",
|
||||
"Rivera",
|
||||
"Campbell",
|
||||
"Mitchell",
|
||||
"Carter",
|
||||
"Roberts",
|
||||
"Gomez",
|
||||
"Phillips",
|
||||
"Evans",
|
||||
"Turner",
|
||||
"Diaz",
|
||||
"Parker",
|
||||
"Cruz",
|
||||
"Edwards",
|
||||
"Collins",
|
||||
"Reyes",
|
||||
"Stewart",
|
||||
"Morris",
|
||||
"Morales",
|
||||
"Murphy",
|
||||
"Cook",
|
||||
"Rogers",
|
||||
"Gutierrez",
|
||||
"Ortiz",
|
||||
"Morgan",
|
||||
"Cooper",
|
||||
"Peterson",
|
||||
"Bailey",
|
||||
"Reed",
|
||||
"Kelly",
|
||||
"Howard",
|
||||
"Ramos",
|
||||
"Kim",
|
||||
"Cox",
|
||||
"Ward",
|
||||
"Richardson",
|
||||
"Watson",
|
||||
"Brooks",
|
||||
"Chavez",
|
||||
"Wood",
|
||||
"James",
|
||||
"Bennett",
|
||||
"Gray",
|
||||
"Mendoza",
|
||||
"Ruiz",
|
||||
"Hughes",
|
||||
"Price",
|
||||
"Alvarez",
|
||||
"Castillo",
|
||||
"Sanders",
|
||||
"Patel",
|
||||
"Myers",
|
||||
"Long",
|
||||
"Ross",
|
||||
"Foster",
|
||||
"Jimenez",
|
||||
"Powell",
|
||||
"Jenkins",
|
||||
"Perry",
|
||||
"Russell",
|
||||
"Sullivan",
|
||||
"Bell",
|
||||
"Coleman",
|
||||
"Butler",
|
||||
"Henderson",
|
||||
"Barnes",
|
||||
"Gonzales",
|
||||
"Fisher",
|
||||
"Vasquez",
|
||||
"Simmons",
|
||||
"Romero",
|
||||
"Jordan",
|
||||
"Patterson",
|
||||
"Alexander",
|
||||
"Hamilton",
|
||||
"Graham",
|
||||
"Reynolds",
|
||||
"Griffin",
|
||||
"Wallace",
|
||||
"Moreno",
|
||||
"West",
|
||||
"Cole",
|
||||
"Hayes",
|
||||
"Bryant",
|
||||
"Herrera",
|
||||
"Gibson",
|
||||
"Ellis",
|
||||
"Tran",
|
||||
"Medina",
|
||||
"Aguilar",
|
||||
"Stevens",
|
||||
"Murray",
|
||||
"Ford",
|
||||
"Castro",
|
||||
"Marshall",
|
||||
"Owens",
|
||||
"Harrison",
|
||||
"Fernandez",
|
||||
"McDonald",
|
||||
"Woods",
|
||||
"Washington",
|
||||
"Kennedy",
|
||||
"Wells",
|
||||
"Vargas",
|
||||
"Henry",
|
||||
"Chen",
|
||||
"Freeman",
|
||||
"Webb",
|
||||
"Tucker",
|
||||
"Guzman",
|
||||
"Burns",
|
||||
"Crawford",
|
||||
"Olson",
|
||||
"Simpson",
|
||||
"Porter",
|
||||
"Hunter",
|
||||
"Gordon",
|
||||
"Mendez",
|
||||
"Silva",
|
||||
"Shaw",
|
||||
"Snyder",
|
||||
"Mason",
|
||||
"Dixon",
|
||||
"Blackwood",
|
||||
"Shepherd",
|
||||
"Frost",
|
||||
"Hawkins",
|
||||
"Pearson",
|
||||
"Fleming",
|
||||
"Dawson",
|
||||
"Palmer",
|
||||
"Nash",
|
||||
"Barker",
|
||||
"Thornton",
|
||||
"Fitzgerald",
|
||||
"Winters",
|
||||
"Mckenzie",
|
||||
"Chandler",
|
||||
"Griffith",
|
||||
"Cunningham",
|
||||
"Doyle",
|
||||
"Fletcher",
|
||||
"Hicks",
|
||||
"Walton",
|
||||
"Briggs",
|
||||
"Pearce",
|
||||
"Nichols",
|
||||
"Blake",
|
||||
"Hodges",
|
||||
"Benson",
|
||||
"Marsh",
|
||||
"Whitaker",
|
||||
"Skinner",
|
||||
"Robbins",
|
||||
"Goodwin",
|
||||
"Kirby",
|
||||
"Savage",
|
||||
"Hensley",
|
||||
"Hancock",
|
||||
"Pratt",
|
||||
"Gallagher",
|
||||
"Yates",
|
||||
"Dennis",
|
||||
"Swanson",
|
||||
"Steele",
|
||||
"Bauer",
|
||||
"Holt",
|
||||
"Barber",
|
||||
"Schultz",
|
||||
"Foley",
|
||||
"Fowler",
|
||||
"Wise",
|
||||
"Malone",
|
||||
"Cannon",
|
||||
"Tate",
|
||||
"Stark",
|
||||
"Welch",
|
||||
"Dyer",
|
||||
"Booth",
|
||||
"Payne",
|
||||
"Shannon",
|
||||
"Harmon",
|
||||
"Woodward",
|
||||
"Morse",
|
||||
"Jacobson",
|
||||
"Knowles",
|
||||
"Blanchard",
|
||||
"Dillon",
|
||||
"Stokes",
|
||||
"Buckley",
|
||||
"Dickerson",
|
||||
"Middleton",
|
||||
"Sellers",
|
||||
"Cobb",
|
||||
"Stephenson",
|
||||
"Roach",
|
||||
"Moody",
|
||||
"Beard",
|
||||
"Mccarthy",
|
||||
"Garner",
|
||||
"Mcguire",
|
||||
"Sloan",
|
||||
"Ballard",
|
||||
"Shields",
|
||||
"Orr",
|
||||
"Savage",
|
||||
"Graves",
|
||||
"Dempsey",
|
||||
"Weeks",
|
||||
"Mckay",
|
||||
"Cooke",
|
||||
"Riddle",
|
||||
"Gates",
|
||||
"Alexander",
|
||||
"Allen",
|
||||
"Alvarez",
|
||||
"Anderson",
|
||||
"Atkins",
|
||||
"Farrell",
|
||||
"Lowery",
|
||||
"Huffman",
|
||||
"Livingston",
|
||||
"Davenport",
|
||||
"Hendricks",
|
||||
"Kerr",
|
||||
"Pollard",
|
||||
"Hoover",
|
||||
"Wolfe",
|
||||
"Bailey",
|
||||
"Baker",
|
||||
"Baldwin",
|
||||
"Ballard",
|
||||
"Barber",
|
||||
"Barker",
|
||||
"Barnes",
|
||||
"Barrett",
|
||||
"Bauer",
|
||||
"Baxter",
|
||||
"Beard",
|
||||
"Bell",
|
||||
"Bennett",
|
||||
"Benson",
|
||||
"Black",
|
||||
"Blackwood",
|
||||
"Blake",
|
||||
"Blanchard",
|
||||
"Booth",
|
||||
"Bowman",
|
||||
"Bradley",
|
||||
"Briggs",
|
||||
"Brooks",
|
||||
"Brown",
|
||||
"Bryant",
|
||||
"Buckley",
|
||||
"Burke",
|
||||
"Burns",
|
||||
"Butler",
|
||||
"Byrd",
|
||||
"Cain",
|
||||
"Campbell",
|
||||
"Cannon",
|
||||
"Carlson",
|
||||
"Carter",
|
||||
"Castillo",
|
||||
"Castro",
|
||||
"Chandler",
|
||||
"Chapman",
|
||||
"Chavez",
|
||||
"Chen",
|
||||
"Clark",
|
||||
"Clarke",
|
||||
"Cobb",
|
||||
"Cole",
|
||||
"Coleman",
|
||||
"Collier",
|
||||
"Collins",
|
||||
"Conrad",
|
||||
"Cook",
|
||||
"Cooke",
|
||||
"Cooper",
|
||||
"Cox",
|
||||
"Craig",
|
||||
"Crawford",
|
||||
"Cross",
|
||||
"Cruz",
|
||||
"Cunningham",
|
||||
"Dalton",
|
||||
"Daniel",
|
||||
"Davenport",
|
||||
"Davis",
|
||||
"Dawson",
|
||||
"Dempsey",
|
||||
"Dennis",
|
||||
"Diaz",
|
||||
"Dickerson",
|
||||
"Dillon",
|
||||
"Dixon",
|
||||
"Doyle",
|
||||
"Duran",
|
||||
"Dyer",
|
||||
"Edwards",
|
||||
"Ellis",
|
||||
"Erickson",
|
||||
"Evans",
|
||||
"Ewing",
|
||||
"Farley",
|
||||
"Farr",
|
||||
"Farrell",
|
||||
"Fernandez",
|
||||
"Ferris",
|
||||
"Fisher",
|
||||
"Fitzgerald",
|
||||
"Fleming",
|
||||
"Fletcher",
|
||||
"Flores",
|
||||
"Foley",
|
||||
"Ford",
|
||||
"Foster",
|
||||
"Fowler",
|
||||
"Frazier",
|
||||
"Freeman",
|
||||
"Frost",
|
||||
"Fuller",
|
||||
"Gallagher",
|
||||
"Garcia",
|
||||
"Gardner",
|
||||
"Garner",
|
||||
"Garrett",
|
||||
"Gates",
|
||||
"Gibbs",
|
||||
"Gibson",
|
||||
"Gill",
|
||||
"Glover",
|
||||
"Goldberg",
|
||||
"Gomez",
|
||||
"Gonzales",
|
||||
"Gonzalez",
|
||||
"Goodman",
|
||||
"Goodwin",
|
||||
"Gordon",
|
||||
"Graham",
|
||||
"Graves",
|
||||
"Gray",
|
||||
"Green",
|
||||
"Griffin",
|
||||
"Griffith",
|
||||
"Gross",
|
||||
"Gutierrez",
|
||||
"Guzman",
|
||||
"Hale",
|
||||
"Hall",
|
||||
"Hamilton",
|
||||
"Hancock",
|
||||
"Harmon",
|
||||
"Harris",
|
||||
"Harrison",
|
||||
"Hawkins",
|
||||
"Hayes",
|
||||
"Hayward",
|
||||
"Henderson",
|
||||
"Hendricks",
|
||||
"Henry",
|
||||
"Hensley",
|
||||
"Hernandez",
|
||||
"Herrera",
|
||||
"Hicks",
|
||||
"Hill",
|
||||
"Hodges",
|
||||
"Holland",
|
||||
"Holmes",
|
||||
"Holt",
|
||||
"Hoover",
|
||||
"Howard",
|
||||
"Hudson",
|
||||
"Huffman",
|
||||
"Hughes",
|
||||
"Humphrey",
|
||||
"Hunter",
|
||||
"Hutchinson",
|
||||
"Isaac",
|
||||
"Jackson",
|
||||
"Jacobson",
|
||||
"James",
|
||||
"Jarvis",
|
||||
"Jenkins",
|
||||
"Jimenez",
|
||||
"Johnson",
|
||||
"Jones",
|
||||
"Jordan",
|
||||
"Keller",
|
||||
"Kelly",
|
||||
"Kennedy",
|
||||
"Kent",
|
||||
"Kerr",
|
||||
"Kim",
|
||||
"King",
|
||||
"Kirby",
|
||||
"Knowles",
|
||||
"Koch",
|
||||
"Landry",
|
||||
"Lang",
|
||||
"Lawson",
|
||||
"Lee",
|
||||
"Levy",
|
||||
"Lewis",
|
||||
"Livingston",
|
||||
"Logan",
|
||||
"Long",
|
||||
"Lopez",
|
||||
"Lowery",
|
||||
"Lowry",
|
||||
"Lucas",
|
||||
"Luna",
|
||||
"Malone",
|
||||
"Manning",
|
||||
"Marsh",
|
||||
"Marshall",
|
||||
"Martin",
|
||||
"Martinez",
|
||||
"Mason",
|
||||
"May",
|
||||
"McBride",
|
||||
"McCoy",
|
||||
"McDonald",
|
||||
"McKinney",
|
||||
"Mccarthy",
|
||||
"Mcguire",
|
||||
"Mckay",
|
||||
"Mckenzie",
|
||||
"Meadows",
|
||||
"Medina",
|
||||
"Mendez",
|
||||
"Mendoza",
|
||||
"Middleton",
|
||||
"Miller",
|
||||
"Miranda",
|
||||
"Mitchell",
|
||||
"Moody",
|
||||
"Moore",
|
||||
"Morales",
|
||||
"Moreno",
|
||||
"Morgan",
|
||||
"Morris",
|
||||
"Morse",
|
||||
"Murphy",
|
||||
"Murray",
|
||||
"Myers",
|
||||
"Nash",
|
||||
"Neal",
|
||||
"Nelson",
|
||||
"Nguyen",
|
||||
"Nichols",
|
||||
"Nielsen",
|
||||
"Nixon",
|
||||
"Norton",
|
||||
"Olson",
|
||||
"Orr",
|
||||
"Ortiz",
|
||||
"Owens",
|
||||
"Palmer",
|
||||
"Parker",
|
||||
"Parsons",
|
||||
"Patel",
|
||||
"Patterson",
|
||||
"Payne",
|
||||
"Pearce",
|
||||
"Pearson",
|
||||
"Perez",
|
||||
"Perry",
|
||||
"Peterson",
|
||||
"Phillips",
|
||||
"Pickett",
|
||||
"Pollard",
|
||||
"Porter",
|
||||
"Potter",
|
||||
"Powell",
|
||||
"Pratt",
|
||||
"Preston",
|
||||
"Price",
|
||||
"Ramirez",
|
||||
"Ramos",
|
||||
"Randall",
|
||||
"Ray",
|
||||
"Reed",
|
||||
"Reyes",
|
||||
"Reynolds",
|
||||
"Richardson",
|
||||
"Riddle",
|
||||
"Riley",
|
||||
"Rivera",
|
||||
"Roach",
|
||||
"Robbins",
|
||||
"Roberts",
|
||||
"Robinson",
|
||||
"Rodriguez",
|
||||
"Rogers",
|
||||
"Romero",
|
||||
"Ross",
|
||||
"Rowe",
|
||||
"Ruiz",
|
||||
"Russell",
|
||||
"Salinas",
|
||||
"Sanchez",
|
||||
"Sanders",
|
||||
"Santana",
|
||||
"Savage",
|
||||
"Sawyer",
|
||||
"Schmidt",
|
||||
"Schultz",
|
||||
"Scott",
|
||||
"Sellers",
|
||||
"Shannon",
|
||||
"Sharp",
|
||||
"Shaw",
|
||||
"Shepherd",
|
||||
"Shields",
|
||||
"Short",
|
||||
"Silva",
|
||||
"Simmons",
|
||||
"Simpson",
|
||||
"Skinner",
|
||||
"Sloan",
|
||||
"Smith",
|
||||
"Snyder",
|
||||
"Sparks",
|
||||
"Spears",
|
||||
"Stanley",
|
||||
"Stark",
|
||||
"Steele",
|
||||
"Stephenson",
|
||||
"Stevens",
|
||||
"Stewart",
|
||||
"Stokes",
|
||||
"Sullivan",
|
||||
"Sutton",
|
||||
"Swanson",
|
||||
"Tanner",
|
||||
"Tate",
|
||||
"Taylor",
|
||||
"Thomas",
|
||||
"Thompson",
|
||||
"Thornton",
|
||||
"Torres",
|
||||
"Tran",
|
||||
"Travis",
|
||||
"Tucker",
|
||||
"Turner",
|
||||
"Tyler",
|
||||
"Underwood",
|
||||
"Frazier"
|
||||
"Vargas",
|
||||
"Vasquez",
|
||||
"Vincent",
|
||||
"Wagner",
|
||||
"Walker",
|
||||
"Wallace",
|
||||
"Walters",
|
||||
"Walton",
|
||||
"Ward",
|
||||
"Washington",
|
||||
"Watkins",
|
||||
"Watson",
|
||||
"Weaver",
|
||||
"Webb",
|
||||
"Weeks",
|
||||
"Welch",
|
||||
"Wells",
|
||||
"West",
|
||||
"Whitaker",
|
||||
"White",
|
||||
"Wilcox",
|
||||
"Wilkinson",
|
||||
"Williams",
|
||||
"Willis",
|
||||
"Wilson",
|
||||
"Winters",
|
||||
"Wise",
|
||||
"Wolfe",
|
||||
"Wood",
|
||||
"Woods",
|
||||
"Woodward",
|
||||
"Wright",
|
||||
"Wyatt",
|
||||
"Yates",
|
||||
"York",
|
||||
"Young",
|
||||
"Zamora",
|
||||
"Zimmerman"
|
||||
];
|
||||
|
||||
@@ -56,19 +56,16 @@ export class UsernameEmailGenerator {
|
||||
break;
|
||||
}
|
||||
|
||||
// Add birth year variations
|
||||
if (this.getSecureRandom(3) !== 0) {
|
||||
switch (this.getSecureRandom(2)) {
|
||||
case 0:
|
||||
parts.push(identity.birthDate.getFullYear().toString().substring(2));
|
||||
break;
|
||||
case 1:
|
||||
parts.push(identity.birthDate.getFullYear().toString());
|
||||
break;
|
||||
}
|
||||
} else if (this.getSecureRandom(2) === 0) {
|
||||
// Add random numbers for more uniqueness
|
||||
parts.push((this.getSecureRandom(990) + 10).toString());
|
||||
// Always add birth year variations for uniqueness
|
||||
switch (this.getSecureRandom(2)) {
|
||||
case 0:
|
||||
// Full year (e.g., 1990)
|
||||
parts.push(identity.birthDate.getFullYear().toString());
|
||||
break;
|
||||
case 1:
|
||||
// Last two digits of year (e.g., 90)
|
||||
parts.push(identity.birthDate.getFullYear().toString().substring(2));
|
||||
break;
|
||||
}
|
||||
|
||||
// Join parts with random symbols, possibly multiple
|
||||
|
||||
Reference in New Issue
Block a user