diff --git a/.gitea/hooks/pre-commit b/.gitea/hooks/pre-commit deleted file mode 100755 index 4b6a65f..0000000 --- a/.gitea/hooks/pre-commit +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# Check if the lint fix command fails -# If it does, exit with a non-zero status to prevent the commit. - -echo "Running lint fix..." -# Replace with your actual lint fix command -# This is just an example, adapt it to your linter and project -npm run lint:fix # e.g., "eslint . --fix" - -# Example: Capture the exit code of the command -lint_fix_result=$? - -if [ $lint_fix_result -ne 0 ]; then - echo "Linting failed. Commit aborted." - exit 1 # Exit with a non-zero status to prevent the commit -fi - -echo "Lint fix completed successfully. Commit allowed." -exit 0 # Exit with a zero status to allow the commit diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml new file mode 100644 index 0000000..e55741b --- /dev/null +++ b/.gitea/workflows/test.yaml @@ -0,0 +1,26 @@ +name: Test Workflow +on: + pull_request: + branches: + - main + +jobs: + test-and-lint: + # Use the label matching your registered runner + runs-on: ubuntu-latest + # Or specify a custom image directly if your runner supports it + container: + image: node:24-alpine + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install dependencies + run: npm install --force + + - name: Run linter + run: npm run lint + + - name: Run tests + run: npm test diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..de2300b --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +export default { + presets: [['@babel/preset-env', { targets: { node: 'current' } }]], +} diff --git a/package.json b/package.json index b661d5b..9b0c7fa 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,11 @@ "description": "A basic Express application with PostgreSQL integration.", "main": "app.js", "scripts": { - "start": "node app.js", "dev": "nodemon app.js", "lint": "eslint .", "lint:fix": "eslint . --fix", - "format": "prettier --config prettier.config.js --write .", - "migrate": "node-pg-migrate" + "migrate": "node-pg-migrate", + "test": "jest --forceExit" }, "type": "module", "keywords": [ @@ -22,14 +21,21 @@ "author": "Your Name", "license": "MIT", "dependencies": { + "bcryptjs": "^3.0.3", + "body-parser": "^2.2.2", + "cors": "^2.8.6", "csv-parser": "^3.0.0", "dotenv": "^16.0.3", - "express": "^4.18.2", + "express": "^4.22.2", + "jsonwebtoken": "^9.0.3", "node-pg": "^1.0.1", "p-limit": "^7.3.0", "pg": "^8.20.0" }, "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.5", + "babel-jest": "^30.4.1", "eslint": "^10.3.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", @@ -37,6 +43,7 @@ "jest": "^29.0.0", "node-pg-migrate": "^8.0.4", "nodemon": "^3.0.0", - "prettier": "^3.8.3" + "prettier": "^3.8.3", + "supertest": "^7.2.2" } } diff --git a/app.js b/src/app.js similarity index 57% rename from app.js rename to src/app.js index b8629cf..d08097f 100644 --- a/app.js +++ b/src/app.js @@ -1,9 +1,8 @@ -const express = require('express') -const path = require('path') - -const authRoutes = require('./routes/auth') -const productsRoutes = require('./routes/products') -const usersRoutes = require('./routes/users') +import express from 'express' +import path from 'path' +import cors from 'cors' +import healthCheckRoutes from './routes/healthcheck' +import authRoutes from './routes/auth' const app = express() const port = process.env.PORT || 3000 @@ -11,14 +10,11 @@ const port = process.env.PORT || 3000 // Middleware app.use(express.json()) // For parsing application/json app.use(express.urlencoded({ extended: true })) // For parsing URL-encoded form data - -// Static Files (Serving Frontend) -app.use(express.static(path.join(__dirname, 'public'))) +app.use(cors()) // Mount Routes +app.use('/healthcheck', healthCheckRoutes) app.use('/auth', authRoutes) -app.use('/products', productsRoutes) -app.use('/users', usersRoutes) // Default Route (Catch-all for undefined routes) app.use((req, res, next) => { @@ -28,3 +24,5 @@ app.use((req, res, next) => { app.listen(port, () => { console.log(`Server listening on port ${port}`) }) + +export default app diff --git a/config/index.js b/src/config/index.js similarity index 100% rename from config/index.js rename to src/config/index.js diff --git a/migrations/001_create-image-table.js b/src/migrations/001_create-image-table.js similarity index 100% rename from migrations/001_create-image-table.js rename to src/migrations/001_create-image-table.js diff --git a/migrations/002_create-collection-table.js b/src/migrations/002_create-collection-table.js similarity index 100% rename from migrations/002_create-collection-table.js rename to src/migrations/002_create-collection-table.js diff --git a/migrations/003_create-item-table.js b/src/migrations/003_create-item-table.js similarity index 100% rename from migrations/003_create-item-table.js rename to src/migrations/003_create-item-table.js diff --git a/migrations/004_create-location-table.js b/src/migrations/004_create-location-table.js similarity index 100% rename from migrations/004_create-location-table.js rename to src/migrations/004_create-location-table.js diff --git a/migrations/005_create-set-table.js b/src/migrations/005_create-set-table.js similarity index 100% rename from migrations/005_create-set-table.js rename to src/migrations/005_create-set-table.js diff --git a/migrations/006_create-role-table.js b/src/migrations/006_create-role-table.js similarity index 100% rename from migrations/006_create-role-table.js rename to src/migrations/006_create-role-table.js diff --git a/migrations/007_create-user-table.js b/src/migrations/007_create-user-table.js similarity index 100% rename from migrations/007_create-user-table.js rename to src/migrations/007_create-user-table.js diff --git a/src/routes/auth/index.js b/src/routes/auth/index.js new file mode 100644 index 0000000..23d0690 --- /dev/null +++ b/src/routes/auth/index.js @@ -0,0 +1,75 @@ +import express from 'express' +import jwt from 'jsonwebtoken' +import bcrypt from 'bcryptjs' +import { database } from '../../config' + +const { Pool } = require('pg') + +const router = express.Router() + +// Database connection pool +const pool = new Pool(database) + +// Route to handle login and issue JWT token +router.post('/login', async (req, res) => { + const { username, password } = req.body + + try { + const client = await pool.connect() + const queryText = 'SELECT * FROM users WHERE username = $1' + const result = await client.query(queryText, [username]) + + if (!result.rows.length) { + return res.status(401).json({ message: 'Invalid credentials' }) + } + + const user = result.rows[0] + + // Compare passwords + const passwordMatch = await bcrypt.compare(password, user.password) + + if (!passwordMatch) { + return res.status(401).json({ message: 'Invalid credentials' }) + } + + // Issue JWT token + const token = jwt.sign( + { id: user.id, username: user.username }, + process.env.JWT_SECRET, + { expiresIn: '1h' } + ) + + res.json({ token }) + } catch (error) { + console.error(error) + res.status(500).json({ message: 'Internal server error' }) + } +}) + +// Route to refresh JWT token +router.post('/refresh-token', async (req, res) => { + const { refreshToken } = req.body + + if (!refreshToken) { + return res.status(401).json({ message: 'Refresh token is required' }) + } + + try { + // Verify the refresh token + const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET) + + // Create a new access token (JWT) + const token = jwt.sign( + { id: decoded.id, username: decoded.username }, + process.env.JWT_SECRET, + { expiresIn: '1h' } + ) + + res.json({ token }) + } catch (error) { + console.error(error) + res.status(403).json({ message: 'Invalid refresh token' }) + } +}) + +export default router diff --git a/src/routes/healthcheck/index.js b/src/routes/healthcheck/index.js new file mode 100644 index 0000000..961a9de --- /dev/null +++ b/src/routes/healthcheck/index.js @@ -0,0 +1,7 @@ +import express from 'express' + +const router = express.Router() + +router.get('/', (req, res) => res.json({ status: 'UP' })) + +export default router diff --git a/src/routes/healthcheck/test.js b/src/routes/healthcheck/test.js new file mode 100644 index 0000000..99343ad --- /dev/null +++ b/src/routes/healthcheck/test.js @@ -0,0 +1,19 @@ +// src/routes/healthcheck/test.js + +import request from 'supertest' +import app from '../../app' + +describe('Health Check Endpoint', () => { + it('should respond with status up', async () => { + const response = await request(app).get('/healthcheck') + + expect(response.status).toBe(200) + expect(response.body).toEqual({ status: 'UP' }) + }) + + it('should return a JSON response', async () => { + const response = await request(app).get('/healthcheck') + + expect(response.type).toBe('application/json') + }) +})