From d4e39fa8e8af48cbf499578ffeed7f28efba3227 Mon Sep 17 00:00:00 2001 From: William Swanson Date: Fri, 22 Jul 2022 10:26:25 -0700 Subject: [PATCH 1/3] Remove the models index file --- src/NotificationManager.ts | 2 +- src/models/ApiKey.ts | 2 +- src/models/CurrencyThreshold.ts | 2 +- src/models/Defaults.ts | 2 +- src/models/Device.ts | 2 +- src/models/User.ts | 2 +- src/models/index.ts | 5 ----- src/price-script/checkPriceChanges.ts | 4 +++- src/price-script/fetchThresholdPrices.ts | 2 +- src/server/app.ts | 2 +- src/server/controllers/DeviceController.ts | 2 +- src/server/controllers/NotificationController.ts | 2 +- src/server/controllers/UserController.ts | 2 +- 13 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 src/models/index.ts diff --git a/src/NotificationManager.ts b/src/NotificationManager.ts index 6f75034..3d2c8bf 100644 --- a/src/NotificationManager.ts +++ b/src/NotificationManager.ts @@ -1,7 +1,7 @@ import io from '@pm2/io' import admin from 'firebase-admin' -import { ApiKey } from './models' +import { ApiKey } from './models/ApiKey' import BatchResponse = admin.messaging.BatchResponse diff --git a/src/models/ApiKey.ts b/src/models/ApiKey.ts index f94d32d..4122faf 100644 --- a/src/models/ApiKey.ts +++ b/src/models/ApiKey.ts @@ -2,7 +2,7 @@ import { asBoolean, asMap, asObject, asOptional, asString } from 'cleaners' import Nano from 'nano' import { serverConfig } from '../serverConfig' -import { Base } from '.' +import { Base } from './base' const nanoDb = Nano(serverConfig.couchUri) const dbDevices = nanoDb.db.use('db_api_keys') diff --git a/src/models/CurrencyThreshold.ts b/src/models/CurrencyThreshold.ts index be58f47..9b9cf10 100644 --- a/src/models/CurrencyThreshold.ts +++ b/src/models/CurrencyThreshold.ts @@ -2,7 +2,7 @@ import { asBoolean, asMap, asNumber, asObject, asOptional } from 'cleaners' import Nano from 'nano' import { serverConfig } from '../serverConfig' -import { Base } from '.' +import { Base } from './base' import { Defaults } from './Defaults' const nanoDb = Nano(serverConfig.couchUri) diff --git a/src/models/Defaults.ts b/src/models/Defaults.ts index 535e366..de32b3d 100644 --- a/src/models/Defaults.ts +++ b/src/models/Defaults.ts @@ -2,7 +2,7 @@ import { asMap } from 'cleaners' import Nano from 'nano' import { serverConfig } from '../serverConfig' -import { Base } from '.' +import { Base } from './base' const nanoDb = Nano(serverConfig.couchUri) const dbCurrencyThreshold = nanoDb.db.use('defaults') diff --git a/src/models/Device.ts b/src/models/Device.ts index 65c3e96..eb9a331 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -2,7 +2,7 @@ import { asNumber, asObject, asOptional, asString } from 'cleaners' import Nano from 'nano' import { serverConfig } from '../serverConfig' -import { Base } from '.' +import { Base } from './base' const nanoDb = Nano(serverConfig.couchUri) const dbDevices = nanoDb.db.use>('db_devices') diff --git a/src/models/User.ts b/src/models/User.ts index bb09646..563f390 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -2,7 +2,7 @@ import { asBoolean, asMap, asObject, asOptional } from 'cleaners' import Nano from 'nano' import { serverConfig } from '../serverConfig' -import { Base } from '.' +import { Base } from './base' import { Device } from './Device' const nanoDb = Nano(serverConfig.couchUri) diff --git a/src/models/index.ts b/src/models/index.ts deleted file mode 100644 index 023453d..0000000 --- a/src/models/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './base' -export * from './Device' -export * from './User' -export * from './CurrencyThreshold' -export * from './ApiKey' diff --git a/src/price-script/checkPriceChanges.ts b/src/price-script/checkPriceChanges.ts index b7ccd04..e2a478b 100644 --- a/src/price-script/checkPriceChanges.ts +++ b/src/price-script/checkPriceChanges.ts @@ -1,7 +1,9 @@ import io from '@pm2/io' import { MetricType } from '@pm2/io/build/main/services/metrics' -import { CurrencyThreshold, Device, User } from '../models' +import { CurrencyThreshold } from '../models/CurrencyThreshold' +import { Device } from '../models/Device' +import { User } from '../models/User' import { NotificationManager } from '../NotificationManager' import { fetchThresholdPrice } from './fetchThresholdPrices' diff --git a/src/price-script/fetchThresholdPrices.ts b/src/price-script/fetchThresholdPrices.ts index b71ea62..045ba64 100644 --- a/src/price-script/fetchThresholdPrices.ts +++ b/src/price-script/fetchThresholdPrices.ts @@ -1,6 +1,6 @@ import io from '@pm2/io' -import { CurrencyThreshold } from '../models' +import { CurrencyThreshold } from '../models/CurrencyThreshold' import { NotificationPriceChange } from './checkPriceChanges' import { getPrice } from './prices' diff --git a/src/server/app.ts b/src/server/app.ts index 6ec79a4..63f3bdf 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -3,7 +3,7 @@ import bodyParser from 'body-parser' import cors from 'cors' import express from 'express' -import { ApiKey } from '../models' +import { ApiKey } from '../models/ApiKey' import { DeviceController } from './controllers/DeviceController' import { NotificationController } from './controllers/NotificationController' import { UserController } from './controllers/UserController' diff --git a/src/server/controllers/DeviceController.ts b/src/server/controllers/DeviceController.ts index 9a3df61..89b82a8 100644 --- a/src/server/controllers/DeviceController.ts +++ b/src/server/controllers/DeviceController.ts @@ -2,7 +2,7 @@ import { asObject, asString } from 'cleaners' import express from 'express' -import { Device } from '../../models' +import { Device } from '../../models/Device' export const DeviceController = express.Router() diff --git a/src/server/controllers/NotificationController.ts b/src/server/controllers/NotificationController.ts index 2e49bbd..214434b 100644 --- a/src/server/controllers/NotificationController.ts +++ b/src/server/controllers/NotificationController.ts @@ -2,7 +2,7 @@ import { asMap, asObject, asOptional, asString, asUnknown } from 'cleaners' import express from 'express' -import { User } from '../../models' +import { User } from '../../models/User' import { NotificationManager } from '../../NotificationManager' export const NotificationController = express.Router() diff --git a/src/server/controllers/UserController.ts b/src/server/controllers/UserController.ts index 9d63ae9..666a65c 100644 --- a/src/server/controllers/UserController.ts +++ b/src/server/controllers/UserController.ts @@ -2,7 +2,7 @@ import { asArray, asBoolean, asObject, asString } from 'cleaners' import express from 'express' -import { User } from '../../models' +import { User } from '../../models/User' export const UserController = express.Router() From e44a09c0496384eed198b58c8124432bc4345110 Mon Sep 17 00:00:00 2001 From: William Swanson Date: Fri, 22 Jul 2022 10:23:15 -0700 Subject: [PATCH 2/3] Update the API key database to modern tooling --- src/NotificationManager.ts | 12 ++++---- src/db/couchApiKeys.ts | 60 ++++++++++++++++++++++++++++++++++++++ src/models/ApiKey.ts | 23 --------------- src/price-script/index.ts | 11 +++++-- src/server/app.ts | 9 ++++-- src/types/pushTypes.ts | 30 +++++++++++++++++++ 6 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 src/db/couchApiKeys.ts delete mode 100644 src/models/ApiKey.ts create mode 100644 src/types/pushTypes.ts diff --git a/src/NotificationManager.ts b/src/NotificationManager.ts index 3d2c8bf..e96b94a 100644 --- a/src/NotificationManager.ts +++ b/src/NotificationManager.ts @@ -1,7 +1,7 @@ import io from '@pm2/io' import admin from 'firebase-admin' -import { ApiKey } from './models/ApiKey' +import { ApiKey } from './types/pushTypes' import BatchResponse = admin.messaging.BatchResponse @@ -17,11 +17,7 @@ const failureCounter = io.counter({ export class NotificationManager { private constructor(private readonly app: admin.app.App) {} - public static async init( - apiKey: ApiKey | string - ): Promise { - if (typeof apiKey === 'string') apiKey = await ApiKey.fetch(apiKey) - + public static async init(apiKey: ApiKey): Promise { const name = `app:${apiKey.appId}` let app: admin.app.App try { @@ -29,7 +25,9 @@ export class NotificationManager { } catch (err) { app = admin.initializeApp( { - credential: admin.credential.cert(apiKey.adminsdk) + // TODO: We have never passed the correct data type here, + // so either update our database or write a translation layer: + credential: admin.credential.cert(apiKey.adminsdk as any) }, name ) diff --git a/src/db/couchApiKeys.ts b/src/db/couchApiKeys.ts new file mode 100644 index 0000000..6a30812 --- /dev/null +++ b/src/db/couchApiKeys.ts @@ -0,0 +1,60 @@ +import { asBoolean, asObject, asOptional, asString, Cleaner } from 'cleaners' +import { + asCouchDoc, + asMaybeNotFoundError, + DatabaseSetup +} from 'edge-server-tools' +import { ServerScope } from 'nano' + +import { ApiKey, FirebaseAdminKey } from '../types/pushTypes' + +export const asFirebaseAdminKey: Cleaner = asObject({ + type: asOptional(asString), + project_id: asOptional(asString), + + auth_provider_x509_cert_url: asOptional(asString), + auth_uri: asOptional(asString), + client_email: asOptional(asString), + client_id: asOptional(asString), + client_x509_cert_url: asOptional(asString), + private_key_id: asOptional(asString), + private_key: asOptional(asString), + token_uri: asOptional(asString) +}).withRest + +/** + * An API key, as stored in Couch. + */ +export const asCouchApiKey = asCouchDoc>( + asObject({ + appId: asString, + admin: asBoolean, + adminsdk: asOptional(asFirebaseAdminKey) + }) +) +type CouchApiKey = ReturnType + +/** + * The document key is the api key. + */ +export const couchApiKeysSetup: DatabaseSetup = { + name: 'db_api_keys' +} + +export async function getApiKeyByKey( + connection: ServerScope, + apiKey: string +): Promise { + const db = connection.db.use(couchApiKeysSetup.name) + const raw = await db.get(apiKey).catch(error => { + if (asMaybeNotFoundError(error) != null) return + throw error + }) + + if (raw == null) return + return unpackApiKey(asCouchApiKey(raw)) +} + +function unpackApiKey(doc: CouchApiKey): ApiKey { + return { ...doc.doc, apiKey: doc.id } +} diff --git a/src/models/ApiKey.ts b/src/models/ApiKey.ts deleted file mode 100644 index 4122faf..0000000 --- a/src/models/ApiKey.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { asBoolean, asMap, asObject, asOptional, asString } from 'cleaners' -import Nano from 'nano' - -import { serverConfig } from '../serverConfig' -import { Base } from './base' - -const nanoDb = Nano(serverConfig.couchUri) -const dbDevices = nanoDb.db.use('db_api_keys') - -const asApiKey = asObject({ - appId: asString, - admin: asBoolean, - adminsdk: asOptional(asMap(asString)) -}) - -export class ApiKey extends Base implements ReturnType { - public static table = dbDevices - public static asType = asApiKey - - public appId!: string - public admin!: boolean - public adminsdk!: { [key: string]: string } -} diff --git a/src/price-script/index.ts b/src/price-script/index.ts index fd78bdb..ac4e2c7 100644 --- a/src/price-script/index.ts +++ b/src/price-script/index.ts @@ -2,6 +2,7 @@ import io from '@pm2/io' import { makePeriodicTask } from 'edge-server-tools' import nano from 'nano' +import { getApiKeyByKey } from '../db/couchApiKeys' import { syncedSettings } from '../db/couchSettings' import { setupDatabases } from '../db/couchSetup' import { NotificationManager } from '../NotificationManager' @@ -22,9 +23,13 @@ async function main(): Promise { // Read the API keys from settings: const managers = await Promise.all( - syncedSettings.doc.apiKeys.map( - async partner => await NotificationManager.init(partner.apiKey) - ) + syncedSettings.doc.apiKeys.map(async partner => { + const apiKey = await getApiKeyByKey(nano(couchUri), partner.apiKey) + if (apiKey == null) { + throw new Error(`Cannot find API key ${partner.apiKey}`) + } + return await NotificationManager.init(apiKey) + }) ) // Check the prices every few minutes: diff --git a/src/server/app.ts b/src/server/app.ts index 63f3bdf..e927311 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -2,8 +2,11 @@ import io from '@pm2/io' import bodyParser from 'body-parser' import cors from 'cors' import express from 'express' +import nano from 'nano' -import { ApiKey } from '../models/ApiKey' +import { getApiKeyByKey } from '../db/couchApiKeys' +import { serverConfig } from '../serverConfig' +import { ApiKey } from '../types/pushTypes' import { DeviceController } from './controllers/DeviceController' import { NotificationController } from './controllers/NotificationController' import { UserController } from './controllers/UserController' @@ -46,8 +49,8 @@ router.use(async (req, res, next) => { const apiKey = req.header('X-Api-Key') if (!apiKey) return res.sendStatus(401) - const key = await ApiKey.fetch(apiKey) - if (!key) return res.sendStatus(401) + const key = await getApiKeyByKey(nano(serverConfig.couchUri), apiKey) + if (key == null) return res.sendStatus(401) req.apiKey = key diff --git a/src/types/pushTypes.ts b/src/types/pushTypes.ts new file mode 100644 index 0000000..979fcb8 --- /dev/null +++ b/src/types/pushTypes.ts @@ -0,0 +1,30 @@ +/** + * Firebase admin credentials. + * + * We should probably match the `ServiceAccount` type from the Firebase SDK, + * but this is the existing situation in the database. + */ +export interface FirebaseAdminKey { + type?: string + project_id?: string + + auth_provider_x509_cert_url?: string + auth_uri?: string + client_email?: string + client_id?: string + client_x509_cert_url?: string + private_key_id?: string + private_key?: string + token_uri?: string +} + +/** + * An API key, along with some extra info. + */ +export interface ApiKey { + apiKey: string + appId: string + + admin: boolean + adminsdk?: FirebaseAdminKey +} From 084d12dfae3efbcb08dbd35fb3df51dc0f44cb65 Mon Sep 17 00:00:00 2001 From: William Swanson Date: Fri, 22 Jul 2022 13:06:52 -0700 Subject: [PATCH 3/3] wip --- src/db/couchSettings.ts | 12 ++++++-- src/db/couchUser.ts | 42 ++++++++++++++++++++++++++ src/types/pushTypes.ts | 65 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 src/db/couchUser.ts diff --git a/src/db/couchSettings.ts b/src/db/couchSettings.ts index 5dba2f5..ecf5f8a 100644 --- a/src/db/couchSettings.ts +++ b/src/db/couchSettings.ts @@ -1,4 +1,4 @@ -import { asArray, asMaybe, asNumber, asObject, asString } from 'cleaners' +import { asArray, asBoolean, asMaybe, asNumber, asObject, asString } from 'cleaners' import { asReplicatorSetupDocument, DatabaseSetup, @@ -21,14 +21,22 @@ const asSettings = asObject({ priceCheckInMinutes: asMaybe(asNumber, 5) }) +const asDefaultThresholds = asObject({ + anomaly: asBoolean +}) + export const syncedReplicators = syncedDocument( 'replicators', asReplicatorSetupDocument ) export const syncedSettings = syncedDocument('settings', asSettings.withRest) +export const syncedDefaultThresholds = syncedDocument( + 'defaultThresholds', + asDefaultThresholds.withRest +) export const settingsSetup: DatabaseSetup = { name: 'push-settings', - syncedDocuments: [syncedReplicators, syncedSettings] + syncedDocuments: [syncedReplicators, syncedSettings, syncedDefaultThresholds] } diff --git a/src/db/couchUser.ts b/src/db/couchUser.ts new file mode 100644 index 0000000..8c45a35 --- /dev/null +++ b/src/db/couchUser.ts @@ -0,0 +1,42 @@ +import { + asArray, + asBoolean, + asMap, + asMaybe, + asObject, + asString +} from 'cleaners' +import { asCouchDoc } from 'edge-server-tools' +import { domainToASCII } from 'url' + +import { User } from '../types/pushTypes' + +const asCouchUser = asCouchDoc>( + asObject({ + devices: asObject(asBoolean), + notifications: asObject({ + enabled: asBoolean, + currencyCodes: asObject( + asObject({ + '1': asBoolean, + '24': asBoolean + }) + ) + }) + }) +) +type CouchUser = ReturnType + +export async function getUserById( + connection: ServerScope, + id: string +): Promise {} + +export async function updateUserById( + connection: ServerScope, + user: User +): Promise {} + +function unpackUser(doc: CouchUser): User { + return { ...doc.doc, loginId: doc.id, notifications: Object.keys(doc.notifications.currencyCodes).map(key => ...arguments.FutureNotifType) } +} diff --git a/src/types/pushTypes.ts b/src/types/pushTypes.ts index 979fcb8..40bdf6a 100644 --- a/src/types/pushTypes.ts +++ b/src/types/pushTypes.ts @@ -28,3 +28,68 @@ export interface ApiKey { admin: boolean adminsdk?: FirebaseAdminKey } + +export interface User { + loginId: string + devices: string[] + notifications: FutureNotifType[] +} + +export interface NewDevice { + deviceId: string // from Firebase + loginIds: string[] + + subscriptions: Subscription[] +} + +export interface NewUser { + loginId: string // Username, hashed + deviceIds: string[] + + subscriptions: Subscription[] +} + +type Subscription = + | { + type: 'price-change' + } + | { + type: 'price-level' + } + +// These are one-shots, +// and will go away once expired +export interface Events { + deviceId?: string + loginId?: string + created: Date // Older than 6 months = expired, ignore it + // Older than 2 months, refresh it in the latest db. + action: + | { + type: 'price-change' + tokenId: string + pluginId: string // "bitcoin" / "ethereum" + percentage: number + perHours: 24 | 1 + } + | { + type: 'price' + above: number + tokenId: string + pluginId: string // "bitcoin" / "ethereum" + } + | { + type: 'tx-confirm' + tokenId: string + pluginId: string // "bitcoin" / "ethereum" + percentage: number + perHours: 24 | 1 + } + | { + type: 'address-balance' + tokenId: string + pluginId: string // "bitcoin" / "ethereum" + percentage: number + perHours: 24 | 1 + } +}