Commit 532a5ce1 authored by Dennis Willers's avatar Dennis Willers
Browse files

Merge branch 'dev-greep' into 'master'

Merge new backend API to Master

See merge request Dennis.Willers/voting!1
parents d4df8b91 ad4e12d3
Loading
Loading
Loading
Loading

.eslintrc.json

0 → 100644
+14 −0
Original line number Diff line number Diff line
{
    "env": {
        "browser": true,
        "commonjs": true,
        "es2021": true,
        "node": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaVersion": 12
    },
    "rules": {
    }
}

.vscode/launch.json

0 → 100644
+17 −0
Original line number Diff line number Diff line
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-node",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}\\index.js"
        }
    ]
}
 No newline at end of file
+1 −1
Original line number Diff line number Diff line
FROM node:12.18.1
FROM node:lts
ENV NODE_ENV=production
EXPOSE 8080
EXPOSE 3000
+74 −26
Original line number Diff line number Diff line
@@ -9,45 +9,93 @@ The client can connect to the socket server via `willers.digital` on port `3201`
If you just want to call the REST-Server you can do it with: `https://tm-voting.willers.digital`.

## REST Services
There are currently only two REST interfaces.
**All parameters can be done in the url query `/getVotes?map=blah` or in the Body you sent in a JSON format `{"map":"blah"}`**

**POST /getMapInfo**<br>
**GET /getVotes**<br>
Returns the current voting status for the current map.<br>
Example Request:
```

| Parameter 	| Type      | Description                         	| Example                     
|------------	|-------    |-------------------------------------	|-----------------------------
| map        	| String    | The map UID to show the votes on it 	| `OkNwgzSwDLZSWSRPsUKY7EA1Cg5`

Example response:
```json
{
    "mapId": "currentMapUId",
    "userId": "UserLogin",
    "name": "mapname for example: Summer 2021 - 01"
    "mapUid": "OkNwgzSwDLZSWSRPsUKY7EA1Cg5",
    "votes": 53,
    "average": 85,
    "lastVoteDate": "2021-08-23T00:28:16.273Z"
}
```
Info: I heard that the name is not really relevant, because the API of trackmania.io already provides information about a map. Nevertheless, in the current implementation this field should at least not be null.

**POST /setVote**<br>
Writes the vote of a player to the respective map into the database table.
Example Request:
```
**GET /getPlayerVote**<br>
Returns the current voting status of a player for the current map.<br>

| Parameter 	| Type      | Description                         	| Example                     
|------------	|-------    |-------------------------------------	|-----------------------------
| map        	| String    | The map UID to show the votes on it 	| `OkNwgzSwDLZSWSRPsUKY7EA1Cg5`
| player       	| String    | The player login that he voted on it 	| `Jtmn3kBnSSadky_mLNhp_A`

