Update env variables and refactor metadata storage (#1371)

This commit is contained in:
Leendert de Borst
2025-11-20 07:59:41 +01:00
parent a937098315
commit 4d613175ed
54 changed files with 336 additions and 226 deletions

View File

@@ -37,11 +37,19 @@ FORCE_HTTPS_REDIRECT=true
# your DNS. Please refer to the full documentation for more instructions on DNS:
# https://docs.aliasvault.net/installation/install.html#3-email-server-setup
#
# Set the private email domains below that are allowed to be used (comma separated values).
# Set the private email domains below that the server should accept incoming mail for (comma separated values).
# Example: PRIVATE_EMAIL_DOMAINS=example.com,example2.org
# To disable the private email domains feature, keep this empty.
PRIVATE_EMAIL_DOMAINS=
# Set private email domains that should be hidden from UI components (comma separated values).
# These domains will still function as private email domains for receiving email and claims,
# but will not appear in domain selection dropdowns or settings. This is useful for deprecating
# legacy domains while maintaining backwards compatibility.
# Example: HIDDEN_PRIVATE_EMAIL_DOMAINS=old-domain.com,deprecated.org
# Note: Domains listed here should ALSO be included in PRIVATE_EMAIL_DOMAINS above.
HIDDEN_PRIVATE_EMAIL_DOMAINS=
# Enable TLS for SMTP.
# ⚠️ Requires valid TLS certificates on your mail server (not provided by the AliasVault installer).
# If set to true without proper certificates, the SMTP service will fail to start.

View File

@@ -105,6 +105,10 @@ export async function handleStoreVault(
await storage.setItem('session:privateEmailDomains', vaultRequest.privateEmailDomainList);
}
if (vaultRequest.hiddenPrivateEmailDomainList) {
await storage.setItem('session:hiddenPrivateEmailDomains', vaultRequest.hiddenPrivateEmailDomainList);
}
if (vaultRequest.vaultRevisionNumber) {
await storage.setItem('session:vaultRevisionNumber', vaultRequest.vaultRevisionNumber);
}
@@ -168,6 +172,7 @@ export async function handleSyncVault(
{ key: 'session:encryptedVault', value: vaultResponse.vault.blob },
{ key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList },
{ key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList },
{ key: 'session:hiddenPrivateEmailDomains', value: vaultResponse.vault.hiddenPrivateEmailDomainList },
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber }
]);
}
@@ -186,6 +191,7 @@ export async function handleGetVault(
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
const hiddenPrivateEmailDomains = await storage.getItem('session:hiddenPrivateEmailDomains') as string[] ?? [];
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
if (!encryptedVault) {
@@ -208,6 +214,7 @@ export async function handleGetVault(
vault: decryptedVault,
publicEmailDomains: publicEmailDomains ?? [],
privateEmailDomains: privateEmailDomains ?? [],
hiddenPrivateEmailDomains: hiddenPrivateEmailDomains ?? [],
vaultRevisionNumber: vaultRevisionNumber ?? 0
};
} catch (error) {
@@ -229,6 +236,7 @@ export function handleClearVault(
'session:encryptionKeyDerivationParams',
'session:publicEmailDomains',
'session:privateEmailDomains',
'session:hiddenPrivateEmailDomains',
'session:vaultRevisionNumber'
]);
@@ -497,13 +505,11 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
credentialsCount: sqliteClient.getAllCredentials().length,
currentRevisionNumber: vaultRevisionNumber,
emailAddressList: emailAddresses,
privateEmailDomainList: [], // Empty on purpose, API will not use this for vault updates.
publicEmailDomainList: [], // Empty on purpose, API will not use this for vault updates.
encryptionPublicKey: '', // Empty on purpose, only required if new public/private key pair is generated.
client: '', // Empty on purpose, API will not use this for vault updates.
updatedAt: new Date().toISOString(),
username: username,
version: (await sqliteClient.getDatabaseVersion()).version
version: (await sqliteClient.getDatabaseVersion()).version,
// TODO: add public RSA encryption key to payload when implementing vault creation from browser extension. Currently only web app does this.
encryptionPublicKey: '',
};
const webApi = new WebApiService(() => {});

View File

@@ -45,18 +45,18 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const [selectedDomain, setSelectedDomain] = useState('');
const [isPopupVisible, setIsPopupVisible] = useState(false);
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState<string[]>([]);
const popupRef = useRef<HTMLDivElement>(null);
// Get private email domains from vault metadata
useEffect(() => {
/**
* Load private email domains from vault metadata.
* Load private email domains from vault metadata, excluding hidden ones.
*/
const loadDomains = async (): Promise<void> => {
const metadata = await dbContext.getVaultMetadata();
if (metadata?.privateEmailDomains) {
setPrivateEmailDomains(metadata.privateEmailDomains);
}
setPrivateEmailDomains(metadata?.privateEmailDomains ?? []);
setHiddenPrivateEmailDomains(metadata?.hiddenPrivateEmailDomains ?? []);
};
loadDomains();
}, [dbContext]);
@@ -84,9 +84,10 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
setLocalPart(local);
setSelectedDomain(domain);
// Check if it's a custom domain
// Check if it's a custom domain (including hidden private domains as known domains)
const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
privateEmailDomains.includes(domain);
privateEmailDomains.includes(domain) ||
hiddenPrivateEmailDomains.includes(domain);
setIsCustomDomain(!isKnownDomain);
} else {
setLocalPart(value);
@@ -102,7 +103,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, privateEmailDomains, showPrivateDomains]);
}, [value, privateEmailDomains, hiddenPrivateEmailDomains, showPrivateDomains]);
// Handle local part changes
const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
@@ -246,20 +247,22 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
{t('credentials.privateEmailDescription')}
</p>
<div className="flex flex-wrap gap-2">
{privateEmailDomains.map((domain) => (
<button
key={domain}
type="button"
onClick={() => selectDomain(domain)}
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
selectedDomain === domain
? 'bg-primary-600 text-white hover:bg-primary-700'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600'
}`}
>
{domain}
</button>
))}
{privateEmailDomains
.filter((domain) => !hiddenPrivateEmailDomains.includes(domain))
.map((domain) => (
<button
key={domain}
type="button"
onClick={() => selectDomain(domain)}
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
selectedDomain === domain
? 'bg-primary-600 text-white hover:bg-primary-700'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600'
}`}
>
{domain}
</button>
))}
</div>
</div>
)}

View File

