Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
CoinRadar is a crypto portfolio tracking web application with wallet management, transaction history, and swap support.

## Stack

- Frontend: React, TypeScript, Redux Toolkit, RTK Query, Tailwind CSS, Chart.js
- Backend: Node.js, Express, TypeScript, PostgreSQL, Prisma ORM, Zod, JWT

## Key Features

- Session-based authentication with HttpOnly cookies, access/refresh tokens, and refresh token rotation
- Google OAuth sign-in with callback flow and account linking safeguards
- Wallet management with per-user wallet isolation and access control
Expand Down
23 changes: 21 additions & 2 deletions apps/backend/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/tests/setup/jest.setup.ts'],
setupFilesAfterEnv: ['<rootDir>/tests/setup/jest.setup.cjs'],
globalSetup: '<rootDir>/tests/setup/globalSetup.cjs',
globalTeardown: '<rootDir>/tests/setup/globalTeardown.cjs',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': [
'@swc/jest',
{
jsc: {
parser: {
syntax: 'typescript',
},
target: 'es2022',
},
module: {
type: 'commonjs',
},
},
],
},
maxWorkers: 1,
verbose: true,
};
14 changes: 10 additions & 4 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "backend",
"version": "1.0.0",
"main": "src/server.ts",
"type": "module",
"main": "dist/src/server.js",
"scripts": {
"dev": "nodemon --exec \"ts-node --esm src/server.ts\"",
"dev": "tsx watch src/server.ts",
"prisma": "prisma",
"seed": "ts-node ./src/prisma.ts",
"seed": "tsx ./src/prisma.ts",
"build": "prisma generate && tsc",
"test": "jest --config ./jest.config.cjs --runInBand",
"test:integration": "jest --config ./jest.config.cjs --runInBand"
Expand All @@ -14,10 +15,15 @@
"license": "ISC",
"description": "",
"devDependencies": {
"@swc/core": "^1.13.5",
"@swc/jest": "^0.2.39",
"@types/jest": "^29.5.14",
"@types/supertest": "^7.2.0",
"jest": "^29.7.0",
"nodemon": "^3.1.10",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0"
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.6"
},
"dependencies": {
"@prisma/client": "6.19.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "Transactions" RENAME TO "Transaction";
25 changes: 12 additions & 13 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,18 @@ generator client {
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
//! 7.0.0 - не стабільна версія тому залишив (6.19.0) без prisma.config.ts (a high-severity vulnerability in the 'hono' package)
}

enum BuyOrSell {
buy
sell
}

model Transactions {
model Transaction {
id String @id @default(uuid())
walletId String

coinSymbol String
coinSymbol String
swapGroupId String?

buyOrSell BuyOrSell
Expand All @@ -43,7 +42,7 @@ model Wallet {
userId String
user User @relation(fields: [userId], references: [id])

transactions Transactions[]
transactions Transaction[]
swapSettings SwapSettings?

@@unique([name, userId]) //Композитний унікальний індекс - в межах одного користувача (userId)
Expand All @@ -61,16 +60,16 @@ model User {
}

model RefreshToken {
id String @id @default(uuid())
userId String
tokenHash String @unique
expiresAt DateTime
createdAt DateTime @default(now())
revokedAt DateTime?
id String @id @default(uuid())
userId String
tokenHash String @unique
expiresAt DateTime
createdAt DateTime @default(now())
revokedAt DateTime?
replacedByTokenHash String?
userAgent String?
ip String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userAgent String?
ip String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@index([userId])
}
Expand Down
57 changes: 34 additions & 23 deletions apps/backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
require('dotenv').config();
const express = require('express');
import type { Express, Request, Response } from 'express';
const cors = require('cors');
const prisma = require('./prisma');
import "dotenv/config";
import express, { type Express, type Request, type Response } from "express";
import cors from "cors";
import prisma from "./prisma.js";

const authRouter = require('./router/authRouter');
const walletRouter = require('./router/walletRouter');
import authRouter from "./router/authRouter.js";
import walletRouter from "./router/walletRouter.js";

const app: Express = express();

const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || process.env.FRONTEND_URL || 'http://localhost:5173')
.split(',')
.map((origin: string) => origin.trim())
.filter(Boolean);
const allowedOrigins = (
process.env.CORS_ALLOWED_ORIGINS ||
process.env.FRONTEND_URL ||
"http://localhost:5173"
)
.split(",")
.map((origin: string) => origin.trim())
.filter(Boolean);

app.use(express.json());
app.use(cors({
app.use(
cors({
origin: allowedOrigins,
credentials: true,
}));
}),
);

app.use('/api/auth', authRouter);
app.use('/api/wallets', walletRouter);
app.use("/api/auth", authRouter);
app.use("/api/wallets", walletRouter);

app.get('/api/status', async (_req: Request, res: Response) => {
try {
await prisma.$queryRaw`SELECT 1`;
res.json({ message: 'API Server is running and DB is connected!', status: 'OK' });
} catch (_error) {
res.status(500).json({ message: 'API Server is running, but DB connection failed.', status: 'ERROR' });
}
app.get("/api/status", async (_req: Request, res: Response) => {
try {
await prisma.$queryRaw`SELECT 1`;
res.json({
message: "API Server is running and DB is connected!",
status: "OK",
});
} catch (_error) {
res.status(500).json({
message: "API Server is running, but DB connection failed.",
status: "ERROR",
});
}
});

module.exports = { app };
export { app };
Loading
Loading