-
-
Couldn't load subscription status.
- Fork 0
Feat/transfer routes #595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jona159
wants to merge
81
commits into
dev
Choose a base branch
from
feat/transfer-routes
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Feat/transfer routes #595
Changes from all commits
Commits
Show all changes
81 commits
Select commit
Hold shift + click to select a range
35eeb87
feat: add draft for port of user registration to resource route
scheidtdav 42593f0
feat: partly implement refresh token
scheidtdav dcd635f
docs: simplify contributing and add info about api routes and shared …
scheidtdav be4ebed
feat(api): finalize user registration endpoint
scheidtdav b4b8421
fix(tests): get the tests to run be reconfiguring build steps
scheidtdav 8fbb075
docs(db): readd db setup and seed scripts with README info for it
scheidtdav 1de6e69
fix: wrong import of utils
scheidtdav 08f4405
refactor: remove leftover custom server stuff
scheidtdav 4a3f8e4
fix(tests): add missing refresh token table
scheidtdav 29d3034
fix(tests): reenable remaining tests for registration
scheidtdav d164738
fix(ci): remove playwright and use correct node version
scheidtdav 7566724
fix(ci): run the tests with a postgres container
scheidtdav 44894b4
feat(tests): add coverage report
scheidtdav 77b4cc9
fix(build): reorganize server modules to correctly split client/ server
scheidtdav 5612a5f
fix(build): miss an import
scheidtdav 2e335d5
fix(build): remove leftovers from custom server implementation
scheidtdav bbf5430
chore(deps): bump react-router dependencies
scheidtdav bf19c7e
chore(deps): update react-router
scheidtdav 7d91045
feat/user me api (#559)
scheidtdav a5699de
feat(api): add root route (#560)
scheidtdav 760914b
start
JerryVincent 81a1f9c
new commit
JerryVincent acf1770
tested docs
JerryVincent eedb806
added a route
JerryVincent a6245ea
Added API Docs
JerryVincent 4712c6a
modified
JerryVincent f63dc07
removed unsupported packages
JerryVincent 7741945
updated
JerryVincent 3b620a0
Modified
JerryVincent f51f518
script generation without using ts-node.
JerryVincent 5f7a6ef
modified
JerryVincent f9ecca4
Merge branch 'api-prod' into feat/user-registration-api
scheidtdav bd0e52e
fix: update package-lock.json
scheidtdav 31f793c
Updated (#575)
JerryVincent c5b9835
feat: add command for drizzle studio
jona159 43798ff
feat: devices loader
jona159 a9262df
feat: load single device
jona159 e5f0e9f
feat: uncomment get boxes, delete box path
jona159 87c390d
feat(wip): add boxes test suite
jona159 6301974
feat: add devices service
jona159 e96766c
fix: some types, formatting
jona159 511b94a
Removed duplicate Documentation section (#576)
JerryVincent 98617c3
Update README.md
JerryVincent 9492e13
Feat/api email and password (#561)
scheidtdav 8a721f7
feat/api auth (#562)
scheidtdav ef552e1
feat(api): boxes for user endpoints (#573)
scheidtdav 46e89cd
feat/api misc (#571)
scheidtdav fcf4b8d
Merge branch 'dev' into feat/user-registration-api
jona159 1bd97eb
feat(api): add route and test files
scheidtdav 46b0422
feat: add test code
scheidtdav 787b662
feat: add dummy sensors to devices and implement getting them back
scheidtdav 95acb53
Merge branch 'dev' into feat/api-boxes-sensors
scheidtdav eb84619
feat: prefer dev server in no production envs and hide dev in prod
scheidtdav 84f57ca
feat(docs): start adding docs to route
scheidtdav eeb6049
feat: wip devices api
jona159 a232d6b
Merge branch 'dev' into feat/api-boxes-sensors
scheidtdav cf36b0a
feat: finish up to the point where we need measurements
scheidtdav 0a35d3b
fix: api routes without need for measurements
scheidtdav 89e8c04
fix: stats call
scheidtdav 9a8e679
fix: remaining tests
scheidtdav 32bdb9e
fix: frontend issue from changing the service implementation
scheidtdav 711ea65
fix: tests
scheidtdav 6f1520f
Merge branch 'feat/api-boxes-sensors' into feat/api-boxes
scheidtdav c0713e9
refactor: use modern syntax for assertion
scheidtdav 1bbf604
feat: adjust for zod schema
jona159 38c763f
feat: add drizzle check
jona159 6fb23fa
feat: transfer routes
jona159 00e38fe
feat(wip): transfer boxes
jona159 bc3dddd
fix: deprecated json function
jona159 611c2da
fix: imports
jona159 7d7882c
Merge branch 'dev' into feat/transfer-routes
jona159 4956550
fix: imports, json
jona159 1c665f4
fix: imports, json
jona159 3e1675d
fix: import
jona159 dd8678c
fix: import order
jona159 c943e71
fix: import order
jona159 9406ebb
Merge branch 'dev' into feat/transfer-routes
jona159 bce5938
fix: migrations
jona159 7b4f005
feat: wrap device and claim update in transaction
jona159 b76d9d3
fix: rm duplicate functions, tests
jona159 88f9f39
fix: error code
jona159 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| import { eq } from "drizzle-orm"; | ||
| import { drizzleClient } from "~/db.server"; | ||
| import { getDevice } from "~/models/device.server"; | ||
| import { createTransfer, getTransferByBoxId, isClaimExpired, removeTransfer, updateTransferExpiration } from "~/models/transfer.server"; | ||
| import { claim, type Claim, device } from "~/schema"; | ||
|
|
||
|
|
||
| export const createBoxTransfer = async ( | ||
| userId: string, | ||
| boxId: string, | ||
| expiresAtStr?: string | ||
| ): Promise<Claim> => { | ||
| const box = await getDevice({ id: boxId }); | ||
|
|
||
| if (!box) { | ||
| throw new Error("Box not found"); | ||
| } | ||
|
|
||
| if (box.user.id !== userId) { | ||
| throw new Error("You don't have permission to transfer this box"); | ||
| } | ||
|
|
||
| const existingTransfer = await getTransferByBoxId(boxId); | ||
| if (existingTransfer) { | ||
| throw new Error("Transfer already exists for this device"); | ||
| } | ||
|
|
||
| let expirationDate: Date | undefined; | ||
|
|
||
| if (expiresAtStr) { | ||
| expirationDate = new Date(expiresAtStr); | ||
|
|
||
| if (isNaN(expirationDate.getTime())) { | ||
| throw new Error("Invalid expiration date format"); | ||
| } | ||
|
|
||
| if (expirationDate <= new Date()) { | ||
| throw new Error("Expiration date must be in the future"); | ||
| } | ||
| } | ||
|
|
||
| const transferClaim = await createTransfer(boxId, expirationDate); | ||
|
|
||
| return transferClaim; | ||
| }; | ||
|
|
||
| export const getBoxTransfer = async ( | ||
| userId: string, | ||
| boxId: string | ||
| ): Promise<Claim> => { | ||
| const box = await getDevice({ id: boxId }); | ||
|
|
||
| if (!box) { | ||
| throw new Error("Box not found"); | ||
| } | ||
|
|
||
| if (box.user.id !== userId) { | ||
| throw new Error("You don't have permission to view this transfer"); | ||
| } | ||
|
|
||
| const transfer = await getTransferByBoxId(boxId); | ||
|
|
||
| if (!transfer) { | ||
| throw new Error("Transfer not found"); | ||
| } | ||
|
|
||
| if (isClaimExpired(transfer.expiresAt)) { | ||
| throw new Error("Transfer token has expired"); | ||
| } | ||
|
|
||
| return transfer; | ||
| }; | ||
|
|
||
| export const removeBoxTransfer = async ( | ||
| userId: string, | ||
| boxId: string, | ||
| token: string | ||
| ): Promise<void> => { | ||
| const box = await getDevice({id: boxId}); | ||
| if (!box) { | ||
| throw new Error("Box not found"); | ||
| } | ||
|
|
||
| if (box.user.id !== userId) { | ||
| throw new Error("You don't have permission to remove this transfer"); | ||
| } | ||
|
|
||
| await removeTransfer(boxId, token); | ||
| }; | ||
|
|
||
| export const claimBox = async (userId: string, token: string) => { | ||
| const [activeClaim] = await drizzleClient | ||
| .select() | ||
| .from(claim) | ||
| .where(eq(claim.token, token)) | ||
| .limit(1); | ||
|
|
||
| if (!activeClaim) { | ||
| throw new Error("Invalid or expired transfer token"); | ||
| } | ||
|
|
||
| if (activeClaim.expiresAt && activeClaim.expiresAt <= new Date()) { | ||
| throw new Error("Transfer token has expired"); | ||
| } | ||
|
|
||
| const [box] = await drizzleClient | ||
| .select() | ||
| .from(device) | ||
| .where(eq(device.id, activeClaim.boxId)) | ||
| .limit(1); | ||
|
|
||
| if (!box) { | ||
| throw new Error("Device not found"); | ||
| } | ||
|
|
||
| if (box.userId === userId) { | ||
| throw new Error("You already own this device"); | ||
| } | ||
|
|
||
| await drizzleClient.transaction(async (tx) => { | ||
| await tx | ||
| .update(device) | ||
| .set({ userId, updatedAt: new Date() }) | ||
| .where(eq(device.id, activeClaim.boxId)); | ||
|
|
||
| await tx | ||
| .delete(claim) | ||
| .where(eq(claim.id, activeClaim.id)); | ||
| }); | ||
|
|
||
| return { message: "Device successfully claimed!", boxId: activeClaim.boxId }; | ||
| }; | ||
|
|
||
| export const validateTransferParams = ( | ||
| boxId?: string, | ||
| expiresAt?: string | ||
| ): { isValid: boolean; error?: string } => { | ||
| if (!boxId || boxId.trim() === "") { | ||
| return { isValid: false, error: "Box ID is required" }; | ||
| } | ||
|
|
||
| if (expiresAt) { | ||
| const date = new Date(expiresAt); | ||
| if (isNaN(date.getTime())) { | ||
| return { isValid: false, error: "Invalid date format" }; | ||
| } | ||
| if (date <= new Date()) { | ||
| return { isValid: false, error: "Expiration date must be in the future" }; | ||
| } | ||
| } | ||
|
|
||
| return { isValid: true }; | ||
| }; | ||
|
|
||
| /** | ||
| * Update transfer expiration date | ||
| * Only the box owner can update their transfer expiration | ||
| * | ||
| * @param userId - ID of the requesting user | ||
| * @param boxId - ID of the box | ||
| * @param token - The transfer token (for verification) | ||
| * @param newExpiresAtStr - New expiration date as ISO string | ||
| * @returns The updated transfer claim | ||
| */ | ||
| export const updateBoxTransferExpiration = async ( | ||
| userId: string, | ||
| boxId: string, | ||
| token: string, | ||
| newExpiresAtStr: string | ||
| ): Promise<Claim> => { | ||
| const box = await getDevice({ id: boxId }); | ||
|
|
||
| if (!box) { | ||
| throw new Error("Box not found"); | ||
| } | ||
|
|
||
| if (box.user.id !== userId) { | ||
| throw new Error("You don't have permission to update this transfer"); | ||
| } | ||
|
|
||
| const transfer = await getTransferByBoxId(boxId); | ||
|
|
||
| if (!transfer) { | ||
| throw new Error("Transfer not found"); | ||
| } | ||
|
|
||
| if (transfer.token !== token) { | ||
| throw new Error("Invalid transfer token"); | ||
| } | ||
|
|
||
| if (isClaimExpired(transfer.expiresAt)) { | ||
| throw new Error("Transfer token has expired"); | ||
| } | ||
|
|
||
| const newExpiresAt = new Date(newExpiresAtStr); | ||
|
|
||
| if (isNaN(newExpiresAt.getTime())) { | ||
| throw new Error("Invalid expiration date format"); | ||
| } | ||
|
|
||
| if (newExpiresAt <= new Date()) { | ||
| throw new Error("Expiration date must be in the future"); | ||
| } | ||
|
|
||
| const updated = await updateTransferExpiration(transfer.id, newExpiresAt); | ||
|
|
||
| return updated; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import { eq } from "drizzle-orm"; | ||
| import { drizzleClient } from "~/db.server"; | ||
| import { type Claim, claim, device, type Device } from "~/schema"; | ||
|
|
||
| export interface TransferCode { | ||
| id: string; | ||
| boxId: string; | ||
| token: string; | ||
| expiresAt: Date; | ||
| createdAt: Date; | ||
| } | ||
|
|
||
| export const getDefaultExpirationDate = (): Date => { | ||
| const now = new Date(); | ||
| now.setHours(now.getHours() + 24); | ||
| return now; | ||
| }; | ||
|
|
||
| export const isClaimExpired = (expiresAt: Date | null): boolean => { | ||
| if (!expiresAt) return false; | ||
| return expiresAt <= new Date(); | ||
| }; | ||
|
|
||
| export const createTransfer = async ( | ||
| boxId: string, | ||
| expiresAt?: Date | ||
| ): Promise<Claim> => { | ||
| const token = generateTransferCode(); | ||
| const expirationDate = expiresAt || getDefaultExpirationDate(); | ||
|
|
||
| const [newClaim] = await drizzleClient | ||
| .insert(claim) | ||
| .values({ | ||
| boxId, | ||
| token, | ||
| expiresAt: expirationDate, | ||
| }) | ||
| .returning(); | ||
|
|
||
| if (!newClaim) { | ||
| throw new Error("Failed to create transfer claim"); | ||
| } | ||
|
|
||
| return newClaim; | ||
| }; | ||
|
|
||
|
|
||
| export const generateTransferCode = (): string => { | ||
| const crypto = require('crypto'); | ||
| return crypto.randomBytes(6).toString('hex'); | ||
| }; | ||
|
|
||
| export function getTransfer({ id }: Pick<Device, 'id'>){ | ||
| return drizzleClient.query.claim.findFirst({ | ||
| where: (claim, {eq}) => eq(claim.boxId, id) | ||
| }) | ||
| }; | ||
|
|
||
|
|
||
| export const getTransferByBoxId = async ( | ||
| boxId: string | ||
| ): Promise<Claim | null> => { | ||
| const [result] = await drizzleClient | ||
| .select() | ||
| .from(claim) | ||
| .where(eq(claim.boxId, boxId)) | ||
| .limit(1); | ||
|
|
||
| return result || null; | ||
| }; | ||
|
|
||
| export const deleteClaimById = async (claimId: string): Promise<void> => { | ||
| await drizzleClient.delete(claim).where(eq(claim.id, claimId)); | ||
| }; | ||
|
|
||
| export const removeTransfer = async ( | ||
| boxId: string, | ||
| token: string | ||
| ): Promise<void> => { | ||
| const [existingClaim] = await drizzleClient | ||
| .select() | ||
| .from(claim) | ||
| .where(eq(claim.token, token) && eq(claim.boxId, boxId)); | ||
|
|
||
| if (!existingClaim) { | ||
| throw new Error("Transfer token not found"); | ||
| } | ||
|
|
||
| await drizzleClient | ||
| .delete(claim) | ||
| .where(eq(claim.id, existingClaim.id)); | ||
| }; | ||
|
|
||
| export const updateTransferExpiration = async ( | ||
| claimId: string, | ||
| expiresAt: Date | ||
| ): Promise<Claim> => { | ||
| const [updated] = await drizzleClient | ||
| .update(claim) | ||
| .set({ | ||
| expiresAt, | ||
| updatedAt: new Date() | ||
| }) | ||
| .where(eq(claim.id, claimId)) | ||
| .returning(); | ||
|
|
||
| if (!updated) { | ||
| throw new Error("Failed to update transfer claim"); | ||
| } | ||
|
|
||
| return updated; | ||
| } | ||
|
|
||
| export const getTransferByToken = async ( | ||
| token: string | ||
| ): Promise<Claim | null> => { | ||
| const [result] = await drizzleClient | ||
| .select() | ||
| .from(claim) | ||
| .where(eq(claim.token, token)) | ||
| .limit(1); | ||
|
|
||
| return result || null; | ||
| }; | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be redundant, as there is
createBoxTransferintransfer-service.server.ts.Same for the other methods in this file. Why do both versions need to exist? 🤔