@@ -62,8 +62,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
setDbInitialized(true);
setDbAvailable(true);
setVaultMetadata({
publicEmailDomains: vaultResponse.vault.publicEmailDomainList,
privateEmailDomains: vaultResponse.vault.privateEmailDomainList,
publicEmailDomains: vaultResponse.vault.publicEmailDomainList ?? [],
privateEmailDomains: vaultResponse.vault.privateEmailDomainList ?? [],
hiddenPrivateEmailDomains: vaultResponse.vault.hiddenPrivateEmailDomainList ?? [],
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
});
@@ -74,6 +75,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
vaultBlob: vaultResponse.vault.blob,
publicEmailDomainList: vaultResponse.vault.publicEmailDomainList,
privateEmailDomainList: vaultResponse.vault.privateEmailDomainList,
hiddenPrivateEmailDomainList: vaultResponse.vault.hiddenPrivateEmailDomainList,
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
};
@@ -96,6 +98,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
setVaultMetadata({
publicEmailDomains: response.publicEmailDomains ?? [],
privateEmailDomains: response.privateEmailDomains ?? [],
hiddenPrivateEmailDomains: response.hiddenPrivateEmailDomains ?? [],
vaultRevisionNumber: response.vaultRevisionNumber ?? 0,
});
} else {
@@ -123,6 +126,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
setVaultMetadata({
publicEmailDomains: vaultMetadata?.publicEmailDomains ?? [],
privateEmailDomains: vaultMetadata?.privateEmailDomains ?? [],
hiddenPrivateEmailDomains: vaultMetadata?.hiddenPrivateEmailDomains ?? [],
vaultRevisionNumber: revisionNumber,
});
}, [vaultMetadata]);

View File

@@ -1,6 +1,7 @@
type VaultMetadata = {
publicEmailDomains: string[];
privateEmailDomains: string[];
hiddenPrivateEmailDomains: string[];
vaultRevisionNumber: number;
};

View File

