refactored backend structure and added login

This commit is contained in:
Sascha Kühl 2025-03-09 22:27:57 +01:00
parent be7a51f621
commit 3cbb467457
38 changed files with 995 additions and 400 deletions

View File

@ -5,89 +5,89 @@ export const pitchers: Map<number, Pitcher> = new Map([
1, 1,
{ {
id: 1, id: 1,
first_name: "Nolan", firstName: "Nolan",
last_name: "Ryan", lastName: "Ryan",
date_of_birth: new Date("1947-01-31") dateOfBirth: new Date("1947-01-31")
} }
], ],
[ [
2, 2,
{ {
id: 2, id: 2,
first_name: "Sandy", firstName: "Sandy",
last_name: "Koufax", lastName: "Koufax",
date_of_birth: new Date("1935-12-30") dateOfBirth: new Date("1935-12-30")
} }
], ],
[ [
3, 3,
{ {
id: 3, id: 3,
first_name: "Pedro", firstName: "Pedro",
last_name: "Martinez", lastName: "Martinez",
date_of_birth: new Date("1971-10-25") dateOfBirth: new Date("1971-10-25")
} }
], ],
[ [
4, 4,
{ {
id: 4, id: 4,
first_name: "Randy", firstName: "Randy",
last_name: "Johnson", lastName: "Johnson",
date_of_birth: new Date("1963-09-10") dateOfBirth: new Date("1963-09-10")
} }
], ],
[ [
5, 5,
{ {
id: 5, id: 5,
first_name: "Greg", firstName: "Greg",
last_name: "Maddux", lastName: "Maddux",
date_of_birth: new Date("1966-04-14") dateOfBirth: new Date("1966-04-14")
} }
], ],
[ [
6, 6,
{ {
id: 6, id: 6,
first_name: "Bob", firstName: "Bob",
last_name: "Gibson", lastName: "Gibson",
date_of_birth: new Date("1935-11-09") dateOfBirth: new Date("1935-11-09")
} }
], ],
[ [
7, 7,
{ {
id: 7, id: 7,
first_name: "Tom", firstName: "Tom",
last_name: "Seaver", lastName: "Seaver",
date_of_birth: new Date("1944-11-17") dateOfBirth: new Date("1944-11-17")
} }
], ],
[ [
8, 8,
{ {
id: 8, id: 8,
first_name: "Roger", firstName: "Roger",
last_name: "Clemens", lastName: "Clemens",
date_of_birth: new Date("1962-08-04") dateOfBirth: new Date("1962-08-04")
} }
], ],
[ [
9, 9,
{ {
id: 9, id: 9,
first_name: "Walter", firstName: "Walter",
last_name: "Johnson", lastName: "Johnson",
date_of_birth: new Date("1887-11-06") dateOfBirth: new Date("1887-11-06")
} }
], ],
[ [
10, 10,
{ {
id: 10, id: 10,
first_name: "Clayton", firstName: "Clayton",
last_name: "Kershaw", lastName: "Kershaw",
date_of_birth: new Date("1988-03-19") dateOfBirth: new Date("1988-03-19")
} }
]]); ]]);

View File