Example response:
```json
{
    "mapId": "currentMapUId",
    "userId": "UserLogin",
    "name": "mapname for example: Summer 2021 - 01",
    "vote": 40
    "mapUid": "OkNwgzSwDLZSWSRPsUKY7EA1Cg5",
    "playerLogin": "Jtmn3kBnSSadky_mLNhp_A",
    "vote": 50,
    "date": "2021-08-23 04:55:49.105"
}
```
**POST RESPONSE**<br>
The response is the same in both cases:
```

**GET /mostVotedMaps**<br>
Returns the top most voted maps. in decreasing order (from the most voted map to the least voted map)<br>

There's no parameters for this request!

Example response:
```json
{
    "votes": 3,
    "average": 63,
    "vote": 80
    "OkNwgzSwDLZSWSRPsUKY7EA1Cg5": 65,
    "7fsfRSUCQ7YwfBEdRk_GivW6qzj": 56,
    "KOylxZkny8RdOEFhchN1kG6Uoo1": 32,
    "n8KeykdPYEsJM6RPEZcQHVRVRIb": 32,
    "RfGQDoOnJ0FiswOBjpL3V7nkF0a": 32,
    "Iwo4gO_0dQ3FVQ1xeYjho5ZmLrf": 1,
}
```
If other players only listen to the current voting status on the map, the vote for the respective client is not returned, since this is client-specific and therefore not interesting for everyone. So for them the response will be like:

**GET /mostPlayerVoted**<br>
Returns the top of the players who have voted the most maps. in decreasing order (from the most player who voted to the least)<br>

There's no parameters for this request!

Example response:
```json
{
    "Jtmn3kBnSSadky_mLNhp_A": 10,
    "W01C9MLeQH2zZ8v_P-gXvA": 8,
    "TIA7WqNETVyjWNj3RV1shQ": 5,
    "c5jutptORLinoaIUmVWscA": 2,
    "xTn1Lty3Tki8v7w3FBqVIA": 1
}
```

**POST /setVote**<br>
Writes the vote of a player to the respective map into the database table.<br />

| Parameter 	| Type      | Description                         	            | Example                     
|------------	|-------    |-------------------------------------	            |-----------------------------
| map        	| String    | The map UID to vote 	                            | `OkNwgzSwDLZSWSRPsUKY7EA1Cg5`
| player       	| String    | The player login that it will vote 	            | `Jtmn3kBnSSadky_mLNhp_A`
| vote       	| Number    | A number between 0 and 100 to define the vote 	| `56`

Example Response:
```json
{
    "votes": 3,
    "average": 63
    "mapUid": "OkNwgzSwDLZSWSRPsUKY7EA1Cg5",
    "player": "Jtmn3kBnSSadky_mLNhp_A",
    "vote": 65,
    "date": "2021-08-23T03:32:18.301Z",
    "type": "update" // "new" if it's a new vote, "update" if the player already voted on this map before
}
```

@@ -82,4 +130,4 @@ The following star scale was defined for the voting plugin:

## Attention

My backend is really not secure for SQL-Injection and some other secure stuff. If some more people want to use it, I should protect it more.. but that take some time. You are welcome to help me with this. That would be a nice thing if we can introduce a global map vote functionality in a secure and highly available way. :)
 No newline at end of file
My backend is 100% not secure for SQL-Injection and some other secure stuff. If some more people want to use it, I should protect it more.. but that take some time. You are welcome to help me with this. That would be a nice thing if we can introduce a global map vote functionality in a secure and highly available way. :)
 No newline at end of file

events.js

deleted100644 → 0
+0 −209
Original line number Diff line number Diff line
const express = require('express');

function createRouter(db) {
    const router = express.Router();
    
    function isMapIdAndNameNotNull(req) {
        const mapId = req.body.mapId;
        const name = req.body.name;
        return mapId != null && name != null;
    }

    function isVoteRequestValid(req) {
        const userId = req.body.userId;
        const vote = req.body.vote;
        return userId != null && vote != null && isMapIdAndNameNotNull(req);
    }

    function isgetMapInfoValid(req) {
        const userId = req.body.userId;
        return userId != null && isMapIdAndNameNotNull(req);
    }
    
    const hasMap = function hasMap(req, res, next) {
        const mapId = req.body.mapId;
        if (isMapIdAndNameNotNull(req)) {
            const sql = 'SELECT count(*) as Maps From Map WHERE mapId = \"'+mapId+'\";';
            console.log('Map Select: ', sql);
            db.query(
                sql,
                (error, results) => {
                if (error) {
                    console.log(error);
                    res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'error in hasMap'});
                } else {
                    numberResults = results[0].Maps;
                    if (numberResults === 1) next();
                    else if (numberResults === 0) addMap(req, res, next);
                    else res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'There are multiple maps with the same id'});
                }
            });
        } else {
            res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'JSON-Parameter are not valid'});
        }
    };

    const addMap = function addMap(req, res, next) {
        const mapId = req.body.mapId;
        const name = req.body.name;
        const sql = 'INSERT INTO Map (mapId, name) VALUES (\"'+mapId+'\", \"'+name+'\");';
        console.log('Map Insert: ', sql);
        db.query(
            sql,
            (error, results) => {
            if (error) {
                console.log(error);
                res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'error in addMap'});
            } else {
                if (results.affectedRows === 1) next();
                else res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'error in insertMap'});
            }
        });
    };

    const hasVote = function hasVote(req, res, next) {
        if (isVoteRequestValid(req)) {
            const userId = req.body.userId;
            const mapId = req.body.mapId;
            const sql = 'SELECT userId FROM Vote WHERE userId = \"'+ userId + '\" AND mapId = \"' + mapId + '\";';
            console.log('Map Insert: ', sql);
            db.query(
                sql,
                (error, results) => {
                if (error) {
                    console.log(error);
                    res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'error in hasVote'});
                } else {
                    if (results.length === 0) insertVote(req, res, next);
                    else if (results.length === 1) updateVote(req, res, next);
                    else res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'too many votes by user'});
                }
            });
        } else {
            res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'Vote Request is not valid'});
        }
    };

    const insertVote = function insertVote(req, res, next) {
        if (isVoteRequestValid(req)) {
            const userId = req.body.userId;
            const mapId = req.body.mapId;
            const vote = req.body.vote;
            const sql = 'INSERT INTO Vote (userId, mapId, vote) VALUES (\"'+ userId + '\", \"'+ mapId + '\", \"' + vote +'\");';
            console.log('Insert Vote: ', sql);
            db.query(
                sql,
                (error, results) => {
                if (error) {
                    console.log(error);
                    res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'error in insertVote'});
                } else {
                    if (results.affectedRows === 1) next();
                    else res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'error in insertVote'});
                }
            });
        } else {
            res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'Vote Request is not valid'});
        }
    };

    const updateVote = function updateVote(req, res, next) {
        if (isVoteRequestValid(req)) {
            const userId = req.body.userId;
            const mapId = req.body.mapId;
            const vote = req.body.vote;
            const sql = 'UPDATE Vote SET userId = \"'+ userId + '\", mapId = \"'+ mapId + '\", vote = \"' + vote +'\" WHERE userID = \"'+ userId +'\" AND mapId = \"' + mapId + '\";';
            console.log('Update Vote: ', sql);
            db.query(
                sql,
                (error, results) => {
                if (error) {
                    console.log(error);
                    res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'error in updateVote'});
                } else {
                    if (results.affectedRows === 1) next();
                    else res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'error in updateVote'});
                }
            });
        } else {
            res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'Vote Request is not valid'});
        }
    };

    const getMapInfo = function getMapInfo(req, res) {
        if (isVoteRequestValid(req)) {
            const mapId = req.body.mapId;
            const vote = req.body.vote;
            const sql = 'SELECT count(userId) as votes, round(avg(vote)) as average FROM Vote WHERE mapId LIKE \"'+ mapId + '\";';
            console.log('getMapInfo: ', sql);
            db.query(
                sql,
                (error, results) => {
                if (error) {
                    console.log(error);
                    res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'error in getMapInfo'});
                } else {
                    if (results.length === 1) {
                        let average = results[0].average;
                        let votes = results[0].votes;
                        if (!average) average = 0;
                        if (!votes) votes = 0;
                        res.header('Access-Control-Allow-Origin', "*").status(200).json({votes: votes, average: average, vote: vote});
                        return;
                    }
                    res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'unexpected result in getMapInfo'});
                }
            });
        } else {
            res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'Vote Request is not valid'});
        }
    };

    const getVote = function getVote(req, res) {
        if (isgetMapInfoValid(req)) {
            const mapId = req.body.mapId;
            const userId = req.body.userId;
            const sql = 'SELECT vote FROM Vote WHERE mapId LIKE \"'+ mapId + '\" AND userId LIKE \"' + userId + '\";';
            console.log('Update Vote: ', sql);
            db.query(
                sql,
                (error, results) => {
                    if (error) {
                        console.log(error);
                        res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'error in getVote'});
                    } else {
                        if (results.length < 2) {
                            let vote = -1;
                            if (results.length === 1) {
                                vote = results[0].vote;
                            }
                            if (!isNaN(vote)) {
                                req.body.vote = vote;
                                getMapInfo(req, res);
                                return;
                            }
                            res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'unexpected result in getVote'});
                        } else {
                            res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'too many results in getVote'});
                        }
                    }
                });
        } else {
            res.header('Access-Control-Allow-Origin', "*").status(500).json({status: 'Vote Request is not valid'});
        }
    };

    const test = function test(req, res) {
        res.header('Access-Control-Allow-Origin', "*").status(200).json({status: "Test valid!"});
    };

    // the routes are defined here

    router.get('/test', [test]);
    router.post('/setVote', [hasMap, hasVote, getMapInfo]);
    router.post('/getMapInfo', [hasMap, getVote]);

    return router;
}

module.exports = createRouter;
Loading