mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
feat(map): amap as map api provider and display template
This commit is contained in:
@@ -36,6 +36,12 @@ export const ADMIN_SETTINGS_KEYS = [
|
||||
'notify_trip_reminder',
|
||||
'password_login', 'password_registration', 'oidc_login', 'oidc_registration',
|
||||
'passkey_login', 'webauthn_rp_id', 'webauthn_origins',
|
||||
// Which places provider answers search/autocomplete/details: 'auto' (the
|
||||
// default and what every install had before Amap existed), 'google', 'amap' or
|
||||
// 'openstreetmap'. Validated in updateAppSettings — an unknown value is
|
||||
// refused rather than stored, because the reader degrades a bad row to 'auto'
|
||||
// and the admin would be left looking at a setting that does nothing.
|
||||
'places_provider',
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -53,6 +59,7 @@ export function stripUserForClient(user: User): Record<string, unknown> {
|
||||
maps_api_key: _m,
|
||||
openweather_api_key: _o,
|
||||
unsplash_api_key: _u,
|
||||
amap_api_key: _a,
|
||||
mfa_secret: _mf,
|
||||
mfa_backup_codes: _mbc,
|
||||
...rest
|
||||
|
||||
@@ -15,6 +15,9 @@ import { validatePassword } from '../common/passwordPolicy';
|
||||
import { encryptMfaSecret, decryptMfaSecret } from '../common/crypto/mfaCrypto';
|
||||
import { decrypt_api_key, maybe_encrypt_api_key, encrypt_api_key } from '../common/crypto/apiKeyCrypto';
|
||||
import { resolveApiKey } from '../settings/instance-api-keys';
|
||||
// Type-and-guard only: the app-config read reports the provider choice, it does
|
||||
// not construct one, so this does not pull the maps domain into auth.
|
||||
import { isPlacesProviderChoice } from '../maps/providers/places-provider';
|
||||
import { EphemeralTokenService } from './ephemeral-token.service';
|
||||
// Import from sessionManager directly, NOT the ../../mcp barrel: the barrel pulls
|
||||
// the whole tools fan-out (and via the domain bridges, the Nest services) into
|
||||
@@ -263,6 +266,12 @@ export class AuthService {
|
||||
// question is only about the instance, which is the first two steps of the
|
||||
// chain; id 0 matches no row.
|
||||
const hasGoogleKey = !!resolveApiKey(this.db, 'maps_api_key', authenticatedUser?.id ?? 0, readEnv().maps.placesApiKey).key;
|
||||
// The same question for Amap, asked the same way. The client needs both to
|
||||
// tell "search is unavailable" from "search runs on OpenStreetMap", and to
|
||||
// know whether the provider the admin selected actually has a credential.
|
||||
const hasAmapKey = !!resolveApiKey(this.db, 'amap_api_key', authenticatedUser?.id ?? 0, readEnv().maps.amapApiKey).key;
|
||||
const placesProviderRow = this.db.get<{ value: string }>("SELECT value FROM app_settings WHERE key = 'places_provider'")?.value;
|
||||
const placesProvider = isPlacesProviderChoice(placesProviderRow) ? placesProviderRow : 'auto';
|
||||
const oidcDisplayName = readEnv().oidc.displayName ||
|
||||
this.db.get<{ value: string }>("SELECT value FROM app_settings WHERE key = 'oidc_display_name'")?.value || null;
|
||||
const oidcConfigured = !!(
|
||||
@@ -313,6 +322,8 @@ export class AuthService {
|
||||
version,
|
||||
is_prerelease: version.includes('-pre.'),
|
||||
has_maps_key: hasGoogleKey,
|
||||
has_amap_key: hasAmapKey,
|
||||
places_provider: placesProvider,
|
||||
oidc_configured: oidcConfigured,
|
||||
oidc_display_name: oidcConfigured ? (oidcDisplayName || 'SSO') : undefined,
|
||||
require_mfa: requireMfaRow?.value === 'true',
|
||||
@@ -695,6 +706,10 @@ export class AuthService {
|
||||
if (key === 'require_mfa') {
|
||||
val = body[key] === true || val === 'true' ? 'true' : 'false';
|
||||
}
|
||||
// An unknown provider name is dropped, not stored: the maps service
|
||||
// degrades an unrecognised row to 'auto', so writing one would show the
|
||||
// admin a saved setting that quietly does nothing.
|
||||
if (key === 'places_provider' && !isPlacesProviderChoice(val)) continue;
|
||||
if (key === 'smtp_pass' && val === '••••••••') continue;
|
||||
if (key === 'smtp_pass') val = encrypt_api_key(val);
|
||||
if (key === 'admin_webhook_url' && val === '••••••••') continue;
|
||||
|
||||
@@ -53,8 +53,8 @@ export class UserProfileService {
|
||||
|
||||
/** The three key columns plus the role that decides where a save lands. */
|
||||
private currentKeys(userId: number) {
|
||||
return this.db.get<Pick<User, 'role' | 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key'>>(
|
||||
'SELECT role, maps_api_key, openweather_api_key, unsplash_api_key FROM users WHERE id = ?',
|
||||
return this.db.get<Pick<User, 'role' | 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key' | 'amap_api_key'>>(
|
||||
'SELECT role, maps_api_key, openweather_api_key, unsplash_api_key, amap_api_key FROM users WHERE id = ?',
|
||||
userId
|
||||
);
|
||||
}
|
||||
@@ -67,8 +67,8 @@ export class UserProfileService {
|
||||
* their own column only speaks when no instance value has been set yet.
|
||||
*/
|
||||
private storedKeyPlaintext(
|
||||
name: 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key',
|
||||
current: Pick<User, 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key'> | undefined,
|
||||
name: 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key' | 'amap_api_key',
|
||||
current: Pick<User, 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key' | 'amap_api_key'> | undefined,
|
||||
isAdmin: boolean,
|
||||
): string {
|
||||
if (isAdmin && (INSTANCE_API_KEY_NAMES as readonly string[]).includes(name)) {
|
||||
@@ -91,12 +91,12 @@ export class UserProfileService {
|
||||
*/
|
||||
private changedKeyNames(
|
||||
body: Record<string, unknown>,
|
||||
current: Pick<User, 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key'> | undefined,
|
||||
current: Pick<User, 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key' | 'amap_api_key'> | undefined,
|
||||
isAdmin: boolean,
|
||||
skipped: string[] = [],
|
||||
): string[] {
|
||||
const norm = (v: unknown) => String(v ?? '').trim();
|
||||
return (['maps_api_key', 'openweather_api_key', 'unsplash_api_key'] as const).filter(
|
||||
return (['maps_api_key', 'openweather_api_key', 'unsplash_api_key', 'amap_api_key'] as const).filter(
|
||||
(name) => body[name] !== undefined && !skipped.includes(name) && norm(body[name]) !== norm(this.storedKeyPlaintext(name, current, isAdmin))
|
||||
);
|
||||
}
|
||||
@@ -135,7 +135,7 @@ export class UserProfileService {
|
||||
}
|
||||
|
||||
updateApiKeys(userId: number, rawBody: unknown) {
|
||||
const body = rawBody as { maps_api_key?: string; openweather_api_key?: string; unsplash_api_key?: string };
|
||||
const body = rawBody as { maps_api_key?: string; openweather_api_key?: string; unsplash_api_key?: string; amap_api_key?: string };
|
||||
const { blocked } = splitManagedKeys(body, this.managed);
|
||||
for (const key of blocked) delete body[key as keyof typeof body];
|
||||
const current = this.currentKeys(userId);
|
||||
@@ -146,17 +146,18 @@ export class UserProfileService {
|
||||
// `?? null` instead of the former non-null assertions: a user row deleted
|
||||
// mid-request must degrade to a 0-row UPDATE, not a TypeError/500.
|
||||
this.db.run(
|
||||
'UPDATE users SET maps_api_key = ?, openweather_api_key = ?, unsplash_api_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
'UPDATE users SET maps_api_key = ?, openweather_api_key = ?, unsplash_api_key = ?, amap_api_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
body.maps_api_key !== undefined ? maybe_encrypt_api_key(body.maps_api_key) : current?.maps_api_key ?? null,
|
||||
body.openweather_api_key !== undefined ? maybe_encrypt_api_key(body.openweather_api_key) : current?.openweather_api_key ?? null,
|
||||
body.unsplash_api_key !== undefined ? maybe_encrypt_api_key(body.unsplash_api_key) : current?.unsplash_api_key ?? null,
|
||||
body.amap_api_key !== undefined ? maybe_encrypt_api_key(body.amap_api_key) : current?.amap_api_key ?? null,
|
||||
userId
|
||||
);
|
||||
this.mirrorInstanceKeys(body, isAdmin);
|
||||
});
|
||||
|
||||
const updated = this.db.get<Pick<User, 'id' | 'username' | 'email' | 'role' | 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key' | 'avatar' | 'mfa_enabled'>>(
|
||||
'SELECT id, username, email, role, maps_api_key, openweather_api_key, unsplash_api_key, avatar, mfa_enabled FROM users WHERE id = ?',
|
||||
const updated = this.db.get<Pick<User, 'id' | 'username' | 'email' | 'role' | 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key' | 'amap_api_key' | 'avatar' | 'mfa_enabled'>>(
|
||||
'SELECT id, username, email, role, maps_api_key, openweather_api_key, unsplash_api_key, amap_api_key, avatar, mfa_enabled FROM users WHERE id = ?',
|
||||
userId
|
||||
);
|
||||
|
||||
@@ -164,7 +165,7 @@ export class UserProfileService {
|
||||
return {
|
||||
success: true,
|
||||
...(blocked.length ? { managed_keys: blocked } : {}),
|
||||
user: { ...u, maps_api_key: mask_stored_api_key(u?.maps_api_key), openweather_api_key: mask_stored_api_key(u?.openweather_api_key), unsplash_api_key: mask_stored_api_key(u?.unsplash_api_key), avatar_url: avatarUrl(updated || {}) },
|
||||
user: { ...u, maps_api_key: mask_stored_api_key(u?.maps_api_key), openweather_api_key: mask_stored_api_key(u?.openweather_api_key), unsplash_api_key: mask_stored_api_key(u?.unsplash_api_key), amap_api_key: mask_stored_api_key(u?.amap_api_key), avatar_url: avatarUrl(updated || {}) },
|
||||
changedKeys,
|
||||
};
|
||||
}
|
||||
@@ -173,8 +174,8 @@ export class UserProfileService {
|
||||
userId: number,
|
||||
rawBody: unknown
|
||||
): { error?: string; status?: number; success?: boolean; user?: Record<string, unknown>; changedKeys?: string[] } {
|
||||
const body = rawBody as { maps_api_key?: string; openweather_api_key?: string; unsplash_api_key?: string; username?: string; email?: string };
|
||||
const { maps_api_key, openweather_api_key, unsplash_api_key, username, email } = body;
|
||||
const body = rawBody as { maps_api_key?: string; openweather_api_key?: string; unsplash_api_key?: string; amap_api_key?: string; username?: string; email?: string };
|
||||
const { maps_api_key, openweather_api_key, unsplash_api_key, amap_api_key, username, email } = body;
|
||||
|
||||
if (username !== undefined) {
|
||||
const trimmed = username.trim();
|
||||
@@ -208,6 +209,7 @@ export class UserProfileService {
|
||||
if (maps_api_key !== undefined && !keyLocked) { updates.push('maps_api_key = ?'); params.push(maybe_encrypt_api_key(maps_api_key)); }
|
||||
if (openweather_api_key !== undefined && !keyLocked) { updates.push('openweather_api_key = ?'); params.push(maybe_encrypt_api_key(openweather_api_key)); }
|
||||
if (unsplash_api_key !== undefined && !keyLocked) { updates.push('unsplash_api_key = ?'); params.push(maybe_encrypt_api_key(unsplash_api_key)); }
|
||||
if (amap_api_key !== undefined && !keyLocked) { updates.push('amap_api_key = ?'); params.push(maybe_encrypt_api_key(amap_api_key)); }
|
||||
if (username !== undefined) { updates.push('username = ?'); params.push(username.trim()); }
|
||||
if (email !== undefined) { updates.push('email = ?'); params.push(email.trim()); }
|
||||
|
||||
@@ -226,8 +228,8 @@ export class UserProfileService {
|
||||
});
|
||||
}
|
||||
|
||||
const updated = this.db.get<Pick<User, 'id' | 'username' | 'email' | 'role' | 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key' | 'avatar' | 'mfa_enabled'>>(
|
||||
'SELECT id, username, email, role, maps_api_key, openweather_api_key, unsplash_api_key, avatar, mfa_enabled FROM users WHERE id = ?',
|
||||
const updated = this.db.get<Pick<User, 'id' | 'username' | 'email' | 'role' | 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key' | 'amap_api_key' | 'avatar' | 'mfa_enabled'>>(
|
||||
'SELECT id, username, email, role, maps_api_key, openweather_api_key, unsplash_api_key, amap_api_key, avatar, mfa_enabled FROM users WHERE id = ?',
|
||||
userId
|
||||
);
|
||||
|
||||
@@ -235,14 +237,14 @@ export class UserProfileService {
|
||||
return {
|
||||
success: true,
|
||||
...(blocked.length ? { managed_keys: blocked } : {}),
|
||||
user: { ...u, maps_api_key: mask_stored_api_key(u?.maps_api_key), openweather_api_key: mask_stored_api_key(u?.openweather_api_key), unsplash_api_key: mask_stored_api_key(u?.unsplash_api_key), avatar_url: avatarUrl(updated || {}) },
|
||||
user: { ...u, maps_api_key: mask_stored_api_key(u?.maps_api_key), openweather_api_key: mask_stored_api_key(u?.openweather_api_key), unsplash_api_key: mask_stored_api_key(u?.unsplash_api_key), amap_api_key: mask_stored_api_key(u?.amap_api_key), avatar_url: avatarUrl(updated || {}) },
|
||||
changedKeys,
|
||||
};
|
||||
}
|
||||
|
||||
getSettings(userId: number): { error?: string; status?: number; settings?: Record<string, unknown> } {
|
||||
const user = this.db.get<Pick<User, 'role' | 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key'>>(
|
||||
'SELECT role, maps_api_key, openweather_api_key, unsplash_api_key FROM users WHERE id = ?',
|
||||
const user = this.db.get<Pick<User, 'role' | 'maps_api_key' | 'openweather_api_key' | 'unsplash_api_key' | 'amap_api_key'>>(
|
||||
'SELECT role, maps_api_key, openweather_api_key, unsplash_api_key, amap_api_key FROM users WHERE id = ?',
|
||||
userId
|
||||
);
|
||||
if (user?.role !== 'admin') return { error: 'Admin access required', status: 403 };
|
||||
@@ -258,6 +260,7 @@ export class UserProfileService {
|
||||
maps_api_key: null,
|
||||
openweather_api_key: null,
|
||||
unsplash_api_key: null,
|
||||
amap_api_key: null,
|
||||
managed_keys: [...MANAGED_LOCKED_PROFILE_KEYS],
|
||||
},
|
||||
};
|
||||
@@ -272,6 +275,7 @@ export class UserProfileService {
|
||||
maps_api_key: readInstanceApiKey(this.db, 'maps_api_key') ?? decrypt_api_key(user.maps_api_key),
|
||||
openweather_api_key: decrypt_api_key(user.openweather_api_key),
|
||||
unsplash_api_key: readInstanceApiKey(this.db, 'unsplash_api_key') ?? decrypt_api_key(user.unsplash_api_key),
|
||||
amap_api_key: readInstanceApiKey(this.db, 'amap_api_key') ?? decrypt_api_key(user.amap_api_key),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user