diff --git a/src/controllers/token/index.spec.ts b/src/controllers/token/index.spec.ts index c881a48..17352f0 100644 --- a/src/controllers/token/index.spec.ts +++ b/src/controllers/token/index.spec.ts @@ -195,6 +195,64 @@ describe("requests the /token route", () => { expect(row).toBeUndefined(); }); + it("associating a username with a token, should succeed", async () => { + await db + .insert(deviceTokensTable) + .values({ deviceId: "123", token: "123" }); + + const res = await app.request("/token", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ + token: "123", + deviceId: "123", + username: "aniplay", + }), + }); + + expect(res.json()).resolves.toEqual({ success: true }); + expect(res.status).toBe(200); + }); + + it("associating a username with a token should update existing entry", async () => { + const minimumTimestamp = DateTime.now(); + await db + .insert(deviceTokensTable) + .values({ deviceId: "123", token: "123" }); + await app.request("/token", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ + token: "123", + deviceId: "123", + username: "aniplay", + }), + }); + + const row = await db + .select() + .from(deviceTokensTable) + .where(eq(deviceTokensTable.deviceId, "123")) + .get(); + + expect(row).toEqual({ + deviceId: "123", + token: "123", + username: "aniplay", + lastConnectedAt: expect.any(String), + }); + // since SQL timestamp doesn't support milliseconds, compare to nearest second + expect( + +DateTime.fromSQL(row!.lastConnectedAt!, { zone: "utc" }).startOf( + "second", + ), + ).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second")); + }); + it("token is invalid, should fail", async () => { mock.module("src/libs/fcm/verifyFcmToken", () => ({ verifyFcmToken: () => false, diff --git a/src/controllers/token/index.ts b/src/controllers/token/index.ts index a6a8a0a..fd6abd7 100644 --- a/src/controllers/token/index.ts +++ b/src/controllers/token/index.ts @@ -69,11 +69,7 @@ app.openapi(route, async (c) => { await saveToken(env(c, "workerd"), deviceId, token, username); } catch (error) { - // when token already exists in the database - if ( - error.code === "SQLITE_CONSTRAINT" && - error.message.includes("device_tokens.token") - ) { + if (error.message === "Token already exists in the database") { return c.json(ErrorResponse, 412); } diff --git a/src/models/schema.ts b/src/models/schema.ts index 21a2521..dd3cc09 100644 --- a/src/models/schema.ts +++ b/src/models/schema.ts @@ -11,7 +11,9 @@ export const deviceTokensTable = sqliteTable("device_tokens", { token: text("token").notNull().unique(), username: text("username"), /** Used to determine if a device hasn't been used in a while. Should start to ignore tokens where the device hasn't connected in about a month. */ - lastConnectedAt: text("last_connected_at").default(sql`(CURRENT_TIMESTAMP)`), + lastConnectedAt: text("last_connected_at") + .default(sql`(CURRENT_TIMESTAMP)`) + .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), }); export const watchStatusTable = sqliteTable( diff --git a/src/models/token.ts b/src/models/token.ts index 703a8f0..ecd042b 100644 --- a/src/models/token.ts +++ b/src/models/token.ts @@ -1,4 +1,4 @@ -import { eq, sql } from "drizzle-orm"; +import { eq, or, sql } from "drizzle-orm"; import type { Env } from "~/types/env"; @@ -10,6 +10,42 @@ export function saveToken( deviceId: string, token: string, username: string | null, +) { + return getDb(env) + .select() + .from(deviceTokensTable) + .where( + or( + eq(deviceTokensTable.deviceId, deviceId), + eq(deviceTokensTable.token, token), + ), + ) + .then((existingTokens) => { + const existingToken = existingTokens.find( + ({ token: existingToken, deviceId: existingDeviceId }) => + existingToken === token || existingDeviceId === deviceId, + ); + if (!existingToken) { + return insertToken(env, deviceId, token, username); + } + + if ( + existingToken.token === token && + !existingToken.username && + !username + ) { + throw new Error("Token already exists in the database"); + } + + return updateToken(env, deviceId, token, username); + }); +} + +function insertToken( + env: Env, + deviceId: string, + token: string, + username: string | null, ) { return getDb(env) .insert(deviceTokensTable) @@ -17,6 +53,19 @@ export function saveToken( .run(); } +function updateToken( + env: Env, + deviceId: string, + token: string, + username: string | null, +) { + return getDb(env) + .update(deviceTokensTable) + .set({ token, username }) + .where(eq(deviceTokensTable.deviceId, deviceId)) + .run(); +} + export function updateDeviceLastConnectedAt(env: Env, deviceId: string) { return getDb(env) .update(deviceTokensTable)