diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index cd24e97..b107ff1 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -31,6 +31,12 @@ jobs: run: | docker build \ --build-arg DATABASE_URL=${{ secrets.DATABASE_URL }} \ + --build-arg DB_HOST=${{ secrets.DB_HOST }} \ + --build-arg DB_USERNAME=${{ secrets.DB_USERNAME }} \ + --build-arg DB_PASSWORD=${{ secrets.DB_PASSWORD }} \ + --build-arg DB_PORT=${{ secrets.DB_PORT }} \ + --build-arg DB_DATABASE=${{ secrets.DB_DATABASE }} \ + --build-arg JWT_SECRET=${{ secrets.JWT_SECRET }} \ -t gitea.nelson-household.com/hard-at-work/tcg-collectors-server/tcg-collectors-server:${{ github.sha }} . docker push gitea.nelson-household.com/hard-at-work/tcg-collectors-server/tcg-collectors-server:${{ github.sha }} diff --git a/Dockerfile b/Dockerfile index d1a5e5c..c1b79c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,20 @@ FROM node:24-alpine ARG DATABASE_URL +ARG DB_HOST +ARG DB_USERNAME +ARG DB_PASSWORD +ARG DB_PORT +ARG DB_DATABASE +ARG JWT_SECRET ENV DATABASE_URL=${DATABASE_URL} +ENV DB_HOST=${DB_HOST} +ENV DB_USERNAME=${DB_USERNAME} +ENV DB_PASSWORD=${DB_PASSWORD} +ENV DB_PORT=${DB_PORT} +ENV DB_DATABASE=${DB_DATABASE} +ENV JWT_SECRET=${JWT_SECRET} WORKDIR /app COPY . . diff --git a/src/app.js b/src/app.js index d08097f..c62d529 100644 --- a/src/app.js +++ b/src/app.js @@ -3,6 +3,8 @@ import path from 'path' import cors from 'cors' import healthCheckRoutes from './routes/healthcheck' import authRoutes from './routes/auth' +import userRoutes from './routes/users' +import roleRoutes from './routes/roles' const app = express() const port = process.env.PORT || 3000 @@ -15,6 +17,8 @@ app.use(cors()) // Mount Routes app.use('/healthcheck', healthCheckRoutes) app.use('/auth', authRoutes) +app.use('/users', userRoutes) +app.use('/roles', roleRoutes) // Default Route (Catch-all for undefined routes) app.use((req, res, next) => { diff --git a/src/config/index.js b/src/config/index.js index c3f9d4b..e7adcca 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -7,4 +7,7 @@ export default { database: process.env.DB_NAME || 'your_db_name', port: process.env.DB_PORT || 5432, }, + jwtEnv: { + secret: process.env.JWT_SECRET || '', + }, } diff --git a/src/routes/auth/index.js b/src/routes/auth/index.js index 23d0690..f6e0d4a 100644 --- a/src/routes/auth/index.js +++ b/src/routes/auth/index.js @@ -1,7 +1,7 @@ import express from 'express' import jwt from 'jsonwebtoken' import bcrypt from 'bcryptjs' -import { database } from '../../config' +import { database, jwtEnv } from '../../config' const { Pool } = require('pg') @@ -35,7 +35,7 @@ router.post('/login', async (req, res) => { // Issue JWT token const token = jwt.sign( { id: user.id, username: user.username }, - process.env.JWT_SECRET, + jwtEnv.secret, { expiresIn: '1h' } ) @@ -56,12 +56,12 @@ router.post('/refresh-token', async (req, res) => { try { // Verify the refresh token - const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET) + const decoded = jwt.verify(refreshToken, jwtEnv.secret) // Create a new access token (JWT) const token = jwt.sign( { id: decoded.id, username: decoded.username }, - process.env.JWT_SECRET, + jwtEnv.secret, { expiresIn: '1h' } ) diff --git a/src/routes/roles/index.js b/src/routes/roles/index.js new file mode 100644 index 0000000..6530e17 --- /dev/null +++ b/src/routes/roles/index.js @@ -0,0 +1,93 @@ +import express from 'express' +import { database } from '../../config' + +const { Pool } = require('pg') + +const router = express.Router() + +// Create a connection pool to the database +const pool = new Pool(database) + +// Create a new role +router.post('/', async (req, res) => { + const { name } = req.body + const client = await pool.connect() + + try { + await client.query('BEGIN') + const result = await client.query( + 'INSERT INTO roles (name, created_at, updated_at) VALUES ($1, NOW(), NOW()) RETURNING *', + [name] + ) + await client.query('COMMIT') + res.status(201).json(result.rows[0]) + } catch (err) { + await client.query('ROLLBACK') + res.status(500).json({ error: err.message }) + } +}) + +// Get all roles +router.get('/', async (req, res) => { + try { + const result = await pool.query('SELECT * FROM roles') + res.json(result.rows) + } catch (err) { + res.status(500).json({ error: err.message }) + } +}) + +// Get a single role by id +router.get('/:id', async (req, res) => { + const { id } = req.params + try { + const result = await pool.query('SELECT * FROM roles WHERE id = $1', [id]) + if (result.rows.length === 0) + return res.status(404).json({ error: 'Role not found' }) + res.json(result.rows[0]) + } catch (err) { + res.status(500).json({ error: err.message }) + } +}) + +// Update a role by id +router.put('/:id', async (req, res) => { + const { id } = req.params + const { name } = req.body + const client = await pool.connect() + + try { + await client.query('BEGIN') + const result = await client.query( + 'UPDATE roles SET name = $1, updated_at = NOW() WHERE id = $2 RETURNING *', + [name, id] + ) + if (result.rows.length === 0) + return res.status(404).json({ error: 'Role not found' }) + await client.query('COMMIT') + res.json(result.rows[0]) + } catch (err) { + await client.query('ROLLBACK') + res.status(500).json({ error: err.message }) + } +}) + +// Delete a role by id +router.delete('/:id', async (req, res) => { + const { id } = req.params + const client = await pool.connect() + + try { + await client.query('BEGIN') + const result = await client.query('DELETE FROM roles WHERE id = $1', [id]) + if (result.rows.length === 0) + return res.status(404).json({ error: 'Role not found' }) + await client.query('COMMIT') + res.json(result.rows[0]) + } catch (err) { + await client.query('ROLLBACK') + res.status(500).json({ error: err.message }) + } +}) + +export default router diff --git a/src/routes/users/index.js b/src/routes/users/index.js new file mode 100644 index 0000000..09cc91d --- /dev/null +++ b/src/routes/users/index.js @@ -0,0 +1,158 @@ +import express from 'express' +import { database } from '../../config' + +const { Pool } = require('pg') + +const router = express.Router() + +// Create a connection pool to the database +const pool = new Pool(database) + +/** + * Get all users. + */ +router.get('/', async (req, res) => { + const { rows } = await pool.query('SELECT * FROM users') + res.json(rows) +}) + +/** + * Create a new user. + * + * @param {string} name - The user's name + * @param {string} email - The user's email address + * @param {string} password - The user's password (must be hashed) + * @param {string} role_name - The name of the role to assign to the user + */ +router.post('/', async (req, res) => { + const { name, email, password, role_name } = req.body + + const client = await pool.connect() + try { + await client.query('BEGIN') + + // Query for the UUID of the role by name + const roleResult = await client.query( + 'SELECT id FROM roles WHERE name = $1', + [role_name] + ) + if (roleResult.rows.length === 0) { + throw new Error(`Role ${role_name} not found`) + } + const roleId = roleResult.rows[0].id + + // Insert the new user into the database + const result = await client.query( + 'INSERT INTO users (name, email, password, role_id, created_at, updated_at) VALUES ($1, $2, $3, $4, NOW(), NOW()) RETURNING *', + [name, email, password, roleId] + ) + + // Return the newly created user + res.status(201).json(result.rows[0]) + + await client.query('COMMIT') + } catch (err) { + await client.query('ROLLBACK') + res.status(500).send(err) + } finally { + client.release() + } +}) + +/** + * Update an existing user. + * + * @param {number} id - The ID of the user to update + * @param {string} name - The new name for the user + * @param {string} email - The new email address for the user (optional) + * @param {string} password - The new password for the user (optional, must be hashed) + * @param {string} role_name - The name of the new role to assign to the user (optional) + */ +router.put('/:id', async (req, res) => { + const { id } = req.params + const { name, email, password, role_name } = req.body + + const client = await pool.connect() + try { + await client.query('BEGIN') + + // Query for the UUID of the role by name if provided + let roleId = null + if (role_name) { + const roleResult = await client.query( + 'SELECT id FROM roles WHERE name = $1', + [role_name] + ) + if (roleResult.rows.length === 0) { + throw new Error(`Role ${role_name} not found`) + } + roleId = roleResult.rows[0].id + } + + // Update the user in the database + let queryText = 'UPDATE users SET name = $1' + const values = [name] + + if (email) { + queryText += ', email = $2' + values.push(email) + } + + if (password) { + queryText += ', password = $3' + values.push(password) + } + + if (roleId !== null) { + queryText += ', role_id = $4' + values.push(roleId) + } + + queryText += ' , updated_at = NOW() WHERE id = $5 RETURNING *' + values.push(id) + + const result = await client.query(queryText, values) + + // Return the updated user + res.json(result.rows[0]) + + await client.query('COMMIT') + } catch (err) { + await client.query('ROLLBACK') + res.status(500).send(err) + } finally { + client.release() + } +}) + +/** + * Delete an existing user. + * + * @param {number} id - The ID of the user to delete + */ +router.delete('/:id', async (req, res) => { + const { id } = req.params + + const client = await pool.connect() + try { + await client.query('BEGIN') + + // Delete the user from the database + const result = await client.query( + 'DELETE FROM users WHERE id = $1 RETURNING *', + [id] + ) + + // Return the deleted user + res.json(result.rows[0]) + + await client.query('COMMIT') + } catch (err) { + await client.query('ROLLBACK') + res.status(500).send(err) + } finally { + client.release() + } +}) + +export default router