@@ -28,18 +28,18 @@ type ApiErrorResponse = {
* Vault type.
*/
type Vault = {
blob: string;
createdAt: string;
credentialsCount: number;
currentRevisionNumber: number;
emailAddressList: string[];
privateEmailDomainList: string[];
publicEmailDomainList: string[];
encryptionPublicKey: string;
updatedAt: string;
username: string;
blob: string;
version: string;
client: string;
currentRevisionNumber: number;
credentialsCount: number;
createdAt: string;
updatedAt: string;
encryptionPublicKey?: string;
emailAddressList?: string[];
privateEmailDomainList?: string[];
hiddenPrivateEmailDomainList?: string[];
publicEmailDomainList?: string[];
};
/**

View File

@@ -2,5 +2,6 @@ export type StoreVaultRequest = {
vaultBlob: string;
publicEmailDomainList?: string[];
privateEmailDomainList?: string[];
hiddenPrivateEmailDomainList?: string[];
vaultRevisionNumber?: number;
}

View File

@@ -3,5 +3,6 @@ export type VaultResponse = {
vault?: string,
publicEmailDomains?: string[],
privateEmailDomains?: string[],
hiddenPrivateEmailDomains?: string[],
vaultRevisionNumber?: number
};

View File

@@ -47,6 +47,7 @@ class VaultMetadataManager(
JSONObject().apply {
put("publicEmailDomains", JSONArray(updatedMetadata.publicEmailDomains))
put("privateEmailDomains", JSONArray(updatedMetadata.privateEmailDomains))
put("hiddenPrivateEmailDomains", JSONArray(updatedMetadata.hiddenPrivateEmailDomains))
put("vaultRevisionNumber", updatedMetadata.vaultRevisionNumber)
}.toString(),
)
@@ -158,6 +159,9 @@ class VaultMetadataManager(
privateEmailDomains = json.optJSONArray("privateEmailDomains")?.let { array ->
List(array.length()) { i -> array.getString(i) }
} ?: emptyList(),
hiddenPrivateEmailDomains = json.optJSONArray("hiddenPrivateEmailDomains")?.let { array ->
List(array.length()) { i -> array.getString(i) }
} ?: emptyList(),
vaultRevisionNumber = json.optInt("vaultRevisionNumber", 0),
)
} catch (e: Exception) {

View File

@@ -33,13 +33,10 @@ class VaultMutate(
json.put("credentialsCount", vault.credentialsCount)
json.put("currentRevisionNumber", vault.currentRevisionNumber)
json.put("emailAddressList", JSONArray(vault.emailAddressList))
json.put("privateEmailDomainList", JSONArray(vault.privateEmailDomainList))
json.put("publicEmailDomainList", JSONArray(vault.publicEmailDomainList))
json.put("encryptionPublicKey", vault.encryptionPublicKey)
json.put("updatedAt", vault.updatedAt)
json.put("username", vault.username)
json.put("version", vault.version)
json.put("client", vault.client)
val response = webApiService.executeRequest(
method = "POST",
@@ -124,8 +121,6 @@ class VaultMutate(
} catch (e: Exception) {
"0.0.0"
}
val baseVersion = version.split("-").firstOrNull() ?: "0.0.0"
val client = "android-$baseVersion"
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US)
dateFormat.timeZone = java.util.TimeZone.getTimeZone("UTC")
@@ -137,13 +132,11 @@ class VaultMutate(
credentialsCount = credentials.size,
currentRevisionNumber = currentRevision,
emailAddressList = privateEmailAddresses,
privateEmailDomainList = emptyList(),
publicEmailDomainList = emptyList(),
// TODO: add public RSA encryption key to payload when implementing vault creation from mobile app. Currently only web app does this.
encryptionPublicKey = "",
updatedAt = now,
username = username,
version = dbVersion,
client = client,
)
}
@@ -157,13 +150,10 @@ class VaultMutate(
val credentialsCount: Int,
val currentRevisionNumber: Int,
val emailAddressList: List<String>,
val privateEmailDomainList: List<String>,
val publicEmailDomainList: List<String>,
val encryptionPublicKey: String,
val updatedAt: String,
val username: String,
val version: String,
val client: String,
)
private data class VaultPostResponse(

View File

@@ -173,12 +173,34 @@ class VaultSync(
database.storeEncryptedDatabase(vault.vault.blob)
metadata.setVaultRevisionNumber(newRevision)
// Store vault metadata (public/private email domains)
val vaultMetadata = net.aliasvault.app.vaultstore.models.VaultMetadata(
publicEmailDomains = vault.vault.publicEmailDomainList,
privateEmailDomains = vault.vault.privateEmailDomainList,
hiddenPrivateEmailDomains = vault.vault.hiddenPrivateEmailDomainList,
vaultRevisionNumber = newRevision,
)
storeVaultMetadata(vaultMetadata)
if (database.isVaultUnlocked()) {
// Re-unlock with new data
// Note: This requires auth methods to be passed, handled by VaultStore
}
}
/**
* Store vault metadata as JSON string.
*/
private fun storeVaultMetadata(vaultMetadata: net.aliasvault.app.vaultstore.models.VaultMetadata) {
val json = JSONObject().apply {
put("publicEmailDomains", org.json.JSONArray(vaultMetadata.publicEmailDomains))
put("privateEmailDomains", org.json.JSONArray(vaultMetadata.privateEmailDomains))
put("hiddenPrivateEmailDomains", org.json.JSONArray(vaultMetadata.hiddenPrivateEmailDomains))
put("vaultRevisionNumber", vaultMetadata.vaultRevisionNumber)
}
metadata.storeMetadata(json.toString())
}
private fun parseVaultResponse(body: String): VaultResponse {
return try {
val json = JSONObject(body)
@@ -196,6 +218,12 @@ class VaultSync(
privateList.add(privateArray.getString(i))
}
val hiddenPrivateList = mutableListOf<String>()
val hiddenPrivateArray = vaultJson.getJSONArray("hiddenPrivateEmailDomainList")
for (i in 0 until hiddenPrivateArray.length()) {
hiddenPrivateList.add(hiddenPrivateArray.getString(i))
}
val publicList = mutableListOf<String>()
val publicArray = vaultJson.getJSONArray("publicEmailDomainList")
for (i in 0 until publicArray.length()) {
@@ -213,6 +241,7 @@ class VaultSync(
credentialsCount = vaultJson.getInt("credentialsCount"),
emailAddressList = emailList,
privateEmailDomainList = privateList,
hiddenPrivateEmailDomainList = hiddenPrivateList,
publicEmailDomainList = publicList,
createdAt = vaultJson.getString("createdAt"),
updatedAt = vaultJson.getString("updatedAt"),
@@ -253,6 +282,7 @@ class VaultSync(
val credentialsCount: Int,
val emailAddressList: List<String>,
val privateEmailDomainList: List<String>,
val hiddenPrivateEmailDomainList: List<String>,
val publicEmailDomainList: List<String>,
val createdAt: String,
val updatedAt: String,

View File

@@ -14,6 +14,12 @@ data class VaultMetadata(
*/
val privateEmailDomains: List<String> = emptyList(),
/**
* The hidden private email domains of the vault.
* These domains still function as private email domains but are hidden from UI components.
*/
val hiddenPrivateEmailDomains: List<String> = emptyList(),
/**
* The revision number of the vault.
*/

View File

@@ -35,7 +35,8 @@ class VaultStoreTest {
val metadata = """
{
"publicEmailDomains": ["spamok.com", "spamok.nl"],
"privateEmailDomains": ["aliasvault.net", "main.aliasvault.net"],
"privateEmailDomains": ["aliasvault.net", "main.aliasvault.net", "hidden.aliasvault.net"],
"hiddenPrivateEmailDomains": ["hidden.aliasvault.net"],
"vaultRevisionNumber": 1
}
"""

View File

@@ -261,7 +261,6 @@ export default function Initialize() : React.ReactNode {
// Now perform vault sync (network operations - these are skippable)
await syncVault({
initialSync: true,
abortSignal: abortControllerRef.current.signal,
/**
* Handle the status update.

View File

@@ -10,7 +10,7 @@ import { StyleSheet, View, Text, SafeAreaView, TextInput, ActivityIndicator, Ani
import { useApiUrl } from '@/utils/ApiUrlUtility';
import ConversionUtility from '@/utils/ConversionUtility';
import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata';
import type { LoginResponse, VaultResponse } from '@/utils/dist/shared/models/webapi';
import type { LoginResponse } from '@/utils/dist/shared/models/webapi';
import EncryptionUtility from '@/utils/EncryptionUtility';
import { SrpUtility } from '@/utils/SrpUtility';
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
@@ -77,14 +77,12 @@ export default function LoginScreen() : React.ReactNode {
* Process the vault response by storing the vault and logging in the user.
* @param token - The token to use for the vault
* @param refreshToken - The refresh token to use for the vault
* @param vaultResponseJson - The vault response
* @param passwordHashBase64 - The password hash base64
* @param initiateLoginResponse - The initiate login response
*/
const processVaultResponse = async (
token: string,
refreshToken: string,
vaultResponseJson: VaultResponse,
passwordHashBase64: string,
initiateLoginResponse: LoginResponse
) : Promise<void> => {
@@ -109,7 +107,6 @@ export default function LoginScreen() : React.ReactNode {
await continueProcessVaultResponse(
token,
refreshToken,
vaultResponseJson,
passwordHashBase64,
initiateLoginResponse
);
@@ -126,7 +123,6 @@ export default function LoginScreen() : React.ReactNode {
await continueProcessVaultResponse(
token,
refreshToken,
vaultResponseJson,
passwordHashBase64,
initiateLoginResponse
);
@@ -140,7 +136,6 @@ export default function LoginScreen() : React.ReactNode {
await continueProcessVaultResponse(
token,
refreshToken,
vaultResponseJson,
passwordHashBase64,
initiateLoginResponse
);
@@ -151,7 +146,6 @@ export default function LoginScreen() : React.ReactNode {
* Continue processing the vault response after biometric choice
* @param token - The token to use for the vault
* @param refreshToken - The refresh token to use for the vault
* @param vaultResponseJson - The vault response
* @param passwordHashBase64 - The password hash base64
* @param initiateLoginResponse - The initiate login response
* @param encryptionKeyDerivationParams - The encryption key derivation parameters
@@ -159,7 +153,6 @@ export default function LoginScreen() : React.ReactNode {
const continueProcessVaultResponse = async (
token: string,
refreshToken: string,
vaultResponseJson: VaultResponse,
passwordHashBase64: string,
initiateLoginResponse: LoginResponse
) : Promise<void> => {
@@ -169,20 +162,29 @@ export default function LoginScreen() : React.ReactNode {
salt: initiateLoginResponse.salt,
};
// Set auth tokens, store encryption key and key derivation params, and initialize database
/*
* Set auth tokens, store encryption key and key derivation params.
* Note: We don't call initializeDatabase here anymore - instead, syncVault will download
* the vault and store it (including metadata) through native code.
*/
await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), token, refreshToken);
await dbContext.storeEncryptionKey(passwordHashBase64);
await dbContext.storeEncryptionKeyDerivationParams(encryptionKeyDerivationParams);
await dbContext.initializeDatabase(vaultResponseJson);
let checkSuccess = true;
/**
* After setting auth tokens, execute a server status check immediately
* which takes care of certain sanity checks such as ensuring client/server
* compatibility.
* compatibility. This also downloads the vault and stores it (including metadata)
* through native code.
*/
await syncVault({
initialSync: true,
/**
* Update login status during sync.
*/
onStatus: (status) => {
setLoginStatus(status);
},
/**
* Handle the status update.
*/
@@ -218,6 +220,12 @@ export default function LoginScreen() : React.ReactNode {
return;
}
/*
* After syncVault completes, the vault has been downloaded and stored by native code.
* Immediately mark the database as available without file system checks for faster bootstrap.
*/
dbContext.setDatabaseAvailable();
await authContext.login();
authContext.setOfflineMode(false);
@@ -286,23 +294,10 @@ export default function LoginScreen() : React.ReactNode {
setLoginStatus(t('auth.syncingVault'));
await new Promise(resolve => requestAnimationFrame(resolve));
const vaultResponseJson = await webApi.authFetch<VaultResponse>('Vault', { method: 'GET', headers: {
'Authorization': `Bearer ${validationResponse.token.token}`
} });
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
if (vaultError) {
console.error('vaultError', vaultError);
setError(vaultError);
setIsLoading(false);
setLoginStatus(null);
return;
}
await processVaultResponse(
validationResponse.token.token,
validationResponse.token.refreshToken,
vaultResponseJson,
passwordHashBase64,
initiateLoginResponse
);
@@ -357,21 +352,10 @@ export default function LoginScreen() : React.ReactNode {
setLoginStatus(t('auth.syncingVault'));
await new Promise(resolve => requestAnimationFrame(resolve));
const vaultResponseJson = await webApi.authFetch<VaultResponse>('Vault', { method: 'GET', headers: {
'Authorization': `Bearer ${validationResponse.token.token}`
} });
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
if (vaultError) {
setError(vaultError);
setIsLoading(false);
return;
}
await processVaultResponse(
validationResponse.token.token,
validationResponse.token.refreshToken,
vaultResponseJson,
passwordHashBase64,
initiateLoginResponse
);

View File

@@ -245,7 +245,6 @@ export default function ReinitializeScreen() : React.ReactNode {
// Now perform vault sync (network operations - these are skippable)
await syncVault({
initialSync: true,
/**
* Handle the status update.
*/

View File

@@ -75,7 +75,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
}, [dbContext]);
/**
* Check if the email is a private domain.
* Check if the email is a private domain (including hidden domains).
*/
const isPrivateDomain = useCallback(async (emailAddress: string): Promise<boolean> => {
// Get private domains from stored metadata
@@ -84,7 +84,9 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
return false;
}
return metadata.privateEmailDomains.includes(emailAddress.split('@')[1]);
const domain = emailAddress.split('@')[1];
return metadata.privateEmailDomains.includes(domain) ||
(metadata.hiddenPrivateEmailDomains || []).includes(domain);
}, [dbContext]);
// Handle app state changes

View File

@@ -49,18 +49,21 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const [selectedDomain, setSelectedDomain] = useState('');
const [isModalVisible, setIsModalVisible] = useState(false);
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState<string[]>([]);
// Get private email domains from vault metadata
useEffect(() => {
/**
* Load private email domains from vault metadata.
* Load private email domains from vault metadata, excluding hidden ones from UI.
*/
const loadDomains = async (): Promise<void> => {
try {
const metadata = await dbContext.getVaultMetadata();
if (metadata?.privateEmailDomains) {
setPrivateEmailDomains(metadata.privateEmailDomains);
}
setPrivateEmailDomains(metadata?.privateEmailDomains ?? []);
setHiddenPrivateEmailDomains(metadata?.hiddenPrivateEmailDomains ?? []);
console.log('privateEmailDomains', metadata?.privateEmailDomains);
console.log('hiddenPrivateEmailDomains', metadata?.hiddenPrivateEmailDomains);
} catch (err) {
console.error('Error loading email domains:', err);
}
@@ -91,9 +94,10 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
setLocalPart(local);
setSelectedDomain(domain);
// Check if it's a custom domain
// Check if it's a custom domain (including hidden private domains as known domains)
const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
privateEmailDomains.includes(domain);
privateEmailDomains.includes(domain) ||
hiddenPrivateEmailDomains.includes(domain);
setIsCustomDomain(!isKnownDomain);
} else {
setLocalPart(value);
@@ -108,7 +112,7 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
}
}
}
}, [value, privateEmailDomains, showPrivateDomains]);
}, [value, privateEmailDomains, hiddenPrivateEmailDomains, showPrivateDomains]);
// Handle local part changes
const handleLocalPartChange = useCallback((newText: string) => {
@@ -410,7 +414,7 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
{t('credentials.privateEmailDescription')}
</Text>
<View style={styles.domainList}>
{privateEmailDomains.map((domain) => (
{privateEmailDomains.filter(domain => !hiddenPrivateEmailDomains.includes(domain)).map((domain) => (
<TouchableOpacity
key={domain}
style={[

View File

@@ -18,6 +18,8 @@ type DbContextType = {
getVaultMetadata: () => Promise<VaultMetadata | null>;
testDatabaseConnection: (derivedKey: string) => Promise<boolean>;
unlockVault: () => Promise<boolean>;
checkStoredVault: () => Promise<void>;
setDatabaseAvailable: () => void;
}
const DbContext = createContext<DbContextType | undefined>(undefined);
@@ -80,13 +82,16 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
/**
* Initialize the database in the native module.
* This is called during initial login/registration to set up the vault.
* Note: During sync operations, metadata is stored automatically by native VaultSync.
*
* @param vaultResponse The vault response from the API
*/
const initializeDatabase = useCallback(async (vaultResponse: VaultResponse) => {
const metadata: VaultMetadata = {
publicEmailDomains: vaultResponse.vault.publicEmailDomainList,
privateEmailDomains: vaultResponse.vault.privateEmailDomainList,
publicEmailDomains: vaultResponse.vault.publicEmailDomainList ?? [],
privateEmailDomains: vaultResponse.vault.privateEmailDomainList ?? [],
hiddenPrivateEmailDomains: vaultResponse.vault.hiddenPrivateEmailDomainList ?? [],
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
};
@@ -94,7 +99,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
await sqliteClient.storeEncryptedDatabase(vaultResponse.vault.blob);
await sqliteClient.storeMetadata(JSON.stringify(metadata));
// Initialize the database in the native module
// Unlock the vault to make it available for queries
await unlockVault();
setDbInitialized(true);
@@ -156,6 +161,15 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
NativeVaultManager.clearVault();
}, []);
/**
* Manually set the database as available. Used after vault sync to immediately
* mark the database as ready without file system checks.
*/
const setDatabaseAvailable = useCallback(() : void => {
setDbInitialized(true);
setDbAvailable(true);
}, []);
/**
* Get the current vault metadata directly from SQLite client
*/
@@ -199,7 +213,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
unlockVault,
storeEncryptionKey,
storeEncryptionKeyDerivationParams,
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, hasPendingMigrations, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams]);
checkStoredVault,
setDatabaseAvailable,
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, hasPendingMigrations, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams, checkStoredVault, setDatabaseAvailable]);
return (
<DbContext.Provider value={contextValue}>

View File

@@ -186,12 +186,10 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
}
// Database connection failed, navigate to reinitialize flow
console.log('database connection failed, navigating to reinitialize');
router.replace('/reinitialize');
}
} catch {
// Database query failed, navigate to reinitialize flow
console.log('database query failed, navigating to reinitialize');
router.replace('/reinitialize');
}
}

View File

@@ -76,6 +76,7 @@ export function useVaultMutate() : {
currentRevisionNumber: currentRevision,
emailAddressList: privateEmailAddresses,
privateEmailDomainList: [],
hiddenPrivateEmailDomainList: [],
publicEmailDomainList: [],
encryptionPublicKey: '',
client: '',

View File

@@ -16,16 +16,7 @@ import { VaultSyncErrorCode, getVaultSyncErrorCode } from '@/utils/types/errors/
/**
* Utility function to ensure a minimum time has elapsed for an operation
*/
const withMinimumDelay = async <T>(
operation: () => Promise<T>,
minDelayMs: number,
enableDelay: boolean = true
): Promise<T> => {
if (!enableDelay) {
// If delay is disabled, return the result immediately.
return operation();
}
const withMinimumDelay = async <T>(operation: () => Promise<T>, minDelayMs: number): Promise<T> => {
const startTime = Date.now();
const result = await operation();
const elapsedTime = Date.now() - startTime;
@@ -38,7 +29,6 @@ const withMinimumDelay = async <T>(
};
type VaultSyncOptions = {
initialSync?: boolean;
onSuccess?: (hasNewVault: boolean) => void;
onError?: (error: string) => void;
onStatus?: (message: string) => void;
@@ -59,10 +49,7 @@ export const useVaultSync = () : {
const dbContext = useDb();
const syncVault = useCallback(async (options: VaultSyncOptions = {}) => {
const { initialSync = false, onSuccess, onError, onStatus, onOffline, onUpgradeRequired, abortSignal } = options;
// For the initial sync, we add an artifical delay to various steps which makes it feel more fluid.
const enableDelay = initialSync;
const { onSuccess, onError, onStatus, onOffline, onUpgradeRequired, abortSignal } = options;
try {
// Check if operation was aborted
@@ -87,11 +74,6 @@ export const useVaultSync = () : {
// Update status
onStatus?.(t('vault.checkingVaultUpdates'));
// Add artificial delay for initial sync UX
if (enableDelay) {
await new Promise(resolve => setTimeout(resolve, 300));
}
// Check if operation was aborted
if (abortSignal?.aborted) {
console.debug('VaultSync: Operation aborted after status update');
@@ -128,8 +110,7 @@ export const useVaultSync = () : {
// Run downloadVault with a min delay for UX purposes
await withMinimumDelay(
() => NativeVaultManager.downloadVault(newRevision!),
enableDelay ? 500 : 300,
true
300
);
}
} catch (err) {
@@ -183,11 +164,6 @@ export const useVaultSync = () : {
return false;
}
// Add artificial delay for initial sync UX
if (enableDelay) {
await new Promise(resolve => setTimeout(resolve, hasNewVault ? 1000 : 300));
}
onSuccess?.(hasNewVault);
// Register credential identities after sync

View File

@@ -3,11 +3,13 @@ import Foundation
public struct VaultMetadata: Codable {
public var publicEmailDomains: [String]?
public var privateEmailDomains: [String]?
public var hiddenPrivateEmailDomains: [String]?
public var vaultRevisionNumber: Int
public init(publicEmailDomains: [String]? = nil, privateEmailDomains: [String]? = nil, vaultRevisionNumber: Int) {
public init(publicEmailDomains: [String]? = nil, privateEmailDomains: [String]? = nil, hiddenPrivateEmailDomains: [String]? = nil, vaultRevisionNumber: Int) {
self.publicEmailDomains = publicEmailDomains
self.privateEmailDomains = privateEmailDomains
self.hiddenPrivateEmailDomains = hiddenPrivateEmailDomains
self.vaultRevisionNumber = vaultRevisionNumber
}
}

View File

@@ -42,6 +42,7 @@ extension VaultStore {
metadata = VaultMetadata(
publicEmailDomains: [],
privateEmailDomains: [],
hiddenPrivateEmailDomains: [],
vaultRevisionNumber: revisionNumber
)
}

View File

@@ -8,13 +8,10 @@ public struct VaultUpload: Codable {
public let credentialsCount: Int
public let currentRevisionNumber: Int
public let emailAddressList: [String]
public let privateEmailDomainList: [String]
public let publicEmailDomainList: [String]
public let encryptionPublicKey: String
public let updatedAt: String
public let username: String
public let version: String
public let client: String
}
/// Vault POST response from API
@@ -86,11 +83,6 @@ extension VaultStore {
// Get database version
let dbVersion = try getDatabaseVersion()
// Get client version
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
let baseVersion = version.split(separator: "-").first.map(String.init) ?? "0.0.0"
let client = "ios-\(baseVersion)"
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let now = dateFormatter.string(from: Date())
@@ -101,13 +93,11 @@ extension VaultStore {
credentialsCount: credentials.count,
currentRevisionNumber: currentRevision,
emailAddressList: privateEmailAddresses,
privateEmailDomainList: [], // Empty on purpose, API will not use this for vault updates
publicEmailDomainList: [], // Empty on purpose, API will not use this for vault updates
encryptionPublicKey: "", // Empty on purpose, only required if new public/private key pair is generated
// TODO: add public RSA encryption key to payload when implementing vault creation from mobile app. Currently only web app does this.
encryptionPublicKey: "",
updatedAt: now,
username: username,
version: dbVersion,
client: client
)
}

View File

@@ -19,6 +19,7 @@ public struct VaultData: Codable {
public let credentialsCount: Int
public let emailAddressList: [String]
public let privateEmailDomainList: [String]
public let hiddenPrivateEmailDomainList: [String]
public let publicEmailDomainList: [String]
public let createdAt: String
public let updatedAt: String
@@ -177,11 +178,31 @@ extension VaultStore {
try storeEncryptedDatabase(vault.vault.blob)
setCurrentVaultRevisionNumber(newRevision)
// Store vault metadata (public/private email domains)
let metadata = VaultMetadata(
publicEmailDomains: vault.vault.publicEmailDomainList,
privateEmailDomains: vault.vault.privateEmailDomainList,
hiddenPrivateEmailDomains: vault.vault.hiddenPrivateEmailDomainList,
vaultRevisionNumber: newRevision
)
try storeVaultMetadata(metadata)
if isVaultUnlocked {
try unlockVault()
}
}
/// Store vault metadata
private func storeVaultMetadata(_ metadata: VaultMetadata) throws {
let encoder = JSONEncoder()
guard let metadataData = try? encoder.encode(metadata),
let metadataJson = String(data: metadataData, encoding: .utf8) else {
throw VaultSyncError.parseError(message: "Failed to encode vault metadata")
}
try storeMetadata(metadataJson)
}
/// Parse vault response from JSON
private func parseVaultResponse(_ body: String) throws -> VaultResponse {
guard let vaultData = body.data(using: .utf8) else {

View File

@@ -30,7 +30,9 @@ class SqliteClient {
* Store the vault metadata via the native code implementation.
*
* Metadata is stored in plain text in UserDefaults. The metadata consists of the following:
* - public and private email domains
* - public email domains
* - private email domains
* - hidden private email domains
* - vault revision number
*/
public async storeMetadata(metadata: string): Promise<void> {
@@ -77,7 +79,7 @@ class SqliteClient {
return null;
}
const { privateEmailDomains, publicEmailDomains } = metadata;
const { privateEmailDomains, publicEmailDomains, hiddenPrivateEmailDomains } = metadata;
/**
* Check if a domain is valid (not empty, not 'DISABLED.TLD', and exists in either private or public domains)
@@ -98,7 +100,8 @@ class SqliteClient {
}
// If default domain is not valid, fall back to first available private domain
const firstPrivate = privateEmailDomains?.find(isValidDomain);
// Filter out hidden private domains from the list of private domains
const firstPrivate = privateEmailDomains?.filter(domain => !hiddenPrivateEmailDomains?.includes(domain)).find(isValidDomain);
if (firstPrivate) {
return firstPrivate;
}

View File

@@ -314,27 +314,6 @@ export class WebApiService {
return this.get<AuthLogModel[]>('Security/authlogs');
}
/**
* Validates the vault response and returns an error message if validation fails
*/
public validateVaultResponse(vaultResponseJson: VaultResponse): string | null {
/**
* Status 0 = OK, vault is ready.
* Status 1 = Merge required, which only the web client supports.
* Status 2 = Outdated, which means the local vault is outdated and the client should fetch the latest vault from the server before saving can continue.
*/
if (vaultResponseJson.status === 1) {
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
return i18n.t('vault.errors.vaultOutdated');
}
if (vaultResponseJson.status === 2) {
return i18n.t('vault.errors.vaultOutdated');
}
return null;
}
/**
* Get the currently configured API URL from native storage.
*/

View File

@@ -1,6 +1,7 @@
type VaultMetadata = {
publicEmailDomains: string[];
privateEmailDomains: string[];
hiddenPrivateEmailDomains: string[];
vaultRevisionNumber: number;
};

View File

@@ -28,18 +28,18 @@ type ApiErrorResponse = {
* Vault type.
*/
type Vault = {
blob: string;
createdAt: string;
credentialsCount: number;
currentRevisionNumber: number;
emailAddressList: string[];
privateEmailDomainList: string[];
publicEmailDomainList: string[];
encryptionPublicKey: string;
updatedAt: string;
username: string;
blob: string;
version: string;
client: string;
currentRevisionNumber: number;
credentialsCount: number;
createdAt: string;
updatedAt: string;
encryptionPublicKey?: string;
emailAddressList?: string[];
privateEmailDomainList?: string[];
hiddenPrivateEmailDomainList?: string[];
publicEmailDomainList?: string[];
};
/**

View File

@@ -23,4 +23,10 @@ public class Config : SharedConfig
/// Gets or sets the list of private email domains that are available.
/// </summary>
public List<string> PrivateEmailDomains { get; set; } = [];
/// <summary>
/// Gets or sets the list of private email domains that should be hidden from UI components.
/// These domains still function as private email domains but are not shown in domain selection dropdowns.
/// </summary>
public List<string> HiddenPrivateEmailDomains { get; set; } = [];
}

View File

@@ -89,11 +89,7 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
Blob = string.Empty,
Version = string.Empty,
CurrentRevisionNumber = 0,
EncryptionPublicKey = string.Empty,
CredentialsCount = 0,
EmailAddressList = [],
PrivateEmailDomainList = [],
PublicEmailDomainList = [],
CreatedAt = DateTime.MinValue,
UpdatedAt = DateTime.MinValue,
},
@@ -121,6 +117,7 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
// Get dynamic list of private email domains from config.
var privateEmailDomainList = config.PrivateEmailDomains;
var hiddenPrivateEmailDomainList = config.HiddenPrivateEmailDomains;
// Hardcoded list of public (SpamOK) email domains that are available to the client.
var publicEmailDomainList = new List<string>(["spamok.com", "solarflarecorp.com", "spamok.nl", "3060.nl",
@@ -137,8 +134,8 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
CurrentRevisionNumber = vault.RevisionNumber,
EncryptionPublicKey = string.Empty,
CredentialsCount = 0,
EmailAddressList = [],
PrivateEmailDomainList = privateEmailDomainList,
HiddenPrivateEmailDomainList = hiddenPrivateEmailDomainList,
PublicEmailDomainList = publicEmailDomainList,
CreatedAt = vault.CreatedAt,
UpdatedAt = vault.UpdatedAt,
@@ -176,11 +173,7 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
Blob = x.VaultBlob,
Version = x.Version,
CurrentRevisionNumber = x.RevisionNumber,
EncryptionPublicKey = string.Empty,
CredentialsCount = 0,
EmailAddressList = [],
PrivateEmailDomainList = [],
PublicEmailDomainList = [],
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt,
}).ToList(),

View File

@@ -48,6 +48,12 @@ var privateEmailDomains = Environment.GetEnvironmentVariable("PRIVATE_EMAIL_DOMA
.Where(d => !string.IsNullOrWhiteSpace(d));
config.PrivateEmailDomains = privateEmailDomains?.ToList() ?? new List<string>();
var hiddenPrivateEmailDomains = Environment.GetEnvironmentVariable("HIDDEN_PRIVATE_EMAIL_DOMAINS")?
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(d => d.Trim())
.Where(d => !string.IsNullOrWhiteSpace(d));
config.HiddenPrivateEmailDomains = hiddenPrivateEmailDomains?.ToList() ?? new List<string>();
var ipLoggingEnabled = Environment.GetEnvironmentVariable("IP_LOGGING_ENABLED") ?? "false";
config.IpLoggingEnabled = bool.Parse(ipLoggingEnabled);

View File

@@ -9,7 +9,8 @@
"JWT_KEY": "12345678901234567890123456789012",
"DATA_PROTECTION_CERT_PASS": "Development",
"PUBLIC_REGISTRATION_ENABLED": "true",
"PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld",
"PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld,disabled.tld",
"HIDDEN_PRIVATE_EMAIL_DOMAINS": "disabled.tld",
"IP_LOGGING_ENABLED": "true"
},
"dotnetRunMessages": true,
@@ -24,7 +25,8 @@
"JWT_KEY": "12345678901234567890123456789012",
"DATA_PROTECTION_CERT_PASS": "Development",
"PUBLIC_REGISTRATION_ENABLED": "true",
"PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld",
"PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld,disabled.tld",
"HIDDEN_PRIVATE_EMAIL_DOMAINS": "disabled.tld",
"IP_LOGGING_ENABLED": "true"
},
"dotnetRunMessages": true,

View File

@@ -23,6 +23,12 @@ public class Config
/// </summary>
public List<string> PrivateEmailDomains { get; set; } = [];
/// <summary>
/// Gets or sets the list of private email domains that should be hidden from UI components.
/// These domains still function as private email domains but are not shown in domain selection dropdowns.
/// </summary>
public List<string> HiddenPrivateEmailDomains { get; set; } = [];
/// <summary>
/// Gets or sets the list of public email domains that are allowed to be used by the client vault users.
/// </summary>

View File

@@ -99,7 +99,9 @@
private string SelectedDomain = string.Empty;
private bool IsPopupVisible = false;
private List<string> PrivateDomains => Config.PrivateEmailDomains;
private List<string> PrivateDomains => Config.PrivateEmailDomains
.Where(d => !Config.HiddenPrivateEmailDomains.Contains(d))
.ToList();
private List<string> PublicDomains => Config.PublicEmailDomains;
private bool ShowPrivateDomains => PrivateDomains.Count > 0 && !(PrivateDomains.Count == 1 && PrivateDomains[0] == "DISABLED.TLD");

View File

@@ -125,7 +125,9 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Settings.General", "AliasVault.Client");
private List<string> PrivateDomains => Config.PrivateEmailDomains;
private List<string> PrivateDomains => Config.PrivateEmailDomains
.Where(d => !Config.HiddenPrivateEmailDomains.Contains(d))
.ToList();
private List<string> PublicDomains => Config.PublicEmailDomains;
private string DefaultEmailDomain { get; set; } = string.Empty;

View File

@@ -196,6 +196,7 @@ else
CredentialsCount = vault.CredentialsCount,
EmailAddressList = vault.EmailAddressList,
PrivateEmailDomainList = [],
HiddenPrivateEmailDomainList = [],
PublicEmailDomainList = [],
CreatedAt = vault.CreatedAt,
UpdatedAt = vault.UpdatedAt,

View File

@@ -490,6 +490,7 @@ public sealed class DbService : IDisposable
CredentialsCount = credentialsCount,
EmailAddressList = emailAddresses,
PrivateEmailDomainList = [],
HiddenPrivateEmailDomainList = [],
PublicEmailDomainList = [],
CreatedAt = currentDateTime,
UpdatedAt = currentDateTime,

View File

@@ -1,11 +1,13 @@
#!/bin/sh
# Set the default values
DEFAULT_PRIVATE_EMAIL_DOMAINS=""
DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS=""
DEFAULT_SUPPORT_EMAIL=""
DEFAULT_PUBLIC_REGISTRATION_ENABLED="true"
# Use the provided environment variables if they exist, otherwise use defaults
PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS}
HIDDEN_PRIVATE_EMAIL_DOMAINS=${HIDDEN_PRIVATE_EMAIL_DOMAINS:-$DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS}
SUPPORT_EMAIL=${SUPPORT_EMAIL:-$DEFAULT_SUPPORT_EMAIL}
PUBLIC_REGISTRATION_ENABLED=${PUBLIC_REGISTRATION_ENABLED:-$DEFAULT_PUBLIC_REGISTRATION_ENABLED}
@@ -37,9 +39,25 @@ else
json_array=$(echo $PRIVATE_EMAIL_DOMAINS | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i<length(a)) printf ","} printf "]"}')
fi
# Handle empty HIDDEN_PRIVATE_EMAIL_DOMAINS by defaulting to empty array
if [ -z "$HIDDEN_PRIVATE_EMAIL_DOMAINS" ]; then
hidden_json_array="[]"
else
# Convert comma-separated list to JSON array
hidden_json_array=$(echo $HIDDEN_PRIVATE_EMAIL_DOMAINS | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i<length(a)) printf ","} printf "]"}')
fi
# Use sed to update the PrivateEmailDomains field in appsettings.json
sed -i.bak "s|\"PrivateEmailDomains\": \[.*\]|\"PrivateEmailDomains\": $json_array|" /usr/share/nginx/html/appsettings.json
# Add HiddenPrivateEmailDomains field if it doesn't exist, or update it if it does
if grep -q "HiddenPrivateEmailDomains" /usr/share/nginx/html/appsettings.json; then
sed -i "s|\"HiddenPrivateEmailDomains\": \[.*\]|\"HiddenPrivateEmailDomains\": $hidden_json_array|" /usr/share/nginx/html/appsettings.json
else
# Insert HiddenPrivateEmailDomains after PrivateEmailDomains
sed -i "s|\"PrivateEmailDomains\": $json_array|\"PrivateEmailDomains\": $json_array,\n \"HiddenPrivateEmailDomains\": $hidden_json_array|" /usr/share/nginx/html/appsettings.json
fi
# Update support email in appsettings.json
if [ ! -z "$SUPPORT_EMAIL" ]; then
sed -i "s|\"SupportEmail\": \".*\"|\"SupportEmail\": \"$SUPPORT_EMAIL\"|g" /usr/share/nginx/html/appsettings.json

View File

@@ -1,6 +1,7 @@
{
"ApiUrl": "http://localhost:5092",
"PrivateEmailDomains": ["example.tld"],
"PrivateEmailDomains": ["example.tld", "example2.tld", "disabled.tld"],
"HiddenPrivateEmailDomains": ["disabled.tld"],
"SupportEmail": "support@example.tld",
"PublicRegistrationEnabled": "true"
}

View File

@@ -4,7 +4,7 @@
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development",
"PRIVATE_EMAIL_DOMAINS": "example.tld",
"PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld,disabled.tld",
"SMTP_TLS_ENABLED": "false"
},
"dotnetRunMessages": true

View File

@@ -4,7 +4,7 @@
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development",
"PRIVATE_EMAIL_DOMAINS": "example.tld",
"PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld,disabled.tld",
"SMTP_TLS_ENABLED": "false"
},
"dotnetRunMessages": true

View File

@@ -12,6 +12,10 @@ namespace AliasVault.Shared.Models.WebApi.Vault;
/// </summary>
public class Vault
{
// ------------------------------------------------------------
// Required properties, always part of the vault get/set model.
// ------------------------------------------------------------
/// <summary>
/// Gets or sets the username that owns the vault.
/// </summary>
@@ -34,32 +38,12 @@ public class Vault
/// </summary>
public required long CurrentRevisionNumber { get; set; }
/// <summary>
/// Gets or sets the public encryption key that server requires to encrypt user data such as received emails.
/// </summary>
public required string EncryptionPublicKey { get; set; }
/// <summary>
/// Gets or sets the number of credentials stored in the vault. This anonymous data is used in case a vault back-up
/// needs to be restored to get a better idea of the vault size.
/// </summary>
public required int CredentialsCount { get; set; }
/// <summary>
/// Gets or sets the list of email addresses that are used in the vault and should be registered on the server.
/// </summary>
public required List<string> EmailAddressList { get; set; }
/// <summary>
/// Gets or sets the list of private email domains that are available.
/// </summary>
public required List<string> PrivateEmailDomainList { get; set; }
/// <summary>
/// Gets or sets the list of public email domains that are available.
/// </summary>
public required List<string> PublicEmailDomainList { get; set; }
/// <summary>
/// Gets or sets the date and time of creation.
/// </summary>
@@ -69,4 +53,34 @@ public class Vault
/// Gets or sets the date and time of last update.
/// </summary>
public required DateTime UpdatedAt { get; set; }
// ------------------------------------------------------------
// Optional properties, only part of the vault get/set model if available and applicable.
// ------------------------------------------------------------
/// <summary>
/// Gets or sets the public encryption key that server requires to encrypt user data such as received emails.
/// </summary>
public string EncryptionPublicKey { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the list of email addresses that are used in the vault and should be registered on the server.
/// </summary>
public List<string> EmailAddressList { get; set; } = [];
/// <summary>
/// Gets or sets the list of private email domains that are available.
/// </summary>
public List<string> PrivateEmailDomainList { get; set; } = [];
/// <summary>
/// Gets or sets the list of private email domains that should be hidden from UI components.
/// These domains still function as private email domains but are not shown in domain selection dropdowns.
/// </summary>
public List<string> HiddenPrivateEmailDomainList { get; set; } = [];
/// <summary>
/// Gets or sets the list of public email domains that are available.
/// </summary>
public List<string> PublicEmailDomainList { get; set; } = [];
}

View File

@@ -94,8 +94,10 @@ public class ClientPlaywrightTest : PlaywrightTest
ApiBaseUrl = "http://localhost:" + apiPort + "/";
// Set environment variables for the API.
string[] privateEmailDomains = ["example.tld", "example2.tld"];
string[] privateEmailDomains = ["example.tld", "example2.tld","disabled.tld"];
Environment.SetEnvironmentVariable("PRIVATE_EMAIL_DOMAINS", string.Join(",", privateEmailDomains));
string[] hiddenPrivateEmailDomains = ["disabled.tld"];
Environment.SetEnvironmentVariable("HIDDEN_PRIVATE_EMAIL_DOMAINS", string.Join(",", hiddenPrivateEmailDomains));
// Start WebAPI in-memory.
_apiFactory.Port = apiPort;

View File

@@ -163,6 +163,7 @@ ENV ALIASVAULT_VERBOSITY=0 \
IP_LOGGING_ENABLED=true \
SUPPORT_EMAIL="" \
PRIVATE_EMAIL_DOMAINS="" \
HIDDEN_PRIVATE_EMAIL_DOMAINS="" \
HOSTNAME=localhost \
POSTGRES_HOST=localhost \
POSTGRES_PORT=5432 \

View File

@@ -30,6 +30,7 @@ done
export ASPNETCORE_URLS="http://0.0.0.0:3001"
export ASPNETCORE_PATHBASE="/api"
export PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-}"
export HIDDEN_PRIVATE_EMAIL_DOMAINS="${HIDDEN_PRIVATE_EMAIL_DOMAINS:-}"
export PUBLIC_REGISTRATION_ENABLED="${PUBLIC_REGISTRATION_ENABLED:-true}"
export IP_LOGGING_ENABLED="${IP_LOGGING_ENABLED:-true}"

View File

@@ -27,8 +27,10 @@ done
# Client service entrypoint
DEFAULT_PRIVATE_EMAIL_DOMAINS=""
DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS=""
DEFAULT_SUPPORT_EMAIL=""
PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS}
HIDDEN_PRIVATE_EMAIL_DOMAINS=${HIDDEN_PRIVATE_EMAIL_DOMAINS:-$DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS}
SUPPORT_EMAIL=${SUPPORT_EMAIL:-$DEFAULT_SUPPORT_EMAIL}
PUBLIC_REGISTRATION_ENABLED=${PUBLIC_REGISTRATION_ENABLED:-true}
@@ -58,10 +60,19 @@ else
json_array=$(echo "$PRIVATE_EMAIL_DOMAINS" | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i<length(a)) printf ","} printf "]"}')
fi
# Convert comma-separated HIDDEN_PRIVATE_EMAIL_DOMAINS to JSON array
if [ -z "$HIDDEN_PRIVATE_EMAIL_DOMAINS" ]; then
hidden_json_array="[]"
else
# Convert comma-separated list to JSON array
hidden_json_array=$(echo "$HIDDEN_PRIVATE_EMAIL_DOMAINS" | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i<length(a)) printf ","} printf "]"}')
fi
# Create JSON with environment variables
cat > /app/client/wwwroot/appsettings.json << EOF
{
"PrivateEmailDomains": $json_array,
"HiddenPrivateEmailDomains": $hidden_json_array,
"SupportEmail": "$SUPPORT_EMAIL",
"PublicRegistrationEnabled": "$PUBLIC_REGISTRATION_ENABLED"
}

View File

@@ -28,6 +28,7 @@ for i in {1..30}; do
done
export PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-}"
export HIDDEN_PRIVATE_EMAIL_DOMAINS="${HIDDEN_PRIVATE_EMAIL_DOMAINS:-}"
export SMTP_TLS_ENABLED="${SMTP_TLS_ENABLED:-false}"
# Set .NET logging level based on verbosity

View File

@@ -29,6 +29,7 @@ services:
FORCE_HTTPS_REDIRECT: "false"
SUPPORT_EMAIL: ""
PRIVATE_EMAIL_DOMAINS: ""
HIDDEN_PRIVATE_EMAIL_DOMAINS: ""
volumes:
avdata:

View File

@@ -27,3 +27,4 @@ services:
FORCE_HTTPS_REDIRECT: "false"
SUPPORT_EMAIL: ""
PRIVATE_EMAIL_DOMAINS: ""
HIDDEN_PRIVATE_EMAIL_DOMAINS: ""

View File

@@ -3183,6 +3183,12 @@ check_and_populate_env() {
printf " Set PRIVATE_EMAIL_DOMAINS\n"
fi
# HIDDEN_PRIVATE_EMAIL_DOMAINS
if ! grep -q "^HIDDEN_PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE"; then
update_env_var "HIDDEN_PRIVATE_EMAIL_DOMAINS" ""
printf " Set HIDDEN_PRIVATE_EMAIL_DOMAINS\n"
fi
# HTTP_PORT
if ! grep -q "^HTTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "HTTP_PORT" "80"

View File

@@ -1,5 +1,6 @@
export type VaultMetadata = {
publicEmailDomains: string[],
privateEmailDomains: string[],
hiddenPrivateEmailDomains: string[],
vaultRevisionNumber: number
};

View File

@@ -2,16 +2,18 @@
* Vault type.
*/
export type Vault = {
blob: string;
createdAt: string;
credentialsCount: number;
currentRevisionNumber: number;
emailAddressList: string[];
privateEmailDomainList: string[];
publicEmailDomainList: string[];
encryptionPublicKey: string;
updatedAt: string;
// Required properties, always part of the vault get/set model.
username: string;
blob: string;
version: string;
client: string;
currentRevisionNumber: number;
credentialsCount: number;
createdAt: string;
updatedAt: string;
// Optional properties, only part of the vault get/set model if available and applicable.
encryptionPublicKey?: string;
emailAddressList?: string[];
privateEmailDomainList?: string[];
hiddenPrivateEmailDomainList?: string[];
publicEmailDomainList?: string[];
}