commit 5aab318e79b6404584acf13f177cd840d4132209 Author: Lightemerald Date: Thu Nov 23 16:01:56 2023 +0100 FIrst commit diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..a3a88f7 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,55 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:security/recommended" + ], + "env": { + "node": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 2022 + }, + "rules": { + "arrow-spacing": ["warn", { "before": true, "after": true }], + "brace-style": ["error", "stroustrup", { "allowSingleLine": true }], + "comma-dangle": ["error", "always-multiline"], + "comma-spacing": "error", + "comma-style": "error", + "curly": ["error", "multi-line", "consistent"], + "dot-location": ["error", "property"], + "handle-callback-err": "off", + "indent": ["error", "tab"], + "keyword-spacing": "error", + "max-nested-callbacks": ["error", { "max": 4 }], + "max-statements-per-line": ["error", { "max": 2 }], + "no-console": "off", + "no-empty-function": "error", + "no-floating-decimal": "error", + "no-inline-comments": "error", + "no-lonely-if": "error", + "no-multi-spaces": "error", + "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], + "no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }], + "no-trailing-spaces": ["error"], + "no-var": "error", + "object-curly-spacing": ["error", "always"], + "prefer-const": "error", + "quotes": ["error", "single"], + "semi": ["error", "always"], + "space-before-blocks": "error", + "space-before-function-paren": ["error", { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + }], + "space-in-parens": "error", + "space-infix-ops": "error", + "space-unary-ops": "error", + "spaced-comment": "error", + "yoda": "error", + "security/detect-non-literal-fs-filename": "off", + "security/detect-non-literal-require": "off", + "security/detect-object-injection": "off" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..468f82a --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/Classes/Computer.js b/Classes/Computer.js new file mode 100644 index 0000000..6da1cd3 --- /dev/null +++ b/Classes/Computer.js @@ -0,0 +1,109 @@ +import { database } from './Database'; + +class Computer { + constructor(id, brand, model, state, status) { + this.id = id; + this.brand = brand; + this.model = model; + this.status = status; + this.state = state; + } + + async get (id) { + try { + const rows = await database.execute('SELECT * FROM computers WHERE id = ?', [id]); + if (rows.length === 0) { + throw new Error('Computer not found'); + } else { + this.id = rows[0].id; + this.brand = rows[0].brand; + this.model = rows[0].model; + this.status = rows[0].status; + this.state = rows[0].state; + } + } catch (error) { + console.error('Error getting computer:', error); + throw error; + } + } + + async create() { + try { + const result = await database.execute( + 'INSERT INTO computers (brand, model, state, status) VALUES (?, ?, ?, ?)', + [this.brand, this.model, this.state, this.status] + ); + this.id = result.insertId; + } catch (error) { + console.error('Error creating computer:', error); + throw error; + } + } + + async update() { + try { + await database.execute( + 'UPDATE computers SET brand = ?, model = ?, state = ?, status = ? WHERE id = ?', + [this.brand, this.model, this.state, this.status, this.id] + ); + } catch (error) { + console.error('Error updating computer:', error); + throw error; + } + } + + async delete() { + try { + await database.execute('DELETE FROM computers WHERE id = ?', [this.id]); + } catch (error) { + console.error('Error deleting computer:', error); + throw error; + } + } +} + +// Computer list class +class Computers extends Array { + + async getAll() { + try { + const rows = await database.execute('SELECT * FROM computers'); + this.length = 0; // Clear the existing array + rows.forEach(row => { + const computer = new Computer(row.id, row.brand, row.model, row.state, row.status); + this.push(computer); + }); + } catch (error) { + console.error('Error getting computers:', error); + throw error; + } + } + + async create(brand, model, state, status) { + try { + const computer = new Computer(null, brand, model, state, status); + await computer.create(); + this.push(computer); + } catch (error) { + console.error('Error creating computer:', error); + throw error; + } + } + + async delete(id) { + try { + const computerIndex = this.findIndex(computer => computer.id === id); + if (computerIndex === -1) { + throw new Error('Computer not found'); + } + const computer = this[computerIndex]; + await computer.delete(); + this.splice(computerIndex, 1); + } catch (error) { + console.error('Error deleting computer:', error); + throw error; + } + } +} + +export { Computer, Computers }; diff --git a/Classes/Database.js b/Classes/Database.js new file mode 100644 index 0000000..8b32a9f --- /dev/null +++ b/Classes/Database.js @@ -0,0 +1,52 @@ + +const mysql = require('mysql2/promise'); + +class Database { + constructor(config) { + this.pool = mysql.createPool(config); + } + + async query(sql, params) { + const connection = await this.pool.getConnection(); + try { + const [rows] = await connection.query(sql, params); + return rows; + } finally { + connection.release(); + } + } + + async execute(sql, params) { + const connection = await this.pool.getConnection(); + try { + const [result] = await connection.execute(sql, params); + return result; + } finally { + connection.release(); + } + } + + async transaction(callback) { + const connection = await this.pool.getConnection(); + try { + await connection.beginTransaction(); + const result = await callback(connection); + await connection.commit(); + return result; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + } +} + +const database = new Database({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME +}); + +export { Database, database }; \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c96932c --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# inventory + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.js +``` + +This project was created using `bun init` in bun v1.0.13. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/Routes/computer.js b/Routes/computer.js new file mode 100644 index 0000000..03a409c --- /dev/null +++ b/Routes/computer.js @@ -0,0 +1,83 @@ +import express from 'express'; +import { Computer, Computers } from '../Classes/Computer'; + +const router = express.Router(); + +// GET all computers +router.get('/', async (req, res) => { + const computers = new Computers(); + await computers.getAll(); + try { + + res.status(200).json(computers); + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +// GET a computer by ID +router.get('/:id', async (req, res) => { + try { + const computer = new Computer(); + await computer.get(req.params.id); + if (computer.brand === null) { + res.status(404).json({ error: 'Computer not found' }); + } else { + res.status(200).json(computer); + } + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +// POST a new computer +router.post('/', async (req, res) => { + try { + const { brand, model, state, status } = req.body; + if (!brand || !model || !state || !status) { + res.status(400).json({ error: 'Bad Request' }); + } + const computer = new Computer(null, brand, model, state, status); + await computer.create(); + res.status(201).json({ message: 'Computer created successfully' }); + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +// PUT (update) a computer by ID +router.put('/:id', async (req, res) => { + try { + const computer = new Computer(); + await computer.get(req.params.id); + if (computer.brand === null) { + res.status(404).json({ error: 'Computer not found' }); + } + const { brand, model, state, status } = req.body; + computer.brand = brand; + computer.model = model; + computer.state = state; + computer.status = status; + await computer.update(); + res.status(200).json({ message: 'Computer updated successfully' }); + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +// DELETE a computer by ID +router.delete('/:id', async (req, res) => { + try { + const computer = new Computer(); + await computer.get(req.params.id); + if (computer.brand === null) { + res.status(404).json({ error: 'Computer not found' }); + } + await computer.delete(); + res.status(200).json({ message: 'Computer deleted successfully' }); + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +export default router; \ No newline at end of file diff --git a/Routes/user.js b/Routes/user.js new file mode 100644 index 0000000..e2d9391 --- /dev/null +++ b/Routes/user.js @@ -0,0 +1,67 @@ +import express from 'express'; +import { database } from '../Classes/Database'; +import jwt from 'jsonwebtoken'; + +const router = express.Router(); + +// POST to login using username and password +router.post('/login', async (req, res) => { + try { + const { username, password } = req.body; + if (!username || !password) { + res.status(400).json({ error: 'Bad Request' }); + } + const rows = await database.execute('SELECT * FROM users WHERE username = ?', [username]); + if (rows.length === 0) { + res.status(401).json({ error: 'Unauthorized' }); + } else { + const isPasswordValid = await Bun.password.verify(password, rows[0].password); + if (!isPasswordValid) { + res.status(401).json({ error: 'Unauthorized' }); + } else { + const token = jwt.sign({ username, password }, process.env.SECRET); + res.status(200).json({ token }); + } + } + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +// POST to check if token is valid +router.post('/verify', async (req, res) => { + try { + const token = req.body.token; + if (!token) { + res.status(401).json({ error: 'Unauthorized' }); + } else { + jwt.verify(token, process.env.SECRET, async (err, decoded) => { + if (err) res.status(401).json({ error: 'Unauthorized' }); + const rows = await database.execute('SELECT * FROM users WHERE username = ?', [decoded.username]); + if (rows.length === 0) res.status(401).json({ error: 'Unauthorized' }); + const isPasswordValid = await Bun.password.verify(decoded.password, rows[0].password); + if (!isPasswordValid) res.status(401).json({ error: 'Unauthorized' }); + res.status(200).json({ message: 'Authorized' }); + }); + } + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +// POST to register a new user +router.post('/register', async (req, res) => { + try { + const { username, password } = req.body; + if (!username || !password) { + res.status(400).json({ error: 'Bad Request' }); + } + const hashedPassword = await Bun.password.hash(password); + await database.execute('INSERT INTO users (username, password) VALUES (?, ?)', [username, hashedPassword]); + res.status(201).json({ message: 'User created successfully' }); + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +export default router; \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..ab6d2e9 Binary files /dev/null and b/bun.lockb differ diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..d303611 --- /dev/null +++ b/database.sql @@ -0,0 +1,33 @@ +SET default_storage_engine = InnoDB; +DROP DATABASE IF EXISTS `inventory`; +CREATE DATABASE IF NOT EXISTS `inventory` + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +DROP USER IF EXISTS 'inventory'; +CREATE USER 'inventory'@'localhost' IDENTIFIED BY 'irCd3CH9MELH7Pa9KEHWxoz4S987iGWCP+JQZe4w+3s='; +GRANT ALL PRIVILEGES ON inventory.* TO 'inventory'@'localhost'; + +USE `inventory`; + +CREATE TABLE IF NOT EXISTS computers ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + brand VARCHAR(255) NOT NULL, + model VARCHAR(255) NOT NULL, + status VARCHAR(255) NOT NULL, + state VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB; + +INSERT INTO computers (brand, model, status, state) VALUES + ('Dell', 'Latitude 7400', 'Available', 'RAS'), + ('Dell', 'Latitude 7400', 'Available', 'RAS'), + ('Dell', 'Latitude 7400', 'Available', 'RAS'); + + +CREATE TABLE IF NOT EXISTS users ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB; \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..2fea2cb --- /dev/null +++ b/index.js @@ -0,0 +1,40 @@ +import express from 'express'; +import morgan from 'morgan'; +import computerRouter from './Routes/computer.js'; +import userRouter from './Routes/user.js'; +import jwt from 'jsonwebtoken'; + +const app = express(); +app.use(express.json()); +app.use(morgan('dev')); +app.use(express.static('public')); + +const authMiddleware = async (req, res, next) => { + const token = req.headers.authorization; + if (!token) { + return res.status(401).json({ message: 'No auth token provided' }); + } + + try { + const decoded = jwt.verify(token, process.env.SECRET); + if (!decoded) { + return res.status(401).json({ message: 'Invalid auth token' }); + } + const rows = await database.execute('SELECT * FROM users WHERE username = ?', [decoded.username]); + if (rows.length === 0) res.status(401).json({ error: 'Unauthorized' }); + const isPasswordValid = await Bun.password.verify(decoded.password, rows[0].password); + if (!isPasswordValid) res.status(401).json({ error: 'Unauthorized' }); + next(); + } catch (error) { + return res.status(401).json({ message: 'Invalid auth token' }); + } +}; + +computerRouter.use(authMiddleware); +app.use('/computer', computerRouter); +app.use('/user', userRouter); + +// Start the server +app.listen(3000, () => { + console.log('Server is running on port 3000'); +}); \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..7556e1d --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" // add Bun global + ] + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c5ac28b --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "inventory", + "module": "index.js", + "type": "module", + "devDependencies": { + "bun-types": "latest", + "eslint": "^8.54.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "mysql2": "^3.6.3" + } +} \ No newline at end of file diff --git a/public/c/index.html b/public/c/index.html new file mode 100644 index 0000000..1da48e5 --- /dev/null +++ b/public/c/index.html @@ -0,0 +1,354 @@ + + + + Computer Inventory + + + +

Computer Inventory

+
+
+ + + + + + + + + +
IDBrandModelStatusStateActions
+
+
+ + + + + +
+
+ + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..88fc70e --- /dev/null +++ b/public/index.html @@ -0,0 +1,170 @@ + + + + + + Login Page + + + +
+
+

Login

+ +

+ +

+ +
+ +
+ + + +