From a26933b918d7a8fff5f68347d583e02ae09ffcd4 Mon Sep 17 00:00:00 2001 From: William Swanson Date: Tue, 3 Dec 2024 16:56:05 -0800 Subject: [PATCH] Remove legacy database types We keep the legacy routes around, but turn them into no-ops. Registering a new device still works, since we still want to send security notifications to legacy devices. --- src/cli/cli.ts | 2 - src/cli/commands/migrateDevices.ts | 72 ------------- src/models/Device.ts | 31 ------ src/models/User.ts | 116 -------------------- src/models/User/views.ts | 23 ---- src/models/base.ts | 166 ----------------------------- src/server/routes/legacyRoutes.ts | 147 ++++++++++++------------- 7 files changed, 66 insertions(+), 491 deletions(-) delete mode 100644 src/cli/commands/migrateDevices.ts delete mode 100644 src/models/Device.ts delete mode 100644 src/models/User.ts delete mode 100644 src/models/User/views.ts delete mode 100644 src/models/base.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 5beb682..09dd914 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -6,7 +6,6 @@ import { setupDatabases } from '../db/couchSetup' import { serverConfig } from '../serverConfig' import { ServerContext } from './cliTools' import { GetDevice } from './commands/getDevice' -import { MigrateDevices } from './commands/migrateDevices' import { PushMarketing } from './commands/pushMarketing' import { SendMessage } from './commands/sendMessage' @@ -30,7 +29,6 @@ async function main(): Promise { // Our commands: cli.register(GetDevice) - cli.register(MigrateDevices) cli.register(SendMessage) cli.register(PushMarketing) diff --git a/src/cli/commands/migrateDevices.ts b/src/cli/commands/migrateDevices.ts deleted file mode 100644 index 42f5c0e..0000000 --- a/src/cli/commands/migrateDevices.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Command } from 'clipanion' -import { asMaybeNotFoundError, viewToStream } from 'edge-server-tools' -import { base64 } from 'rfc4648' - -import { getDeviceById } from '../../db/couchDevices' -import { syncedSettings } from '../../db/couchSettings' -import { asLegacyDevice } from '../../models/Device' -import { asLegacyUser } from '../../models/User' -import { base58 } from '../../util/base58' -import { makeHeartbeat } from '../../util/heartbeat' -import { verifyData } from '../../util/verifyData' -import { ServerContext } from '../cliTools' - -export class MigrateDevices extends Command { - static paths = [['migrate-devices']] - static usage = { description: 'Migrate v1 devices to the v2 database' } - - async execute(): Promise { - const { connection, stderr, stdout } = this.context - const legacyUserDb = connection.use('db_user_settings') - const legacyDeviceDb = connection.use('db_devices') - - const now = new Date() - const heartbeat = makeHeartbeat(stdout) - - for await (const raw of viewToStream( - async params => await legacyUserDb.list(params) - )) { - try { - const clean = asLegacyUser(raw) - const loginId = base58.parse(clean.id) - const { devices } = clean.doc - - for (const deviceId of Object.keys(devices)) { - if (!devices[deviceId]) continue - - const raw = await legacyDeviceDb.get(deviceId).catch(error => { - if (asMaybeNotFoundError(error) != null) return - throw error - }) - if (raw == null) continue - const clean = asLegacyDevice(raw) - - const deviceRow = await getDeviceById(connection, deviceId, now) - const { device } = deviceRow - if (deviceRow.exists) { - // Add the user to the list: - if (device.loginIds.find(row => verifyData(loginId, row)) == null) { - device.loginIds = [...device.loginIds, loginId] - await deviceRow.save() - } - } else { - // Create the device: - device.deviceToken = clean.doc.tokenId - if (clean.doc.appId === '') { - device.apiKey = syncedSettings.doc.apiKeys[0].apiKey - } - device.loginIds = [loginId] - await deviceRow.save() - } - } - - heartbeat(base64.stringify(loginId)) - } catch (error) { - const id: string = (raw as any)._id - stderr.write(`Could not migrate user ${id} ${String(error)}\n`) - } - } - - return 0 - } -} diff --git a/src/models/Device.ts b/src/models/Device.ts deleted file mode 100644 index e541f8d..0000000 --- a/src/models/Device.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { asNumber, asObject, asOptional, asString } from 'cleaners' -import { asCouchDoc } from 'edge-server-tools' -import Nano from 'nano' - -import { serverConfig } from '../serverConfig' -import { Base } from './base' - -const nanoDb = Nano(serverConfig.couchUri) -const dbDevices = nanoDb.db.use>('db_devices') - -const asDevice = asObject({ - appId: asString, - tokenId: asOptional(asString), - deviceDescription: asString, - osType: asString, - edgeVersion: asString, - edgeBuildNumber: asNumber -}) -export const asLegacyDevice = asCouchDoc(asDevice) - -export class Device extends Base implements ReturnType { - public static table = dbDevices - public static asType = asDevice - - public appId!: string - public tokenId!: string | undefined - public deviceDescription!: string - public osType!: string - public edgeVersion!: string - public edgeBuildNumber!: number -} diff --git a/src/models/User.ts b/src/models/User.ts deleted file mode 100644 index 2abae06..0000000 --- a/src/models/User.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable @typescript-eslint/no-dynamic-delete */ -/* eslint-disable @typescript-eslint/strict-boolean-expressions */ - -import { asBoolean, asMap, asObject, asOptional } from 'cleaners' -import { asCouchDoc } from 'edge-server-tools' -import Nano from 'nano' - -import { serverConfig } from '../serverConfig' -import { Base } from './base' -import { Device } from './Device' - -const nanoDb = Nano(serverConfig.couchUri) -const dbUserSettings = - nanoDb.db.use>('db_user_settings') - -const asUserDevices = asMap(asBoolean) -const asUserCurrencyHours = asObject({ - '1': asBoolean, - '24': asBoolean -}) -const asUserCurrencyCodes = asMap(asUserCurrencyHours) -const asUserNotifications = asObject({ - enabled: asOptional(asBoolean), - currencyCodes: asUserCurrencyCodes -}) -const asUser = asObject({ - devices: asUserDevices, - notifications: asUserNotifications -}) -export const asLegacyUser = asCouchDoc(asUser) - -export interface INotificationsEnabledViewResponse { - devices: ReturnType - currencyCodes: ReturnType -} - -export type IDevicesByCurrencyHoursViewResponse = ReturnType< - typeof asUserDevices -> - -export class User extends Base implements ReturnType { - public static table = dbUserSettings - public static asType = asUser - - public devices: ReturnType - public notifications: ReturnType - - // @ts-expect-error - constructor(...args) { - super(...args) - - // @ts-expect-error - if (!this.devices) this.devices = {} - // @ts-expect-error - if (!this.notifications) { - this.notifications = { - enabled: true, - currencyCodes: {} - } - } - } - - // Fetch data for users that have notifications enabled using CouchDB Design Document View - // https://notif1.edge.app:6984/_utils/#/database/db_user_settings/_design/filter/_view/by-currency - public static async devicesByCurrencyHours( - currencyCode: string, - hours: string - ) { - return await User.table.view( - 'filter', - 'by-currency', - { key: [currencyCode, hours] } - ) - } - - public async fetchDevices(): Promise { - const devices: Device[] = [] - - let updated = false - for (const deviceId in this.devices) { - const device = await Device.fetch(deviceId) - if (device) { - devices.push(device) - continue - } - - delete this.devices[deviceId] - updated = true - } - - if (updated) await this.save() - - return devices - } - - public async registerNotifications(currencyCodes: string[]) { - const currencyCodesToUnregister = Object.keys( - this.notifications.currencyCodes - ).filter(code => !currencyCodes.includes(code)) - for (const code of currencyCodesToUnregister) { - delete this.notifications.currencyCodes[code] - } - - for (const code of currencyCodes) { - if (code in this.notifications.currencyCodes) continue - - this.notifications.currencyCodes[code] = { - '1': true, - '24': true - } - } - - await this.save() - } -} diff --git a/src/models/User/views.ts b/src/models/User/views.ts deleted file mode 100644 index 75b324a..0000000 --- a/src/models/User/views.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable @typescript-eslint/strict-boolean-expressions */ - -declare function emit(...args: any[]): void - -export const views = { - filter: { - // @ts-expect-error - byCurrency(doc) { - const notifs = doc.notifications - if (notifs?.enabled && notifs.currencyCodes) { - const codes = notifs.currencyCodes - for (const currencyCode in codes) { - for (const hours in codes[currencyCode]) { - const enabled = codes[currencyCode][hours] - if (enabled) { - emit([currencyCode, hours], doc.devices) - } - } - } - } - } - } -} diff --git a/src/models/base.ts b/src/models/base.ts deleted file mode 100644 index 2f2a8b2..0000000 --- a/src/models/base.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { asObject, Cleaner } from 'cleaners' -import Nano from 'nano' - -import { logger } from '../util/logger' - -const asModelData = asObject({}) - -type InstanceClass any> = (new ( - ...args: any -) => InstanceType) & - T - -export class Base implements ReturnType { - public static table: Nano.DocumentScope - public static asType: Cleaner = asModelData - - public _id!: string - public _rev!: string - public readonly dataValues: object - - constructor(data: Nano.MaybeDocument = {}, id?: string) { - this.dataValues = {} - - // NOTE: Must use set/get functions in Base constructor since the Proxy isn't setup yet. Subclasses can access - // and set properties directly - this.set(data) - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (!this.get('_id')) this.set('_id', id) - - return new Proxy(this, { - set(target: Base, key: PropertyKey, value: any): any { - // @ts-expect-error - return key in target ? (target[key] = value) : target.set(key, value) - }, - get(target: Base, key: PropertyKey): any { - // @ts-expect-error - return key in target ? target[key] : target.get(key) - } - }) - } - - public validate(): void { - ;(this.constructor as typeof Base).asType(this.dataValues) - } - - public processAPIResponse(response: Nano.DocumentInsertResponse): void { - if (response.ok) { - this._id = response.id - this._rev = response.rev - } - } - - public static async create( - this: InstanceClass, - data: Nano.MaybeDocument = {}, - id?: string - ): Promise> { - const item = new this(data, id) - await item.save() - return item - } - - public static async fetch( - this: InstanceClass, - id: string - ): Promise> { - // @ts-expect-error - let item: InstanceType = null - - try { - const doc = await this.table.get(id, { latest: true }) - item = new this(doc) - item.validate() - return item - } catch (err: any) { - if (err.statusCode === 404) { - logger.warn(`Item with ID "${id}" does not exist`) - } else { - throw err - } - } - - return item - } - - public static async all( - this: InstanceClass - ): Promise>> { - const response = await this.table.list({ include_docs: true }) - return response.rows.map(row => { - const item: InstanceType = new this(row.doc) - item.validate() - return item - }) - } - - public static async where( - this: InstanceClass, - where: Nano.MangoQuery - ): Promise>> { - const response = await this.table.find(where) - return response.docs.map(doc => { - const item: InstanceType = new this(doc) - item.validate() - return item - }) - } - - public get(key: PropertyKey): any { - // @ts-expect-error - return this.dataValues[key] - } - - public set(key: Nano.MaybeDocument | PropertyKey, value?: any): this { - if (typeof key === 'object') { - for (const prop in key) { - // eslint-disable-next-line no-prototype-builtins - if (key.hasOwnProperty(prop)) { - // @ts-expect-error - this.dataValues[prop] = key[prop] - } - } - } else { - // @ts-expect-error - this.dataValues[key] = value - } - - return this - } - - public async save( - key?: Nano.MaybeDocument | string, - value?: any - ): Promise { - const ItemClass = this.constructor as typeof Base - try { - // @ts-expect-error - this.set(key, value) - - this.validate() - - const response = await ItemClass.table.insert(this) - this.processAPIResponse(response) - return this - } catch (err: any) { - switch (err.statusCode) { - case 404: - throw new Error('Database does not exist') - - case 409: { - logger.warn( - 'Document already exists. Fetching current `_rev` and resaving.' - ) - const { _rev } = await ItemClass.fetch(this._id) - return await this.save('_rev', _rev) - } - default: - throw err - } - } - } - - public toJSON(): object { - return this.dataValues - } -} diff --git a/src/server/routes/legacyRoutes.ts b/src/server/routes/legacyRoutes.ts index 2d96134..9119a8a 100644 --- a/src/server/routes/legacyRoutes.ts +++ b/src/server/routes/legacyRoutes.ts @@ -10,14 +10,36 @@ import { import { Serverlet } from 'serverlet' import { getDeviceById } from '../../db/couchDevices' -import { Device } from '../../models/Device' -import { User } from '../../models/User' import { ApiRequest } from '../../types/requestTypes' -import { errorResponse, jsonResponse } from '../../types/responseTypes' +import { jsonResponse } from '../../types/responseTypes' import { base58 } from '../../util/base58' import { checkPayload } from '../../util/checkPayload' import { verifyData } from '../../util/verifyData' +interface LegacyDevice { + appId: string + tokenId: string | undefined + deviceDescription: string + osType: string + edgeVersion: string + edgeBuildNumber: number +} + +interface LegacyUser { + devices: { + [deviceId: string]: boolean + } + notifications: { + enabled: boolean + currencyCodes: { + [currencyCode: string]: { + 1: boolean + 24: boolean + } + } + } +} + /** * The GUI names this `registerDevice`, and calls it at boot. * @@ -26,7 +48,7 @@ import { verifyData } from '../../util/verifyData' * Response body: unused */ export const registerDeviceV1Route: Serverlet = async request => { - const { apiKey, connection, date, json, log, query } = request + const { apiKey, connection, date, json, query } = request const checkedQuery = checkPayload(asRegisterDeviceQuery, query) if (checkedQuery.error != null) return checkedQuery.error @@ -36,16 +58,6 @@ export const registerDeviceV1Route: Serverlet = async request => { if (checkedBody.error != null) return checkedBody.error const { clean } = checkedBody - let device = await Device.fetch(deviceId) - if (device != null) { - await device.save(clean as any) - log('Device updated.') - } else { - device = new Device(clean as any, deviceId) - await device.save() - log(`Device registered.`) - } - // Update the v2 device: { const deviceRow = await getDeviceById(connection, deviceId, date) @@ -56,7 +68,8 @@ export const registerDeviceV1Route: Serverlet = async request => { await deviceRow.save() } - return jsonResponse(device) + const result: LegacyDevice = clean + return jsonResponse(result) } /** @@ -68,19 +81,16 @@ export const registerDeviceV1Route: Serverlet = async request => { * Response body: { notifications: { enabled: boolean } } */ export const fetchStateV1Route: Serverlet = async request => { - const { log, query } = request + const { query } = request const checkedQuery = checkPayload(asUserIdQuery, query) if (checkedQuery.error != null) return checkedQuery.error - const { userId } = checkedQuery.clean + // const { userId } = checkedQuery.clean - const result = await User.fetch(userId) - if (result == null) { - return errorResponse(`Cannot find user ${userId}`, { status: 404 }) + const result: LegacyUser = { + devices: {}, + notifications: { enabled: false, currencyCodes: {} } } - - log(`Got user settings for ${userId}`) - return jsonResponse(result) } @@ -92,23 +102,12 @@ export const fetchStateV1Route: Serverlet = async request => { * Response body: unused */ export const attachUserV1Route: Serverlet = async request => { - const { connection, date, log, query } = request + const { connection, date, query } = request const checkedQuery = checkPayload(asAttachUserQuery, query) if (checkedQuery.error != null) return checkedQuery.error const { deviceId, userId } = checkedQuery.clean - const device = await Device.fetch(deviceId) - if (device == null) { - return errorResponse(`Cannot find device ${deviceId}`, { status: 404 }) - } - - const user = (await User.fetch(userId)) ?? new User(null, userId) - user.devices[deviceId] = true - await user.save() - - log(`Successfully attached device "${deviceId}" to user "${userId}"`) - // Update the v2 device: { const deviceRow = await getDeviceById(connection, deviceId, date) @@ -120,7 +119,11 @@ export const attachUserV1Route: Serverlet = async request => { } } - return jsonResponse(user) + const result: LegacyUser = { + devices: {}, + notifications: { enabled: false, currencyCodes: {} } + } + return jsonResponse(result) } /** @@ -134,22 +137,21 @@ export const attachUserV1Route: Serverlet = async request => { export const registerCurrenciesV1Route: Serverlet< ApiRequest > = async request => { - const { log, json, query } = request + const { json, query } = request const checkedQuery = checkPayload(asUserIdQuery, query) if (checkedQuery.error != null) return checkedQuery.error - const { userId } = checkedQuery.clean + // const { userId } = checkedQuery.clean const checkedBody = checkPayload(asRegisterCurrenciesBody, json) if (checkedBody.error != null) return checkedBody.error - const { currencyCodes } = checkedBody.clean - - const user = (await User.fetch(userId)) ?? new User(null, userId) - await user.registerNotifications(currencyCodes) - - log(`Registered notifications for user ${userId}: ${String(currencyCodes)}`) + // const { currencyCodes } = checkedBody.clean - return jsonResponse(user) + const result: LegacyUser = { + devices: {}, + notifications: { enabled: false, currencyCodes: {} } + } + return jsonResponse(result) } /** @@ -161,25 +163,20 @@ export const registerCurrenciesV1Route: Serverlet< * Response body: { '24': number, '1': number } */ export const fetchCurrencyV1Route: Serverlet = async request => { - const { log, path, query } = request + const { query } = request const checkedQuery = checkPayload(asUserIdQuery, query) if (checkedQuery.error != null) return checkedQuery.error - const { userId } = checkedQuery.clean + // const { userId } = checkedQuery.clean - const match = path.match(/notifications\/([0-9A-Za-z]+)\/?$/) - const currencyCode = match != null ? match[1] : '' + // const match = path.match(/notifications\/([0-9A-Za-z]+)\/?$/) + // const currencyCode = match != null ? match[1] : '' - const user = (await User.fetch(userId)) ?? new User(null, userId) - const currencySettings = user.notifications.currencyCodes[currencyCode] ?? { + return jsonResponse({ '1': false, '24': false, fallbackSettings: true - } - - log(`Got notification settings for ${currencyCode} for user ${userId}`) - - return jsonResponse(currencySettings) + }) } /** @@ -191,31 +188,22 @@ export const fetchCurrencyV1Route: Serverlet = async request => { * Response body: unused */ export const enableCurrencyV1Route: Serverlet = async request => { - const { log, json, path, query } = request + const { json, query } = request const checkedQuery = checkPayload(asUserIdQuery, query) if (checkedQuery.error != null) return checkedQuery.error - const { userId } = checkedQuery.clean + // const { userId } = checkedQuery.clean const checkedBody = checkPayload(asEnableCurrencyBody, json) if (checkedBody.error != null) return checkedBody.error - const { hours, enabled } = checkedBody.clean - const match = path.match(/notifications\/([0-9A-Za-z]+)\/?$/) - const currencyCode = match != null ? match[1] : '' + // const match = path.match(/notifications\/([0-9A-Za-z]+)\/?$/) + // const currencyCode = match != null ? match[1] : '' - const user = (await User.fetch(userId)) ?? new User(null, userId) - const currencySettings = user.notifications.currencyCodes[currencyCode] ?? { + return jsonResponse({ '1': false, '24': false - } - user.notifications.currencyCodes[currencyCode] = currencySettings - currencySettings[hours] = enabled - await user.save() - - log(`Updated notification settings for user ${userId} for ${currencyCode}`) - - return jsonResponse(currencySettings) + }) } /** @@ -227,23 +215,20 @@ export const enableCurrencyV1Route: Serverlet = async request => { * Response body: unused */ export const toggleStateV1Route: Serverlet = async request => { - const { log, json, query } = request + const { json, query } = request const checkedQuery = checkPayload(asUserIdQuery, query) if (checkedQuery.error != null) return checkedQuery.error - const { userId } = checkedQuery.clean + // const { userId } = checkedQuery.clean const checkedBody = checkPayload(asToggleStateBody, json) if (checkedBody.error != null) return checkedBody.error - const { enabled } = checkedBody.clean - const user = (await User.fetch(userId)) ?? new User(null, userId) - user.notifications.enabled = enabled - await user.save() - - log(`User notifications toggled to: ${String(enabled)}`) - - return jsonResponse(user) + const result: LegacyUser = { + devices: {}, + notifications: { enabled: false, currencyCodes: {} } + } + return jsonResponse(result) } const asAttachUserQuery = asObject({