diff --git a/api.MD b/api.MD index 89733c3..3520775 100644 --- a/api.MD +++ b/api.MD @@ -1,6 +1,8 @@ //location //get all curl localhost/api/v0/location +curl localhost/api/v0/location -H "Authorization: ${1a3eabe1-e1b2-46df-b846-585540c68368}" + //get id curl http://localhost/api/v0/location/3 @@ -158,4 +160,14 @@ curl 'http://localhost/api/v0/sensor-data/data?year=2023&month=1&week=1&day=1&se http://localhost/api/v0/sensor-data/filter?windspeed=highest&limit=1 //pagination -http://localhost/api/v0/sensor-data/data?week=1&sensorid=1&locationid=1&page=2&pagesize=10 \ No newline at end of file +http://localhost/api/v0/sensor-data/data?week=1&sensorid=1&locationid=1&page=2&pagesize=10 + + + + + +curl localhost/api/v0/user/new -H "Content-Type: application/json" -X POST -d '{"username": "testuser", "password": "thisisthesystemuserpasswordnoob", "email": "testuser@ecosaver.com", "address": "Nanyang Polytechnic 180 Ang Mo Kio Avenue 8 Singapore 569830", "phone": "12345678"}' + + + +curl localhost/api/v0/apikey/new -H "Content-Type: application/json" -X POST -d '{"userid": 1, "permission": "canWrite"' diff --git a/consumerWebsite/database/model/apiKeyModel.js b/consumerWebsite/database/model/apiKeyModel.js index e9a2df4..b481881 100644 --- a/consumerWebsite/database/model/apiKeyModel.js +++ b/consumerWebsite/database/model/apiKeyModel.js @@ -5,8 +5,8 @@ const { userModel } = require("./userModel"); sequelize.sync(); const apikeyModel = sequelize.define( - "apikey", - { + "apikey", + { id: { type: DataTypes.INTEGER, allowNull: true, @@ -16,39 +16,39 @@ const apikeyModel = sequelize.define( isNumeric: true, }, }, - userid:{ - type: DataTypes.INTEGER, - allowNull: false, - validate: { - isNumeric: true, - }, - //fk - references: { - model: userModel, - key: "id", - }, - }, - apikey: { - type: DataTypes.STRING, - allowNull: false, - length: 255, - unique: true, - validate: { - notEmpty: true, - len: [1, 255], - }, - }, - permission: { - type: DataTypes.STRING, - allowNull: false, - length: 255, - validate: { - notEmpty: true, - len: [1, 255], - isIn: [['canRead' , 'canWrite']], - }, - }, - createdAt: { + userid: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + isNumeric: true, + }, + //fk + references: { + model: userModel, + key: "id", + }, + }, + apikey: { + type: DataTypes.STRING, + allowNull: false, + length: 255, + unique: true, + validate: { + notEmpty: true, + len: [1, 255], + }, + }, + permission: { + type: DataTypes.STRING, + allowNull: false, + length: 255, + validate: { + notEmpty: true, + len: [1, 255], + isIn: [["canRead", "canWrite"]], + }, + }, + createdAt: { type: DataTypes.DATE, allowNull: true, }, @@ -56,11 +56,10 @@ const apikeyModel = sequelize.define( type: DataTypes.DATE, allowNull: true, }, - }, - { - timestamps: true, - } - -) + }, + { + timestamps: true, + } +); module.exports = { apikeyModel }; diff --git a/consumerWebsite/functions/apiDatabase.js b/consumerWebsite/functions/apiDatabase.js new file mode 100644 index 0000000..859d110 --- /dev/null +++ b/consumerWebsite/functions/apiDatabase.js @@ -0,0 +1,30 @@ +const { sequelize } = require("../database/mySql.js"); +const { apikeyModel } = require("../database/model/apikeyModel.js"); +const { userModel } = require("../database/model/userModel.js"); +const { Op, Sequelize } = require("sequelize"); + +async function getUser() { + const user = await userModel.findAll(); + return user; +} + +async function addUser(user) { + //console.log(user); + await userModel.create(user); +} + +async function getAPIKey() { + const apikey = await apikeyModel.findAll(); + return apikey; +} + +async function addAPIKey(apikey) { + await apikeyModel.create(apikey); +} + +module.exports = { + getUser, + addUser, + getAPIKey, + addAPIKey, +}; diff --git a/consumerWebsite/functions/bcrypt.js b/consumerWebsite/functions/bcrypt.js new file mode 100644 index 0000000..a303cae --- /dev/null +++ b/consumerWebsite/functions/bcrypt.js @@ -0,0 +1,37 @@ +const bcrypt = require('bcrypt'); +const saltRounds = 10; +//https://github.com/kelektiv/node.bcrypt.js#readme + + +/* +// Load hash from your password DB. +bcrypt.compare(myPlaintextPassword, hash, function(err, result) { + // result == true +}); +bcrypt.compare(someOtherPlaintextPassword, hash, function(err, result) { + // result == false +}); +*/ + + +/* +//hash with salt +bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) { + // Store hash in your password DB. +}); + +*/ +async function hashPassword(password) { + return await bcrypt.hash(password, saltRounds); +} + +async function hashAPIKey(apikey) { + return await bcrypt.hash(apikey, saltRounds); +} + + + +module.exports = { + hashPassword, + hashAPIKey, +}; \ No newline at end of file diff --git a/consumerWebsite/functions/generateUUID.js b/consumerWebsite/functions/generateUUID.js new file mode 100644 index 0000000..ee5bc8a --- /dev/null +++ b/consumerWebsite/functions/generateUUID.js @@ -0,0 +1,17 @@ +/* +const crypto = require('crypto'); +Calling the UUID method returns a UUID of standard length that you can use in your program. + +let uuid = crypto.randomUUID(); +console.log(uuid); + +*/ +const crypto = require('crypto'); + + +async function generateUUID() { + let uuid = crypto.randomUUID(); + return uuid; +} + +module.exports = { generateUUID }; \ No newline at end of file diff --git a/consumerWebsite/modules/app.js b/consumerWebsite/modules/app.js index fe0da09..0ae4bbf 100644 --- a/consumerWebsite/modules/app.js +++ b/consumerWebsite/modules/app.js @@ -12,16 +12,11 @@ app.disable("x-powered-by"); app.use(express.json()); app.set("json spaces", 2); - //middleware logic ( called by next() ) - -//app.use('/api/v0', require('../middleware/ApiKey.js')); -a//pp.use('/api/v0', APIlogger, require('../routes/api_route.js')); +//app.use('/api/v0', APIlogger, require('../routes/api_route.js')); //route logic -//app.use("/api/v0", require("../routes/api_route.js")); - - +app.use("/api/v0", require("../routes/api_routes")); //consumerWebsite\routes\api_routes.js // Catch 404 and forward to error handler. If none of the above routes are // used, this is what will be called. diff --git a/consumerWebsite/routes/api_routes.js b/consumerWebsite/routes/api_routes.js new file mode 100644 index 0000000..4d110fe --- /dev/null +++ b/consumerWebsite/routes/api_routes.js @@ -0,0 +1,10 @@ +'use strict'; +const router = require('express').Router(); + +router.use('/user', require('./user')); + +router.use('/apikey', require('./apikey')); + +module.exports = router; + + diff --git a/consumerWebsite/routes/apikey.js b/consumerWebsite/routes/apikey.js new file mode 100644 index 0000000..48df481 --- /dev/null +++ b/consumerWebsite/routes/apikey.js @@ -0,0 +1,69 @@ +const { getAPIKey , addAPIKey } = require("../functions/apiDatabase.js"); +const { hashAPIKey } = require("../functions/bcrypt.js"); +const { generateUUID } = require("../functions/generateUUID.js"); + +const express = require("express"); +const router = express.Router(); + +router.get("/", async (req, res, next) => { + try { + const location = await getAPIKey(); + res.status(200).json(location); + } catch (error) { + console.error(error); + next(error); + } +}); + +/* +1) ensure user is logged in (frontend session validation blah or wtv) +2) when user click on generate api key button, it will generate a random api key +3) hash the api key +4) store the api key in database +*/ +router.post("/new", async (req, res, next) => { + try { + let uuid = await generateUUID() + //attach uuid to req.body + req.body.apikey = uuid + //hash apikey + req.body.apikey = await hashAPIKey(req.body.apikey) + + await addAPIKey(req.body); + res.sendStatus(200); + + } catch (error) { + console.error(error); + next(error); + } +}); + +//update +//delete +//getbyid + +module.exports = router; + + +/* +async function addAPIKey(userId) { + let apikey = await generateUUID() + apikey = await hashAPIKey(req.body.apikey) + let token = await apikeyModel.create({apikey, userId}); + return `${token.id}-${apikey}` +} + + +router.post("/new", async (req, res, next) => { + try { + let apikey = await addAPIKey(req.body.userid) + res.json({apiKey: apikey}) + + } catch (error) { + console.error(error); + next(error); + } +}); + + +*/ \ No newline at end of file diff --git a/consumerWebsite/routes/user.js b/consumerWebsite/routes/user.js new file mode 100644 index 0000000..639fe14 --- /dev/null +++ b/consumerWebsite/routes/user.js @@ -0,0 +1,42 @@ +const { getUser, addUser } = require("../functions/apiDatabase.js"); +const { hashPassword } = require("../functions/bcrypt.js"); + +const express = require("express"); +const router = express.Router(); + +router.get("/", async (req, res, next) => { + try { + const location = await getUser(); + res.status(200).json(location); + } catch (error) { + console.error(error); + next(error); + } +}); + +/* +1) req.body is taken from html form or wtv +2) bcrpyt and hash the password on the server side +3) pass to db +*/ +router.post("/new", async (req, res, next) => { + try { + //pass pass to hashPassword + let hash = await hashPassword(req.body.password); + //add hash back to req.body + req.body.password = hash; + await addUser(req.body); + res.sendStatus(200); + } catch (error) { + console.error(error); + next(error); + } +}); + + +//login +//update +//delete +//getbyid + +module.exports = router; diff --git a/webserver/database/model/apiKeyModel.js b/webserver/database/model/apiKeyModel.js new file mode 100644 index 0000000..03bcb95 --- /dev/null +++ b/webserver/database/model/apiKeyModel.js @@ -0,0 +1,65 @@ +"use strict"; +const { Sequelize, DataTypes } = require("sequelize"); +const { sequelize } = require("../mySQL"); +const { userModel } = require("./userModel"); + +//sequelize.sync(); +const apikeyModel = sequelize.define( + "apikey", + { + id: { + type: DataTypes.INTEGER, + allowNull: true, + primaryKey: true, + autoIncrement: true, + validate: { + isNumeric: true, + }, + }, + userid: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + isNumeric: true, + }, + //fk + references: { + model: userModel, + key: "id", + }, + }, + apikey: { + type: DataTypes.STRING, + allowNull: false, + length: 255, + unique: true, + validate: { + notEmpty: true, + len: [1, 255], + }, + }, + permission: { + type: DataTypes.STRING, + allowNull: false, + length: 255, + validate: { + notEmpty: true, + len: [1, 255], + isIn: [["canRead", "canWrite"]], + }, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: true, + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: true, + }, + }, + { + timestamps: true, + } +); + +module.exports = { apikeyModel }; diff --git a/webserver/database/model/sensorDataModel.js b/webserver/database/model/sensorDataModel.js index 843119e..0f0c5be 100644 --- a/webserver/database/model/sensorDataModel.js +++ b/webserver/database/model/sensorDataModel.js @@ -5,7 +5,7 @@ const { locationModel } = require("./locationModel"); const { sensorModel } = require("./sensorModel"); const { isJson } = require('../../functions/validateData'); -sequelize.sync(); +//sequelize.sync(); const sensorDataModel = sequelize.define( "sensorData", { diff --git a/webserver/database/model/userModel.js b/webserver/database/model/userModel.js new file mode 100644 index 0000000..561890c --- /dev/null +++ b/webserver/database/model/userModel.js @@ -0,0 +1,94 @@ +"use strict"; +const { Sequelize, DataTypes } = require("sequelize"); +const { sequelize } = require("../mySQL"); +const { + isAlphaNumericWithSpacesAndDash, + isAddress, +} = require("../../functions/validateData"); + +//sequelize.sync(); +const userModel = sequelize.define( + "user", + { + id: { + type: DataTypes.INTEGER, + allowNull: true, + primaryKey: true, + autoIncrement: true, + validate: { + isNumeric: true, + }, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + length: 60, + validate: { + notEmpty: true, + len: [1, 60], + isAlphaNumericWithSpacesAndDash(value) { + if (!isAlphaNumericWithSpacesAndDash(value)) { + throw new Error("Invalid characters in username"); + } + }, + }, + }, + password: { + type: DataTypes.STRING, + allowNull: false, + length: 255, + validate: { + notEmpty: true, + len: [1, 255], + }, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + length: 60, + unique: true, + validate: { + notEmpty: true, + len: [1, 60], + isEmail: true, + }, + }, + address: { + type: DataTypes.STRING, + allowNull: true, + length: 255, + validate: { + notEmpty: true, + len: [1, 255], + isAddress(value) { + if (!isAddress(value)) { + throw new Error("Invalid address"); + } + }, + }, + }, + phone: { + type: DataTypes.STRING, + allowNull: true, + length: 20, + validate: { + notEmpty: true, + len: [1, 20], + isNumeric: true, + }, + }, + //utc time + createdAt: { + type: DataTypes.DATE, + allowNull: true, + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: true, + }, + }, + { + timestamps: true, + } +); +module.exports = { userModel }; diff --git a/webserver/functions/Database.js b/webserver/functions/Database.js index e02eddd..255c15d 100644 --- a/webserver/functions/Database.js +++ b/webserver/functions/Database.js @@ -1,6 +1,7 @@ const { sequelize } = require("../Database/mySql.js"); const { api_log_Model } = require("../Database/model/apiLogModel.js"); const { sensorDataModel } = require("../Database/model/sensorDataModel.js"); +const { apikeyModel } = require("../Database/model/apiKeyModel.js"); async function insertLogData(log){ try{ @@ -32,10 +33,19 @@ async function insertDatatoDB(data) { catch (error) { console.error(error); } +} + +async function checkAPikey(unverified){ + const apikey = apikeyModel.findOne({ + where: { + apikey: unverified + } + }); + return apikey; } -module.exports = { insertLogData , insertDatatoDB}; +module.exports = { insertLogData , insertDatatoDB , checkAPikey}; diff --git a/webserver/functions/bcrypt.js b/webserver/functions/bcrypt.js new file mode 100644 index 0000000..34afc86 --- /dev/null +++ b/webserver/functions/bcrypt.js @@ -0,0 +1,38 @@ +const bcrypt = require('bcrypt'); +const saltRounds = 10; +//https://github.com/kelektiv/node.bcrypt.js#readme + + +/* +// Load hash from your password DB. +bcrypt.compare(myPlaintextPassword, hash, function(err, result) { + // result == true +}); +bcrypt.compare(someOtherPlaintextPassword, hash, function(err, result) { + // result == false +}); +*/ + + +/* +//hash with salt +bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) { + // Store hash in your password DB. +}); + +*/ + +async function hashAPIKey(apikey) { + return await bcrypt.hash(apikey, saltRounds); +} + +async function compareAPIKey(apikey, hash) { + return await bcrypt.compare(apikey, hash); +} + + + +module.exports = { + hashAPIKey, + compareAPIKey +}; \ No newline at end of file diff --git a/webserver/middleware/apiKey.js b/webserver/middleware/apiKey.js index 695b367..28aca0c 100644 --- a/webserver/middleware/apiKey.js +++ b/webserver/middleware/apiKey.js @@ -1,18 +1,62 @@ - -function apiKeyMiddleware(req, res, next) { - const apiKey = req.headers['x-api-key']; - - if (!apiKey) { - return res.status(401).json({ error: 'API key is missing' }); +const { compareAPIKey } = require('../functions/bcrypt.js'); +const { checkAPikey } = require('../functions/database.js'); +async function apikeyCheck(req, res, next) { + //const authHeader = req.headers.authorization + try{ + let apikey = req.headers.authorization + if(!apikey){ + throw new Error('NotAuthed') + } + else{ + //compare apikey to db + + } + next() + }catch(error){ + next(error); } - //logic to check db? - if (apiKey !== 'YOUR_API_KEY') { - return res.status(403).json({ error: 'Invalid API key' }); - } - - // API key is valid, continue to the next middleware or route handler - next(); } -module.exports = { apiKeyMiddleware } +module.exports = { apikeyCheck }; + +/* +1) take user supplied api key +2) hash and salt +3) compare to stored hash and salt in db +4) if match, check permissions +5) if permissions allow, continue +6) else throw error +*/ +/* + +I plan to seed some data in user and api +Than use the system info and my API middleware will somehow check the supplied API key and check +If it's correct API key and has canWrite perms +I allow it to access put and post + + +async function auth(req, res, next){ + try{ + let token = // get token + + req.token = token + + if(req.method === 'GET' && token.canRead){ + return next() + } + + if(req.method === 'POST' && token.canWrite){ + return next() + } + + throw new Error('NotAuthed') + + }catch(error){ + next(error); + } +} + + + +*/ \ No newline at end of file diff --git a/webserver/modules/express.js b/webserver/modules/express.js index b1db1c2..e929b5a 100644 --- a/webserver/modules/express.js +++ b/webserver/modules/express.js @@ -2,6 +2,7 @@ const express = require("express"); const helmet = require("helmet"); const { rateLimit } = require("express-rate-limit"); const { APIlogger } = require('../middleware/apiLogger.js'); +const { apikeyCheck } = require('../middleware/apiKey.js'); const app = express(); app.use(helmet()); @@ -30,14 +31,10 @@ app.set("json spaces", 2); /* middleware logic ( called by next() ) */ -//app.use('/api/v0', require('../middleware/ApiKey.js')); -app.use('/api/v0', APIlogger, require('../routes/api_route.js')); +app.use("/api/seed/v0", [ apikeyCheck , APIlogger] ,require("../routes/seed_route.js")); +app.use('/api/v0', [apikeyCheck, APIlogger] ,require("../routes/api_route.js")); //webserver\routes\api_route.js -//route logic -app.use("/api/v0", require("../routes/api_route.js")); -//seed logic -app.use("/api/seed/v0", require("../routes/seed_route.js")); // Catch 404 and forward to error handler. If none of the above routes are // used, this is what will be called. diff --git a/webserver/routes/Location.js b/webserver/routes/Location.js index cc6517e..486cff9 100644 --- a/webserver/routes/Location.js +++ b/webserver/routes/Location.js @@ -20,7 +20,7 @@ router.get("/", async (req, res, next) => { next(error); } }); -/* + //add location router.post("/new", async (req, res, next) => { @@ -58,7 +58,7 @@ router.delete("/delete", async (req, res, next) => { } }); -*/ + //get location by id router.get("/:id", async (req, res, next) => { diff --git a/webserver/routes/Sensor.js b/webserver/routes/Sensor.js index 22fb2c8..cc1c076 100644 --- a/webserver/routes/Sensor.js +++ b/webserver/routes/Sensor.js @@ -18,7 +18,7 @@ router.get("/", async (req, res, next) => { next(error); } }); -/* + router.post("/new", async (req, res, next) => { try { @@ -52,7 +52,6 @@ router.delete("/delete", async (req, res, next) => { next(error); } }); -*/ router.get("/:id", async (req, res, next) => { try { const sensor = await getSensorById(req.params.id); diff --git a/webserver/routes/SensorData.js b/webserver/routes/SensorData.js index 9119593..c54717d 100644 --- a/webserver/routes/SensorData.js +++ b/webserver/routes/SensorData.js @@ -21,7 +21,7 @@ router.get("/", async (req, res, next) => { next(error); } }); -/* + router.post("/new", async (req, res, next) => { try { const { id, id_sensor, id_location, sensordata } = req.body; @@ -54,7 +54,6 @@ router.delete("/delete", async (req, res, next) => { next(error); } }); -*/ router.get("/data", async (req, res, next) => { try { console.log(req.query);