@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from '@ionic/vue-router'; import { createRouter, createWebHistory } from '@ionic/vue-router';
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import PitcherList from '../views/PitcherList.vue' import PitcherList from '../views/PitcherList.vue'
import Login from '../views/Login.vue'
import PreparePitch from "@/views/PreparePitch.vue"; import PreparePitch from "@/views/PreparePitch.vue";
import FinalizePitch from "@/views/FinalizePitch.vue"; import FinalizePitch from "@/views/FinalizePitch.vue";
import BullpenStats from "@/views/BullpenStats.vue"; import BullpenStats from "@/views/BullpenStats.vue";
@ -8,7 +9,11 @@ import BullpenStats from "@/views/BullpenStats.vue";
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: '/', path: '/',
redirect: '/pitchers' redirect: '/login'
}, {
path: '/login',
name: 'Login',
component: Login
}, { }, {
path: '/pitchers', path: '/pitchers',
name: 'Pitchers', name: 'Pitchers',

View File

@ -6,9 +6,13 @@ export class BullpenSessionService {
private static instance: BullpenSessionService; private static instance: BullpenSessionService;
private static nullPitcher: Pitcher = { private static nullPitcher: Pitcher = {
id: 0, id: 0,
first_name: "", firstName: "",
last_name: "", lastName: "",
date_of_birth: new Date(0) email: "",
password: "",
dateOfBirth: new Date(0),
createdAt: new Date(0),
updatedAt: new Date(0),
}; };
private static nullPitchType: PitchType = { private static nullPitchType: PitchType = {
id: 0, id: 0,

View File

@ -1,5 +1,12 @@
import {pitchTypes} from "@/data/pitchTypes";
import PitchType from "@/types/PitchType"; import PitchType from "@/types/PitchType";
import axios, {AxiosInstance} from "axios";
const apiClient: AxiosInstance = axios.create({
baseURL: "http://localhost:8080/api",
headers: {
"Content-type": "application/json",
},
});
class PitchTypeService { class PitchTypeService {
private static instance: PitchTypeService; private static instance: PitchTypeService;
@ -15,16 +22,18 @@ class PitchTypeService {
} }
async getAllPitchTypes(): Promise<PitchType[]> { async getAllPitchTypes(): Promise<PitchType[]> {
return pitchTypes.values().toArray(); const users = await apiClient.get('/pitch_types');
return users.data;
} }
async getPitchType(id: number): Promise<PitchType> { async getPitchType(id: number): Promise<PitchType> {
const pitcher = pitchTypes.get(id); const pitchType = await apiClient.get(`/pitch_types/${id}`);
if (pitcher !== undefined) { return pitchType.data;
return pitcher; // if (pitcher !== undefined) {
} else { // return pitcher;
return Promise.reject(); // } else {
} // return Promise.reject();
// }
} }
} }

View File

@ -1,5 +1,4 @@
import Pitcher from "@/types/Pitcher"; import Pitcher from "@/types/Pitcher";
import {pitchers} from "@/data/pitchers";
import axios, { AxiosInstance } from "axios"; import axios, { AxiosInstance } from "axios";
const apiClient: AxiosInstance = axios.create({ const apiClient: AxiosInstance = axios.create({
@ -22,12 +21,13 @@ class PitcherService {
} }
async getAllPitchers(): Promise<Pitcher[]> { async getAllPitchers(): Promise<Pitcher[]> {
return apiClient.get('/pitchers') const users = await apiClient.get('/users');
// return pitchers.values().toArray(); return users.data;
} }
async getPitcher(id: number): Promise<Pitcher> { async getPitcher(id: number): Promise<Pitcher> {
return apiClient.get(`/pitchers/${id}`) const pitcher = await apiClient.get(`/users/${id}`);
return pitcher.data;
// const pitcher = pitchers.get(id); // const pitcher = pitchers.get(id);
// if (pitcher !== undefined) { // if (pitcher !== undefined) {
// return pitcher; // return pitcher;

View File

@ -1,6 +1,10 @@
export default interface Pitcher { export default interface Pitcher {
id: number, id: number,
first_name: string, firstName: string,
last_name: string, lastName: string,
date_of_birth: Date, dateOfBirth: Date,
email: string,
createdAt: Date,
updatedAt: Date,
password: string
} }

133
app/src/views/Login.vue Normal file
View File

@ -0,0 +1,133 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="login-logo">
<img src="./../../public/favicon.png" alt="Ionic logo" />
</div>
<form novalidate @submit.prevent="onLogin">
<ion-list>
<ion-item>
<ion-input
label="Username"
labelPlacement="stacked"
v-model="username"
name="username"
type="text"
:spellcheck="false"
autocapitalize="off"
required
></ion-input>
</ion-item>
<ion-item>
<ion-input
labelPlacement="stacked"
label="Password"
v-model="password"
name="password"
type="password"
required
></ion-input>
</ion-item>
</ion-list>
<ion-row responsive-sm class="ion-padding">
<ion-col>
<ion-button :disabled="!canSubmit" type="submit" expand="block"
>Login</ion-button
>
</ion-col>
<ion-col>
<ion-button
:disabled="!canSubmit"
@click="onSignup"
color="light"
expand="block"
>Signup</ion-button
>
</ion-col>
</ion-row>
</form>
<ion-toast
:is-open="showToast"
:message="toastMessage"
:duration="2000"
></ion-toast>
</ion-content>
</ion-page>
</template>
<style scoped>
.login-logo {
padding: 20px 0;
min-height: 200px;
text-align: center;
}
.login-logo img {
max-width: 150px;
}
.list {
margin-bottom: 0;
}
</style>
<script setup lang="ts">
import { computed, ref } from "vue";
import {
IonPage,
IonHeader,
IonToolbar,
IonButtons,
IonMenuButton,
IonButton,
IonContent,
IonList,
IonItem,
IonTitle,
IonRow,
IonCol,
IonInput,
IonToast,
} from "@ionic/vue";
const username = ref("");
const password = ref("");
const submitted = ref(false);
const usernameValid = true;
const passwordValid = true;
const showToast = ref(false);
const toastMessage = ref("");
const canSubmit = computed(
() => username.value.trim() !== "" && password.value.trim() !== ""
);
const onLogin = () => {
submitted.value = true;
if (usernameValid && passwordValid) {
}
};
const onSignup = () => {
toastMessage.value = "Successfully logged in!";
showToast.value = true;
username.value = "";
password.value = "";
};
</script>

View File

@ -15,7 +15,7 @@
<ion-list> <ion-list>
<ion-item v-for="(pitcher, index) in pitchers" :key="index" button @click="goToPreparePitch(pitcher)"> <ion-item v-for="(pitcher, index) in pitchers" :key="index" button @click="goToPreparePitch(pitcher)">
<ion-label>{{ pitcher.first_name }} {{ pitcher.last_name }}</ion-label> <ion-label>{{ pitcher.firstName }} {{ pitcher.lastName }}</ion-label>
</ion-item> </ion-item>
</ion-list> </ion-list>
</ion-content> </ion-content>

View File

@ -0,0 +1,3 @@
module.exports = {
secret: "bullpen-secret-key"
};

View File

@ -1,23 +0,0 @@
{
"development": {
"username": "test",
"password": "test123!",
"database": "bullpen",
"host": "127.0.0.1",
"dialect": "postgres"
},
"test": {
"username": "test",
"password": "test123!",
"database": "bullpen",
"host": "127.0.0.1",
"dialect": "postgres"
},
"production": {
"username": "test",
"password": "test123!",
"database": "bullpen",
"host": "127.0.0.1",
"dialect": "postgres"
}
}

View File

@ -0,0 +1,13 @@
module.exports = {
HOST: "localhost",
USER: "postgres",
PASSWORD: "123",
DB: "testdb",
dialect: "postgres",
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
};

View File

@ -0,0 +1,6 @@
module.exports = {
dialect: 'sqlite',
storage: 'database/example-db.sqlite',
logQueryParameters: true,
benchmark: true
}

View File

@ -0,0 +1,93 @@
const db = require("../models");
const config = require("../config/auth.config");
const User = db.user;
const Role = db.role;
const Op = db.Sequelize.Op;
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
exports.signup = (req, res) => {
// Save User to Database
User.create({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
dateOfBirth: req.body.dateOfBirth,
password: bcrypt.hashSync(req.body.password, 8)
})
.then(user => {
if (req.body.roles) {
Role.findAll({
where: {
name: {
[Op.or]: req.body.roles
}
}
}).then(roles => {
user.setRoles(roles).then(() => {
res.send({ message: "User registered successfully!" });
});
});
} else {
// user role = 1
user.setRoles([1]).then(() => {
res.send({ message: "User registered successfully!" });
});
}
})
.catch(err => {
res.status(500).send({ message: err.message });
});
};
exports.signin = (req, res) => {
User.findOne({
where: {
username: req.body.username
}
})
.then(user => {
if (!user) {
return res.status(404).send({ message: "User Not found." });
}
const passwordIsValid = bcrypt.compareSync(
req.body.password,
user.password
);
if (!passwordIsValid) {
return res.status(401).send({
accessToken: null,
message: "Invalid Password!"
});
}
const token = jwt.sign({ id: user.id },
config.secret,
{
algorithm: 'HS256',
allowInsecureKeySizes: true,
expiresIn: 86400, // 24 hours
});
const authorities = [];
user.getRoles().then(roles => {
for (let i = 0; i < roles.length; i++) {
authorities.push("ROLE_" + roles[i].name.toUpperCase());
}
res.status(200).send({
id: user.id,
username: user.username,
email: user.email,
roles: authorities,
accessToken: token
});
});
})
.catch(err => {
res.status(500).send({ message: err.message });
});
};

View File

@ -0,0 +1,36 @@
const db = require("../models");
const PitchType = db.pitchType;
const Op = db.Sequelize.Op;
exports.findAll = (req, res) => {
PitchType.findAll()
.then(data => {
res.send(data);
})
.catch(err => {
res.status(500).send({
message:
err.message || "Some error occurred while retrieving pitch types."
});
});
};
exports.findOne = (req, res) => {
const id = req.params.id;
PitchType.findByPk(id)
.then(data => {
if (data) {
res.send(data);
} else {
res.status(404).send({
message: `Cannot find pitch type with id=${id}.`
});
}
})
.catch(err => {
res.status(500).send({
message: "Error retrieving pitch type with id=" + id
});
});
};

View File

@ -0,0 +1,55 @@
const db = require("../models");
const User = db.user;
const Op = db.Sequelize.Op;
exports.findAll = (req, res) => {
const title = req.query.title;
const condition = title ? {title: {[Op.iLike]: `%${title}%`}} : null;
User.findAll({ where: condition })
.then(data => {
res.send(data);
})
.catch(err => {
res.status(500).send({
message:
err.message || "Some error occurred while retrieving users."
});
});
};
exports.findOne = (req, res) => {
const id = req.params.id;
User.findByPk(id)
.then(data => {
if (data) {
res.send(data);
} else {
res.status(404).send({
message: `Cannot find user with id=${id}.`
});
}
})
.catch(err => {
res.status(500).send({
message: "Error retrieving user with id=" + id
});
});
};
// exports.allAccess = (req, res) => {
// res.status(200).send("Public Content.");
// };
//
// exports.userBoard = (req, res) => {
// res.status(200).send("User Content.");
// };
//
// exports.adminBoard = (req, res) => {
// res.status(200).send("Admin Content.");
// };
//
// exports.moderatorBoard = (req, res) => {
// res.status(200).send("Moderator Content.");
// };

Binary file not shown.

View File

@ -1,27 +1,66 @@
const app = require('./services/app'); const express = require("express");
const sequelize = require('./sequelize'); const cors = require("cors");
const PORT = 8080;
async function assertDatabaseConnectionOk() { const app = express();
console.log(`Checking database connection...`);
try {
await sequelize.authenticate();
console.log('Database connection OK!');
} catch (error) {
console.log('Unable to connect to the database:');
console.log(error.message);
process.exit(1);
}
}
async function init() { const corsOptions = {
await assertDatabaseConnectionOk(); origin: "http://localhost:8080"
};
console.log(`Starting Sequelize + Express example on port ${PORT}...`); app.use(cors(corsOptions));
// parse requests of content-type - application/json
app.use(express.json());
// parse requests of content-type - application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));
app.listen(PORT, () => { // database
console.log(`Express server started on port ${PORT}. Try some routes, such as '/api/users'.`); const db = require("./models");
const Role = db.role;
const User = db.user;
const PitchType = db.pitchType;
// db.sequelize.sync();
// force: true will drop the table if it already exists
db.sequelize.sync({force: true}).then(() => {
console.log('Drop and Re-sync Database with { force: true }');
initial();
}); });
}
init().then(r => console.log('finished')); // simple route
app.get("/", (req, res) => {
res.json({ message: "Welcome to bullpen application." });
});
// routes
require('./routes/auth.routes')(app);
require('./routes/user.routes')(app);
require('./routes/pitchType.routes')(app);
// set port, listen for requests
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}.`);
});
function initial() {
Role.bulkCreate([
{ name: 'user' },
{ name: 'moderator' },
{ name: 'administrato' },
]);
User.bulkCreate([
{ firstName: 'Nolan', lastName: 'Ryan', dateOfBirth: new Date(1947, 1, 31), email: 'ryan.nolan@bullpen.com', password: 'nolan' },
{ firstName: 'Sandy', lastName: 'Koufax', dateOfBirth: new Date(1935, 12, 30), email: 'sandy.koufax@bullpen.com', password: 'sandy' },
{ firstName: 'Pedro', lastName: 'Martinez', dateOfBirth: new Date(1971, 10, 25), email: 'pedro.martinez@bullpen.com', password: 'pedro' },
{ firstName: 'randy', lastName: 'johnson', dateOfBirth: new Date(1963, 9, 10), email: 'randy.johnson@bullpen.com', password: 'randy' },
]);
PitchType.bulkCreate([
{ name: 'Fastball', abbreviation: 'FB' },
{ name: 'Curveball', abbreviation: 'CB' },
{ name: 'Slider', abbreviation: 'SL' },
{ name: 'Changeup', abbreviation: 'CH' },
{ name: 'Cutter', abbreviation: 'CUT' },
{ name: 'Sweeper', abbreviation: 'SW' },
{ name: 'Slurve', abbreviation: 'SLV' },
]);
}

View File

@ -0,0 +1,90 @@
const jwt = require("jsonwebtoken");
const config = require("../config/auth.config.js");
const db = require("../models");
const User = db.user;
verifyToken = (req, res, next) => {
let token = req.headers["x-access-token"];
if (!token) {
return res.status(403).send({
message: "No token provided!"
});
}
jwt.verify(token,
config.secret,
(err, decoded) => {
if (err) {
return res.status(401).send({
message: "Unauthorized!",
});
}
req.userId = decoded.id;
next();
});
};
isAdmin = (req, res, next) => {
User.findByPk(req.userId).then(user => {
user.getRoles().then(roles => {
for (let i = 0; i < roles.length; i++) {
if (roles[i].name === "admin") {
next();
return;
}
}
res.status(403).send({
message: "Require Admin Role!"
});
});
});
};
isModerator = (req, res, next) => {
User.findByPk(req.userId).then(user => {
user.getRoles().then(roles => {
for (let i = 0; i < roles.length; i++) {
if (roles[i].name === "moderator") {
next();
return;
}
}
res.status(403).send({
message: "Require Moderator Role!"
});
});
});
};
isModeratorOrAdmin = (req, res, next) => {
User.findByPk(req.userId).then(user => {
user.getRoles().then(roles => {
for (let i = 0; i < roles.length; i++) {
if (roles[i].name === "moderator") {
next();
return;
}
if (roles[i].name === "admin") {
next();
return;
}
}
res.status(403).send({
message: "Require Moderator or Admin Role!"
});
});
});
};
const authJwt = {
verifyToken: verifyToken,
isAdmin: isAdmin,
isModerator: isModerator,
isModeratorOrAdmin: isModeratorOrAdmin
};
module.exports = authJwt;

View File

@ -0,0 +1,7 @@
const authJwt = require("./authJwt");
const verifySignUp = require("./verifySignUp");
module.exports = {
authJwt,
verifySignUp
};

View File

@ -0,0 +1,43 @@
const db = require("../models");
const ROLES = db.ROLES;
const User = db.user;
checkDuplicateUsernameOrEmail = (req, res, next) => {
// Username
User.findOne({
where: {
email: req.body.email
}
}).then(user => {
if (user) {
res.status(400).send({
message: "Failed! Email is already in use!"
});
return;
}
next();
});
};
checkRolesExisted = (req, res, next) => {
if (req.body.roles) {
for (let i = 0; i < req.body.roles.length; i++) {
if (!ROLES.includes(req.body.roles[i])) {
res.status(400).send({
message: "Failed! Role does not exist = " + req.body.roles[i]
});
return;
}
}
}
next();
};
const verifySignUp = {
checkDuplicateUsernameOrEmail: checkDuplicateUsernameOrEmail,
checkRolesExisted: checkRolesExisted
};
module.exports = verifySignUp;

View File

@ -1,21 +1,21 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
sequelize.define('bullpenSession', { const BullpenSession = sequelize.define('bullpenSession', {
id: { id: {
allowNull: false, allowNull: false,
autoIncrement: true, autoIncrement: true,
primaryKey: true, primaryKey: true,
type: DataTypes.INTEGER type: DataTypes.INTEGER
}, },
pitcher: { // pitcher: {
type: DataTypes.INTEGER, // type: DataTypes.INTEGER,
allowNull: false, // allowNull: false,
references: { // User belongsTo User 1:1 // references: {
model: 'User', // model: 'User',
key: 'id' // key: 'id'
} // }
}, // },
aimedArea: { aimedArea: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false allowNull: false
@ -28,5 +28,7 @@ module.exports = (sequelize) => {
sequelize, sequelize,
modelName: 'BullpenSession', modelName: 'BullpenSession',
tableName: 'BullpenSessions', tableName: 'BullpenSessions',
}) });
return BullpenSession;
}; };

54
backend/models/index.js Normal file
View File

@ -0,0 +1,54 @@
const config = require("../config/sqlite.config");
const Sequelize = require("sequelize");
const sequelize = new Sequelize({
dialect: config.dialect,
storage: config.storage,
logQueryParameters: config.logQueryParameters,
benchmark: config.benchmark
});
// const sequelize = new Sequelize(
// config.DB,
// config.USER,
// config.PASSWORD,
// {
// host: config.HOST,
// dialect: config.dialect,
// pool: {
// max: config.pool.max,
// min: config.pool.min,
// acquire: config.pool.acquire,
// idle: config.pool.idle
// }
// }
// );
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
db.user = require("../models/user.model.js")(sequelize);
db.role = require("../models/role.model.js")(sequelize);
db.pitchType = require("../models/pitchType.model.js")(sequelize);
db.pitch = require("../models/pitch.model.js")(sequelize);
db.bullpenSession = require("../models/bullpenSession.model.js")(sequelize);
db.role.belongsToMany(db.user, {
through: "UserRoles"
});
db.user.belongsToMany(db.role, {
through: "UserRoles"
});
db.ROLES = ["user", "admin", "moderator"];
db.bullpenSession.hasMany(db.pitch);
db.bullpenSession.belongsTo(db.user, {
as: "pitcher"
});
db.pitch.belongsTo(db.bullpenSession);
db.pitch.belongsTo(db.pitchType);
module.exports = db;

View File

@ -1,7 +1,7 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
sequelize.define('pitch', { const Pitch = sequelize.define('pitch', {
id: { id: {
allowNull: false, allowNull: false,
autoIncrement: true, autoIncrement: true,
@ -36,5 +36,7 @@ module.exports = (sequelize) => {
sequelize, sequelize,
modelName: 'Pitch', modelName: 'Pitch',
tableName: 'Pitches', tableName: 'Pitches',
}) });
return Pitch;
}; };

View File

@ -1,7 +1,7 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
sequelize.define('pitchType', { const PitchType = sequelize.define('pitchType', {
id: { id: {
allowNull: false, allowNull: false,
autoIncrement: true, autoIncrement: true,
@ -21,5 +21,7 @@ module.exports = (sequelize) => {
sequelize, sequelize,
modelName: 'PitchType', modelName: 'PitchType',
tableName: 'PitchTypes' tableName: 'PitchTypes'
}) });
return PitchType;
}; };

View File

@ -0,0 +1,22 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Role = sequelize.define("role", {
id: {
allowNull: false,
autoIncrement: true,
type: DataTypes.INTEGER,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false,
}
}, {
sequelize,
modelName: 'Role',
tableName: "Roles",
});
return Role;
};

View File

@ -0,0 +1,55 @@
const bcrypt = require('bcryptjs');
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const User = sequelize.define("user", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER
},
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING,
allowNull: false
},
dateOfBirth: {
type: DataTypes.DATE,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
// We require usernames to have length of at least 3, and
// only use letters, numbers and underscores.
is: /^\w{3,}$/
}
},
password: {
type: DataTypes.STRING,
allowNull: false
}
}, {
sequelize,
modelName: 'User',
tableName: "Users",
hooks: {
beforeCreate: async (user) => {
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(user.password, salt);
}
}
});
User.prototype.validPassword = async function (password) {
return bcrypt.compare(password, this.password);
};
return User;
};

View File

@ -9,8 +9,12 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"bcryptjs": "^3.0.2",
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"cors": "^2.8.5",
"express": "^4.21.2", "express": "^4.21.2",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"pg": "^8.13.3", "pg": "^8.13.3",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
@ -286,6 +290,15 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/bcryptjs": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
"integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/bindings": { "node_modules/bindings": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
@ -386,6 +399,12 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -754,6 +773,19 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -886,6 +918,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/editorconfig": { "node_modules/editorconfig": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
@ -1169,6 +1210,19 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-validator": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz",
"integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21",
"validator": "~13.12.0"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/express/node_modules/debug": { "node_modules/express/node_modules/debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -1835,12 +1889,97 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "10.4.3", "version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@ -2420,6 +2559,15 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0" "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
} }
}, },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",

View File

@ -14,8 +14,12 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"bcryptjs": "^3.0.2",
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"cors": "^2.8.5",
"express": "^4.21.2", "express": "^4.21.2",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"pg": "^8.13.3", "pg": "^8.13.3",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",

View File

@ -0,0 +1,23 @@
const { verifySignUp } = require("../middleware");
const controller = require("../controllers/auth.controller");
module.exports = function(app) {
app.use(function(req, res, next) {
res.header(
"Access-Control-Allow-Headers",
"x-access-token, Origin, Content-Type, Accept"
);
next();
});
app.post(
"/api/auth/signup",
[
verifySignUp.checkDuplicateUsernameOrEmail,
verifySignUp.checkRolesExisted
],
controller.signup
);
app.post("/api/auth/signin", controller.signin);
};

View File

@ -0,0 +1,15 @@
const { authJwt } = require("../middleware");
const controller = require("../controllers/pitchType.controller");
module.exports = function(app) {
app.use(function(req, res, next) {
res.header(
"Access-Control-Allow-Headers",
"x-access-token, Origin, Content-Type, Accept"
);
next();
});
app.get("/api/pitch_types", controller.findAll);
app.get("/api/pitch_types/:id", controller.findOne);
};

View File

@ -0,0 +1,39 @@
const { authJwt } = require("../middleware");
const controller = require("../controllers/user.controller");
module.exports = function(app) {
app.use(function(req, res, next) {
res.header(
"Access-Control-Allow-Headers",
"x-access-token, Origin, Content-Type, Accept"
);
next();
});
app.get(
"/api/users",
[authJwt.verifyToken, authJwt.isAdmin],
controller.findAll);
app.get(
"/api/users/:id",
[authJwt.verifyToken],
controller.findOne);
// app.get(
// "/api/test/user",
// [authJwt.verifyToken],
// controller.userBoard
// );
//
// app.get(
// "/api/test/mod",
// [authJwt.verifyToken, authJwt.isModerator],
// controller.moderatorBoard
// );
//
// app.get(
// "/api/test/admin",
// [authJwt.verifyToken, authJwt.isAdmin],
// controller.adminBoard
// );
};

View File

@ -1,10 +0,0 @@
function applyExtraSetup(sequelize) {
const { user, pitch, pitchType, bullpenSession } = sequelize.models;
bullpenSession.hasMany(pitch);
bullpenSession.belongsTo(user);
pitch.belongsTo(bullpenSession);
pitchType.belongsTo(pitchType);
}
module.exports = { applyExtraSetup };

View File

@ -1,30 +0,0 @@
const { Sequelize } = require('sequelize');
const { applyExtraSetup } = require('./extra-setup');
// In a real app, you should keep the database connection URL as an environment variable.
// But for this example, we will just use a local SQLite database.
// const sequelize = new Sequelize(process.env.DB_CONNECTION_URL);
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'database/example-db.sqlite',
logQueryParameters: true,
benchmark: true
});
const modelDefiners = [
require('./models/user.model'),
require('./models/pitchType.model'),
require('./models/pitch.model'),
require('./models/bullpenSession.model'),
];
// We define all models according to their files.
for (const modelDefiner of modelDefiners) {
modelDefiner(sequelize);
}
// We execute any extra setup after the models are defined, such as adding associations.
applyExtraSetup(sequelize);
// We export the sequelize connection instance to be used around our app.
module.exports = sequelize;

View File

@ -1,46 +0,0 @@
const { DataTypes } = require('sequelize');
// We export a function that defines the model.
// This function will automatically receive as parameter the Sequelize connection object.
module.exports = (sequelize) => {
sequelize.define('user', {
// The following specification of the 'id' attribute could be omitted
// since it is the default.
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER
},
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING,
allowNull: false
},
dateOfBirth: {
type: DataTypes.DATE,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
// We require usernames to have length of at least 3, and
// only use letters, numbers and underscores.
is: /^\w{3,}$/
}
},
password: {
type: DataTypes.STRING,
allowNull: false
}
}, {
sequelize,
modelName: 'User',
tableName: "Users",
});
};

View File

@ -1,71 +0,0 @@
const express = require('express');
const bodyParser = require('body-parser');
const routes = {
users: require('./routes/users'),
pitch_types: require('./routes/pitchTypes'),
// Add more routes here...
// items: require('./routes/items'),
};
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// We create a wrapper to workaround async errors not being transmitted correctly.
function makeHandlerAwareOfAsyncErrors(handler) {
return async function(req, res, next) {
try {
await handler(req, res);
} catch (error) {
next(error);
}
};
}
// We provide a root route just as an example
app.get('/', (req, res) => {
res.send(`
<h2>Hello, Sequelize + Express!</h2>
<p>Make sure you have executed <b>npm run setup-example-db</b> once to have a populated example database. Otherwise, you will get <i>'no such table'</i> errors.</p>
<p>Try some routes, such as <a href='/api/users'>/api/users</a> or <a href='/api/orchestras?includeInstruments'>/api/orchestras?includeInstruments</a>!</p>
<p>To experiment with POST/PUT/DELETE requests, use a tool for creating HTTP requests such as <a href='https://github.com/jakubroztocil/httpie#readme'>HTTPie</a>, <a href='https://www.postman.com/downloads/'>Postman</a>, or even <a href='https://en.wikipedia.org/wiki/CURL'>the curl command</a>, or write some JS code for it with <a href='https://github.com/sindresorhus/got#readme'>got</a>, <a href='https://github.com/sindresorhus/ky#readme'>ky</a> or <a href='https://github.com/axios/axios#readme'>axios</a>.</p>
`);
});
// We define the standard REST APIs for each route (if they exist).
for (const [routeName, routeController] of Object.entries(routes)) {
if (routeController.getAll) {
app.get(
`/api/${routeName}`,
makeHandlerAwareOfAsyncErrors(routeController.getAll)
);
}
if (routeController.getById) {
app.get(
`/api/${routeName}/:id`,
makeHandlerAwareOfAsyncErrors(routeController.getById)
);
}
if (routeController.create) {
app.post(
`/api/${routeName}`,
makeHandlerAwareOfAsyncErrors(routeController.create)
);
}
if (routeController.update) {
app.put(
`/api/${routeName}/:id`,
makeHandlerAwareOfAsyncErrors(routeController.update)
);
}
if (routeController.remove) {
app.delete(
`/api/${routeName}/:id`,
makeHandlerAwareOfAsyncErrors(routeController.remove)
);
}
}
module.exports = app;

View File

@ -1,11 +0,0 @@
// A helper function to assert the request ID param is valid
// and convert it to a number (since it comes as a string by default)
function getIdParam(req) {
const id = req.params.id;
if (/^\d+$/.test(id)) {
return Number.parseInt(id, 10);
}
throw new TypeError(`Invalid ':id' param: "${id}"`);
}
module.exports = { getIdParam };

View File

@ -1,60 +0,0 @@
const { models } = require('../../sequelize');
const { getIdParam } = require('../helpers');
async function getAll(req, res) {
const pitchType = await models.pitchType.findAll();
res.status(200).json(pitchType);
}
async function getById(req, res) {
const id = getIdParam(req);
const pitchType = await models.pitchType.findByPk(id);
if (pitchType) {
res.status(200).json(pitchType);
} else {
res.status(404).send('404 - Not found');
}
}
async function create(req, res) {
if (req.body.id) {
res.status(400).send(`Bad request: ID should not be provided, since it is determined automatically by the database.`)
} else {
await models.pitchType.create(req.body);
res.status(201).end();
}
}
async function update(req, res) {
const id = getIdParam(req);
// We only accept an UPDATE request if the `:id` param matches the body `id`
if (req.body.id === id) {
await models.pitchType.update(req.body, {
where: {
id: id
}
});
res.status(200).end();
} else {
res.status(400).send(`Bad request: param ID (${id}) does not match body ID (${req.body.id}).`);
}
}
async function remove(req, res) {
const id = getIdParam(req);
await models.pitchType.destroy({
where: {
id: id
}
});
res.status(200).end();
}
module.exports = {
getAll,
getById,
create,
update,
remove,
};

View File

@ -1,60 +0,0 @@
const { models } = require('../../sequelize');
const { getIdParam } = require('../helpers');
async function getAll(req, res) {
const users = await models.user.findAll();
res.status(200).json(users);
}
async function getById(req, res) {
const id = getIdParam(req);
const user = await models.user.findByPk(id);
if (user) {
res.status(200).json(user);
} else {
res.status(404).send('404 - Not found');
}
}
async function create(req, res) {
if (req.body.id) {
res.status(400).send(`Bad request: ID should not be provided, since it is determined automatically by the database.`)
} else {
await models.user.create(req.body);
res.status(201).end();
}
}
async function update(req, res) {
const id = getIdParam(req);
// We only accept an UPDATE request if the `:id` param matches the body `id`
if (req.body.id === id) {
await models.user.update(req.body, {
where: {
id: id
}
});
res.status(200).end();
} else {
res.status(400).send(`Bad request: param ID (${id}) does not match body ID (${req.body.id}).`);
}
}
async function remove(req, res) {
const id = getIdParam(req);
await models.user.destroy({
where: {
id: id
}
});
res.status(200).end();
}
module.exports = {
getAll,
getById,
create,
update,
remove,
};