frontend and backend progress

This commit is contained in:
Sascha Kühl 2025-04-28 07:43:02 +02:00
parent 7788369b73
commit 888cdae3cd
34 changed files with 279 additions and 210 deletions

View File

@ -11,7 +11,7 @@ const store = useStore<AppStore>();
const logout = () => { const logout = () => {
store.dispatch('auth/logout'); store.dispatch('auth/logout');
router.push('/login'); router.push({path: '/login'});
} }
const isOnline = ref(navigator.onLine); const isOnline = ref(navigator.onLine);
@ -23,7 +23,7 @@ onMounted(() => {
window.addEventListener("offline", () => isOnline.value = false); window.addEventListener("offline", () => isOnline.value = false);
if (isOnline.value) { if (isOnline.value) {
router.push("/login"); router.push({path: '/login'});
} else { } else {
// checkStoredUserData(); // checkStoredUserData();
} }

View File

@ -15,8 +15,9 @@ class EventBus {
document.addEventListener(type, listener as EventListener, options); document.addEventListener(type, listener as EventListener, options);
} }
dispatch<T extends Event>(event: T): void { // dispatch<T extends Event>(event: T): void {
document.dispatchEvent(event); dispatch(event: string, data: any = null): void {
document.dispatchEvent(new CustomEvent(event, { detail: data }));
} }
remove<T extends Event>(type: string, listener: (event: T) => void, options?: boolean | EventListenerOptions): void { remove<T extends Event>(type: string, listener: (event: T) => void, options?: boolean | EventListenerOptions): void {

View File

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

View File

@ -8,6 +8,7 @@ import BullpenView from "@/views/BullpenView.vue";
import BullpenSummaryView from "@/views/BullpenSummaryView.vue"; import BullpenSummaryView from "@/views/BullpenSummaryView.vue";
import SetupView from '@/views/SetupView.vue'; import SetupView from '@/views/SetupView.vue';
import backendService from '@/services/BackendService' import backendService from '@/services/BackendService'
import BullpenListView from "@/views/BullpenListView.vue";
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ path: '/', redirect: '/login' }, { path: '/', redirect: '/login' },
@ -16,6 +17,7 @@ const routes: Array<RouteRecordRaw> = [
{ path: '/home', component: HomeView }, { path: '/home', component: HomeView },
{ path: '/pitchers', component: PitcherList }, { path: '/pitchers', component: PitcherList },
{ path: '/bullpen', component: BullpenView }, { path: '/bullpen', component: BullpenView },
{ path: '/stats', component: BullpenListView },
{ path: '/summary', component: BullpenSummaryView } { path: '/summary', component: BullpenSummaryView }
] ]

View File

@ -1,6 +1,7 @@
import axios from "axios"; import axios from "axios";
import backendService from '@/services/BackendService'
const server = JSON.parse(localStorage.getItem("server") || '""'); const server = backendService.getServer()
const instance = axios.create({ const instance = axios.create({
baseURL: `${server.protocol}://${server.host}:${server.port}/api`, baseURL: `${server.protocol}://${server.host}:${server.port}/api`,

View File

@ -0,0 +1,52 @@
import api from "@/services/Api";
import authHeader from "@/services/AuthHeader";
import {AxiosError, AxiosResponse} from "axios";
import EventBus from "@/common/EventBus";
export class ApiService<Type> {
protected name: string;
constructor(name: string) {
this.name = name;
}
public save(entity: Type): Promise<Type> {
return api
.post(`${this.name}`, entity)
.then(response => {
return response.data;
}, (error) => {
this.handleExpiredToken(error);
return Promise.reject(error);
});
}
public fetchAll(): Promise<Type[]> {
return api
.get(`/${this.name}`, { headers: authHeader() })
.then((response: AxiosResponse<Type[]>) => {
return response.data;
}, (error) => {
this.handleExpiredToken(error);
return Promise.reject(error);
});
}
public fetchById(id: number): Promise<Type> {
return api
.get(`/${this.name}/${id}`, { headers: authHeader() })
.then((response: AxiosResponse<Type>) => {
return response.data;
}, (error: AxiosError) => {
this.handleExpiredToken(error);
return Promise.reject(error);
});
}
protected handleExpiredToken(error: any) {
if (error.response && error.response.status === 403) {
console.log('Refresh token expired. Logout and redirect to login page.');
EventBus.dispatch("logout");
}
}
}

View File

@ -2,7 +2,7 @@ import api from './Api';
import TokenService from './TokenService'; import TokenService from './TokenService';
class AuthService { class AuthService {
public login(email: string, password: string) { public async login(email: string, password: string) {
return api return api
.post('/auth/login', { .post('/auth/login', {
email, email,
@ -17,7 +17,7 @@ class AuthService {
}); });
} }
public logout(): Promise<void> { public async logout(): Promise<void> {
TokenService.removeUser(); TokenService.removeUser();
return Promise.resolve(); return Promise.resolve();

View File

@ -5,7 +5,8 @@ class BackendService {
return localStorage.getItem("server") !== null; return localStorage.getItem("server") !== null;
} }
getServer(): Server { getServer(): Server {
return JSON.parse(localStorage.getItem("server") || '{"protocol":"https","host":"localhost","port":8124}'); return JSON.parse(localStorage.getItem("server") || '{"protocol":"http","host":"localhost","port":8080}');
// return JSON.parse(localStorage.getItem("server") || '{"protocol":"https","host":"bullpen-api.palaeomatiker.home64.de","port":443}');
} }
updateServer(server: Server): void { updateServer(server: Server): void {
localStorage.setItem("server", JSON.stringify(server)); localStorage.setItem("server", JSON.stringify(server));

View File

@ -1,9 +1,15 @@
import Pitch from "@/types/Pitch"; import Pitch from "@/types/Pitch";
import User from "@/types/User"; import User from "@/types/User";
import Bullpen from '@/types/Bullpen'; import Bullpen from '@/types/Bullpen';
import Pageable from "@/types/PagingDecorator";
import api from '@/services/Api'; import api from '@/services/Api';
import {ApiService} from "@/services/ApiService";
export class BullpenSessionService extends ApiService<Bullpen> {
public constructor() {
super('bullpen_session');
}
export class BullpenSessionService {
public create(user: User): Bullpen { public create(user: User): Bullpen {
return { return {
id: null, id: null,
@ -14,16 +20,6 @@ export class BullpenSessionService {
} }
} }
public save(bullpen: Bullpen): Promise<Bullpen> {
return api
.post('/bullpen_session', bullpen)
.then(response => {
return response.data;
}, (error) => {
console.log(JSON.stringify(error, null, 2));
});
}
public createPitch(): Pitch { public createPitch(): Pitch {
return { return {
id: undefined, id: undefined,
@ -33,6 +29,20 @@ export class BullpenSessionService {
hitArea: 0 hitArea: 0
} }
} }
public findByPitcherId(id: number, page: number, size: number): Promise<Pageable<Bullpen>> {
return api
.get('/bullpen_session', { params: {
user: id,
page: page,
size: size
}}).then(response => {
return response.data;
}, (error) => {
this.handleExpiredToken(error);
return Promise.reject(error);
});
}
} }
export default new BullpenSessionService(); export default new BullpenSessionService();

View File

@ -1,14 +1,18 @@
import PitchType from "@/types/PitchType"; import PitchType from "@/types/PitchType";
import { ApiService } from './ApiService';
class PitchTypeService { class PitchTypeService extends ApiService<PitchType> {
getLocalPitchTypes(): PitchType[] { constructor() {
super('pitch_types');
}
public getLocalPitchTypes(): PitchType[] {
return JSON.parse(localStorage.getItem("pitchTypes") || '""'); return JSON.parse(localStorage.getItem("pitchTypes") || '""');
} }
updateLocalPitchTypes(pitchTypes: PitchType[]): void { public updateLocalPitchTypes(pitchTypes: PitchType[]): void {
localStorage.setItem("pitchTypes", JSON.stringify(pitchTypes)); localStorage.setItem("pitchTypes", JSON.stringify(pitchTypes));
} }
} }
export default new PitchTypeService(); export default new PitchTypeService();

View File

@ -1,13 +1,9 @@
import User from "@/types/User"; import User from "@/types/User";
import api from './Api'; import { ApiService } from './ApiService';
class UserService { class UserService extends ApiService<User> {
async getAllUsers(): Promise<User[]> { constructor() {
return (await api.get('/users')).data; super('users');
}
async getUser(id: number): Promise<User> {
return (await api.get(`/users/${id}`)).data;
} }
} }

View File

@ -27,7 +27,7 @@ const auth: Module<AuthState, RootState> = {
login({ commit }: AuthActionContext, user) { login({ commit }: AuthActionContext, user) {
return AuthService.login(user.email, user.password).then( return AuthService.login(user.email, user.password).then(
auth => { auth => {
return UserService.getUser(auth.id).then( return UserService.fetchById(auth.id).then(
(user: User) => { (user: User) => {
commit('loginSuccess', user); commit('loginSuccess', user);
return Promise.resolve(user); return Promise.resolve(user);
@ -62,6 +62,9 @@ const auth: Module<AuthState, RootState> = {
return Promise.reject(error); return Promise.reject(error);
} }
); );
},
refreshToken({ commit }: AuthActionContext, accessToken) {
commit('refreshToken', accessToken);
} }
}, },
mutations: { mutations: {
@ -83,6 +86,10 @@ const auth: Module<AuthState, RootState> = {
}, },
registerFailure(state) { registerFailure(state) {
state.isAuthenticated = false; state.isAuthenticated = false;
},
refreshToken(state, accessToken: string) {
state.isAuthenticated = true;
TokenService.updateLocalAccessToken(accessToken);
} }
}, },
getters: { getters: {

View File

@ -4,25 +4,26 @@ import Bullpen from '@/types/Bullpen';
import User from '@/types/User'; import User from '@/types/User';
import BullpenSessionService from '@/services/BullpenSessionService'; import BullpenSessionService from '@/services/BullpenSessionService';
import Pitch from '@/types/Pitch'; import Pitch from '@/types/Pitch';
import Pageable from "@/types/PagingDecorator";
export interface BullpenState { export interface BullpenState {
bullpen: Bullpen | null; bullpen: Bullpen | null;
bullpens: Pageable<Bullpen> | null;
} }
type BullpenActionContext = ActionContext<BullpenState, RootState>; type BullpenActionContext = ActionContext<BullpenState, RootState>;
const bullpen: Module<BullpenState, RootState> = { const bullpen: Module<BullpenState, RootState> = {
namespaced: true, namespaced: true,
state: { bullpen: null }, state: {
bullpen: null,
bullpens: null
},
actions: { actions: {
start({commit}: BullpenActionContext, user: User) { start({commit}: BullpenActionContext, user: User) {
const bullpen = BullpenSessionService.create(user); const bullpen = BullpenSessionService.create(user);
commit('start', bullpen); commit('start', bullpen);
}, },
async finish({commit}: BullpenActionContext, bullpen: Bullpen) {
await BullpenSessionService.save(bullpen);
commit('finish');
}
}, },
mutations: { mutations: {
start(state, bullpen: Bullpen) { start(state, bullpen: Bullpen) {
@ -34,6 +35,9 @@ const bullpen: Module<BullpenState, RootState> = {
}, },
finish(state) { finish(state) {
state.bullpen = null; state.bullpen = null;
},
fetch(state: BullpenState, bullpens: Pageable<Bullpen>) {
state.bullpens = bullpens;
} }
} }
}; };

View File

@ -1,36 +1,18 @@
import pitchTypeService from '@/services/PitchTypeService' import pitchTypeService from '@/services/PitchTypeService'
import PitchType from "@/types/PitchType"; import PitchType from "@/types/PitchType";
import api from "@/services/Api";
import authHeader from '@/services/AuthHeader';
import { Module, ActionContext } from 'vuex'; import { Module } from 'vuex';
import { RootState } from './index'; import { RootState } from './index';
import {AxiosResponse} from 'axios';
export interface PitchTypeState { export interface PitchTypeState {
pitchTypes: PitchType[]; pitchTypes: PitchType[];
} }
type PitchTypeActionContext = ActionContext<PitchTypeState, RootState>;
const pitchTypes: Module<PitchTypeState, RootState> = { const pitchTypes: Module<PitchTypeState, RootState> = {
namespaced: true, namespaced: true,
state: {pitchTypes: []}, state: {pitchTypes: []},
actions: {
initialize({ commit }: PitchTypeActionContext) {
api.get('/pitch_types', { headers: authHeader() }).then(
(response: AxiosResponse) => {
commit('initializedPitchTypes', response.data);
return Promise.resolve(response.data);
},
error => {
return Promise.reject(error);
});
}
},
mutations: { mutations: {
initializedPitchTypes(state, pitchTypeList: PitchType[]) { initialize(state, pitchTypeList: PitchType[]) {
pitchTypeService.updateLocalPitchTypes(pitchTypeList); pitchTypeService.updateLocalPitchTypes(pitchTypeList);
state.pitchTypes = pitchTypeList; state.pitchTypes = pitchTypeList;
} }

View File

@ -0,0 +1,6 @@
export default interface Pageable<Type> {
totalCount: number,
totalPages: number,
currentPage: number,
data: Type[]
}

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import {
IonButton,
IonCard,
IonContent,
IonFooter,
IonHeader,
IonLabel,
IonPage,
IonTitle,
IonToolbar
} from "@ionic/vue";
import {useStore} from 'vuex';
import {useRouter} from 'vue-router';
import {computed} from "vue";
const router = useRouter();
const store = useStore();
const isAuthenticated = computed(() => store.state.auth.isAuthenticated);
const pitcher = computed(() => store.state.auth.user);
const bullpens = computed(() => store.state.bullpen.bullpens);
const gotoHome = () => {
router.push({path: '/home'});
}
</script>
<template>
<ion-page>
<ion-header :translucent="true">
<ion-toolbar>
<ion-title v-if="isAuthenticated">Bullpens of {{ pitcher.firstName }} {{ pitcher.lastName }}</ion-title>
<ion-title v-else>Bullpens</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-card v-for="(bullpen, index) in bullpens.data" :key="index">
<ion-label>Bullpen {{bullpen.id}} ({{bullpen.startedAt}})</ion-label>
</ion-card>
</ion-content>
<ion-footer>
<ion-button expand="full" color="success" @click="gotoHome">Home</ion-button>
</ion-footer>
</ion-page>
</template>
<style scoped>
</style>

View File

@ -20,6 +20,7 @@ import PitchType from "@/types/PitchType";
import {useRouter} from 'vue-router'; import {useRouter} from 'vue-router';
import {computed} from 'vue'; import {computed} from 'vue';
import {useStore} from 'vuex'; import {useStore} from 'vuex';
import BullpenSessionService from "@/services/BullpenSessionService";
const router = useRouter(); const router = useRouter();
const store = useStore(); const store = useStore();
@ -36,8 +37,13 @@ const determinePitchTypeName = (id: number): string => {
} }
const gotoHome = () => { const gotoHome = () => {
store.dispatch("bullpen/finish", bullpen.value.bullpen); BullpenSessionService.save(bullpen.value.bullpen).then((bullpen) => {
store.commit("bullpen/finish", bullpen);
router.push({path: '/home'}); router.push({path: '/home'});
}, (error) => {
// Todo: Handle error
console.log(error);
});
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, onMounted, ref} from "vue"; import {computed, ref} from "vue";
import { import {
IonContent, IonContent,
IonHeader, IonHeader,
@ -32,10 +32,6 @@ const isAuthenticated = computed(() => store.state.auth.isAuthenticated);
const pitchTypes = computed(() => store.state.pitchTypes.pitchTypes); const pitchTypes = computed(() => store.state.pitchTypes.pitchTypes);
const bullpen = computed(() => store.state.bullpen); const bullpen = computed(() => store.state.bullpen);
onMounted(async () => {
await store.dispatch("bullpen/start", pitcher.value);
});
const setPitchType = (pitchType: PitchType) => { const setPitchType = (pitchType: PitchType) => {
pitch.value.pitchTypeId = pitchType.id; pitch.value.pitchTypeId = pitchType.id;
} }

View File

@ -5,6 +5,7 @@ import { playOutline, statsChartOutline, personOutline, logOutOutline } from 'io
// import userImage from '../assets/groot.jpg' // import userImage from '../assets/groot.jpg'
import {useStore} from "vuex"; import {useStore} from "vuex";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import BullpenSessionService from "@/services/BullpenSessionService";
const router = useRouter(); const router = useRouter();
const store = useStore(); const store = useStore();
@ -20,11 +21,15 @@ if (pitcher.value === undefined || pitcher.value === null || pitcher.value === '
} }
const startBullpen = () => { const startBullpen = () => {
store.dispatch("bullpen/start", pitcher.value);
router.push({ path: '/bullpen' }); router.push({ path: '/bullpen' });
}; };
const showStats = () => { const showStats = () => {
BullpenSessionService.fetchAll().then((bullpens) => {
store.commit("bullpen/fetch", bullpens);
router.push({ path: '/stats' }); router.push({ path: '/stats' });
})
}; };
const showProfile = () => { const showProfile = () => {

View File

@ -9,8 +9,6 @@ import {
IonInput IonInput
} from "@ionic/vue"; } from "@ionic/vue";
// Todo: https://github.com/alanmontgomery/ionic-react-login
import { import {
lockClosedOutline, lockClosedOutline,
personOutline, personOutline,
@ -21,6 +19,9 @@ import {Field, useForm} from 'vee-validate';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useStore } from 'vuex' import { useStore } from 'vuex'
import * as yup from 'yup'; import * as yup from 'yup';
import PitchTypeService from "@/services/PitchTypeService";
import EventBus from "@/common/EventBus";
import PitchType from "@/types/PitchType";
const loading = ref(false); const loading = ref(false);
const server = JSON.parse(localStorage.getItem("server") || '""'); const server = JSON.parse(localStorage.getItem("server") || '""');
@ -31,7 +32,7 @@ const schema = yup.object({
.string() .string()
.min(8, 'Minimum 8 characters') .min(8, 'Minimum 8 characters')
.matches(/\d/, 'Must include a number') .matches(/\d/, 'Must include a number')
// .matches(/[^A-Za-z\d]/, 'Must include a special character') .matches(/[^A-Za-z\d]/, 'Must include a special character')
.required('Password is required'), .required('Password is required'),
}); });
@ -75,9 +76,10 @@ const submit = handleSubmit((values, { resetForm }) => {
const onLogin = () => { const onLogin = () => {
console.log('check if pitch types are available.'); console.log('check if pitch types are available.');
store.dispatch('pitchTypes/initialize').then(() => { PitchTypeService.fetchAll().then((pitchTypes: PitchType[]) => {
store.commit('pitchTypes/initialize', pitchTypes);
router.push({path: '/home'}); router.push({path: '/home'});
}, error => { }, (error) => {
console.log(error); console.log(error);
}); });
} }

View File

@ -4,20 +4,21 @@ import { useRouter } from "vue-router";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonList, IonItem, IonLabel } from '@ionic/vue'; import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonList, IonItem, IonLabel } from '@ionic/vue';
import UserService from "@/services/UserService"; import UserService from "@/services/UserService";
import User from "@/types/User"; import User from "@/types/User";
import BullpenSessionService from "@/services/BullpenSessionService"; // import BullpenSessionService from "@/services/BullpenSessionService";
const pitchers = ref<User[]>([]); const pitchers = ref<User[]>([]);
const router = useRouter(); const router = useRouter();
const bps = ref(BullpenSessionService); // const bps = ref(BullpenSessionService);
bps.value.clear(); // bps.value.clear();
onMounted(async () => { onMounted(async () => {
pitchers.value = await UserService.getAllUsers(); pitchers.value = await UserService.fetchAll();
}); });
const goToPreparePitch = (pitcher: User) => { const goToPreparePitch = (pitcher: User) => {
bps.value.startSession( pitcher ); console.log(pitcher);
// bps.value.startSession( pitcher );
router.push({ path: '/bullpen' }); router.push({ path: '/bullpen' });
}; };

View File

@ -27,7 +27,12 @@ const saveServerAddress = () => {
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<ion-row> <ion-row>
<ion-col class="ion-padding"> <ion-col class="ion-padding">
<ion-input name="ipaddress" v-model="server.host" type="text" required placeholder="Enter server address"></ion-input> <ion-input name="ipaddress" v-model="server.host" type="text" required placeholder="Bullpen Server"></ion-input>
</ion-col>
</ion-row>
<ion-row>
<ion-col>
<ion-input name="port" v-model="server.port" type="number" required placeholder="Port"></ion-input>
</ion-col> </ion-col>
</ion-row> </ion-row>
<ion-row> <ion-row>

View File

@ -3,27 +3,26 @@ FROM node:${NODE_VERSION}-alpine AS base
WORKDIR /user/src/app WORKDIR /user/src/app
EXPOSE 8080 EXPOSE 8080
FROM base as production FROM base AS production
ENV NODE_ENV=production ENV NODE_ENV=production
COPY .env.production .env
RUN --mount=type=bind,source=package.json,target=package.json \ RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \ --mount=type=cache,target=/root/.npm \
npm ci --omit=dev npm ci --omit=dev
USER node USER node
COPY . . COPY . .
EXPOSE 8080 COPY .env.production .env
CMD ["node", "server.js"] CMD ["node", "server.js"]
FROM base as development FROM base AS development
ENV NODE_ENV=development ENV NODE_ENV=development
COPY .env.development .env
RUN --mount=type=bind,source=package.json,target=package.json \ RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \ --mount=type=cache,target=/root/.npm \
npm ci --include=dev npm ci --include=dev
USER node USER node
COPY . . COPY . .
COPY .env.development .env
CMD ["npm", "run", "start:dev"] CMD ["npm", "run", "start:dev"]

View File

@ -3,23 +3,13 @@ const cors = require("cors");
const app = express(); const app = express();
const corsOptions = { app.use(cors());
origin: "http://localhost:5173"
};
app.use(cors(corsOptions));
// parse requests of content-type - application/json // parse requests of content-type - application/json
app.use(express.json()); app.use(express.json());
// parse requests of content-type - application/x-www-form-urlencoded // parse requests of content-type - application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
// 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();
// });
// routes // routes
require('./routes/info.routes')(app); require('./routes/info.routes')(app);
require('./routes/auth.routes')(app); require('./routes/auth.routes')(app);

View File

@ -5,6 +5,6 @@ module.exports = {
// jwtRefreshExpiration: 86400, // 24 hours // jwtRefreshExpiration: 86400, // 24 hours
/* for test */ /* for test */
jwtExpiration: 60, // 1 minute jwtExpiration: 30, // 30 seconds
jwtRefreshExpiration: 120 // 2 minutes jwtRefreshExpiration: 60 // 1 minute
}; };

View File

@ -122,7 +122,7 @@ exports.refreshToken = async (req, res) => {
return; return;
} }
let newAccessToken = jwt.sign({ id: auth.id }, config.secret, { let newAccessToken = jwt.sign({ id: refreshToken.authId }, config.secret, {
expiresIn: config.jwtExpiration, expiresIn: config.jwtExpiration,
}); });

1
backend/db/password.txt Normal file
View File

@ -0,0 +1 @@
test123!

View File

@ -21,9 +21,10 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
app: app:
build: image: git.palaeomatiker.home64.de/sascha/bullpen:latest
context: . # build:
target: development # context: .
# target: development
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@ -31,7 +32,7 @@ services:
- "8124:8080" - "8124:8080"
container_name: bullpen container_name: bullpen
environment: environment:
NODE_ENV: development NODE_ENV: production
POSTGRES_HOST: ${DB_HOST} POSTGRES_HOST: ${DB_HOST}
POSTGRES_USER: ${DB_USER} POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD_FILE: /run/secrets/db-password POSTGRES_PASSWORD_FILE: /run/secrets/db-password

View File

@ -46,28 +46,28 @@ const isAdmin = (req, res, next) => {
}); });
}; };
const isModerator = (req, res, next) => { const isCoach = (req, res, next) => {
User.findByPk(req.userId).then(user => { User.findByPk(req.userId).then(user => {
user.getRoles().then(roles => { user.getRoles().then(roles => {
for (let i = 0; i < roles.length; i++) { for (let i = 0; i < roles.length; i++) {
if (roles[i].name === "moderator") { if (roles[i].name === "coach") {
next(); next();
return; return;
} }
} }
res.status(403).send({ res.status(403).send({
message: "Require Moderator Role!" message: "Require Coach Role!"
}); });
}); });
}); });
}; };
const isModeratorOrAdmin = (req, res, next) => { const isCoachOrAdmin = (req, res, next) => {
User.findByPk(req.userId).then(user => { User.findByPk(req.userId).then(user => {
user.getRoles().then(roles => { user.getRoles().then(roles => {
for (let i = 0; i < roles.length; i++) { for (let i = 0; i < roles.length; i++) {
if (roles[i].name === "moderator") { if (roles[i].name === "coach") {
next(); next();
return; return;
} }
@ -79,7 +79,7 @@ const isModeratorOrAdmin = (req, res, next) => {
} }
res.status(403).send({ res.status(403).send({
message: "Require Moderator or Admin Role!" message: "Require Coach or Admin Role!"
}); });
}); });
}); });
@ -88,7 +88,7 @@ const isModeratorOrAdmin = (req, res, next) => {
const authJwt = { const authJwt = {
verifyToken: verifyToken, verifyToken: verifyToken,
isAdmin: isAdmin, isAdmin: isAdmin,
isModerator: isModerator, isModerator: isCoach,
isModeratorOrAdmin: isModeratorOrAdmin isModeratorOrAdmin: isCoachOrAdmin
}; };
module.exports = authJwt; module.exports = authJwt;

View File

@ -28,6 +28,7 @@
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^4.21.2", "express": "^4.21.2",
"express-validator": "^7.2.1", "express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@ -35,17 +36,16 @@
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"process": "^0.11.10", "process": "^0.11.10",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"sequelize-cli": "^6.6.2",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.7.0", "@jest/globals": "^29.7.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"sequelize-cli": "^6.6.2",
"supertest": "^7.0.0" "supertest": "^7.0.0"
} }
} }

View File

@ -2,7 +2,7 @@
# Replace with your actual Gitea Docker registry # Replace with your actual Gitea Docker registry
REGISTRY_URL=git.palaeomatiker.home64.de REGISTRY_URL=git.palaeomatiker.home64.de
IMAGE_NAME=express-sequelize-app IMAGE_NAME=bullpen
TAG=latest TAG=latest
# Authenticate with Docker registry # Authenticate with Docker registry

View File

@ -1,6 +1,20 @@
# Build and push docker image
```bash
docker buildx build --target production -t bullpen:latest .
docker tag bullpen git.palaeomatiker.home64.de/sascha/bullpen:latest
docker login git.paleaomatiker.home64.de
docker push git.palaeomatiker.home64.de/sascha/bullpen:latest
docker logout git.paleaomatiker.home64.de
```
```bash
docker-compose up --build -d docker-compose up --build -d
docker-compose exec app npx sequelize-cli db:migrate docker-compose exec app npx sequelize-cli db:migrate
docker-compose exec app npx sequelize-cli db:seed:all docker-compose exec app npx sequelize-cli db:seed:all
docker-compose exec app sh docker-compose exec app sh
```

View File

@ -7,11 +7,15 @@ module.exports = {
if (process.env.NODE_ENV !== 'production') return; if (process.env.NODE_ENV !== 'production') return;
await queryInterface.bulkInsert('Authentications', [ await queryInterface.bulkInsert('Authentications', [
{ email: 'admin@example.com', password: bcrypt.hashSync('admin$123', 8), createdAt: new Date(), updatedAt: new Date() } { email: 'admin@example.com', password: bcrypt.hashSync('admin$123', 8), createdAt: new Date(), updatedAt: new Date() },
{ email: 'kuehl.julian@gmail.com', password: bcrypt.hashSync('julian$123', 8), createdAt: new Date(), updatedAt: new Date() },
{ email: 'cichocki.c@gmail.com', password: bcrypt.hashSync('clemens$123', 8), createdAt: new Date(), updatedAt: new Date() }
]); ]);
const auths = await queryInterface.select(null, 'Authentications'); const auths = await queryInterface.select(null, 'Authentications');
const adminAuthId = auths.filter((auth) => auth.email === 'admin@example.com').map((auth) => auth.id).shift(); const adminAuthId = auths.filter((auth) => auth.email === 'admin@example.com').map((auth) => auth.id).shift();
const julianAuthId = auths.filter((auth) => auth.email === 'kuehl.julian@gmail.com').map((auth) => auth.id).shift();
const clemensAuthId = auths.filter((auth) => auth.email === 'cichocki.c@gmail.com').map((auth) => auth.id).shift();
await queryInterface.bulkInsert('Users', [{ await queryInterface.bulkInsert('Users', [{
firstName: 'Admin', firstName: 'Admin',
@ -21,19 +25,40 @@ module.exports = {
authId: adminAuthId, authId: adminAuthId,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}] }, {
); firstName: 'Julian',
lastName: 'Kühl',
dateOfBirth: '2009-08-08',
gender: 'male',
authId: julianAuthId,
createdAt: new Date(),
updatedAt: new Date(),
}, {
firstName: 'Clemens',
lastName: 'Cichocki',
dateOfBirth: '1970-01-01',
gender: 'male',
authId: clemensAuthId,
createdAt: new Date(),
updatedAt: new Date(),
}]);
const users = await queryInterface.select(null, 'Users'); const users = await queryInterface.select(null, 'Users');
const adminUserId = users.filter((user) => user.firstName === 'Admin').map((user) => user.id).shift(); const adminUserId = users.filter((user) => user.firstName === 'Admin').map((user) => user.id).shift();
const julianUserId = users.filter((user) => user.firstName === 'Julian').map((user) => user.id).shift();
const clemensUserId = users.filter((user) => user.firstName === 'Clemens').map((user) => user.id).shift();
const roles = await queryInterface.select(null, 'Roles'); const roles = await queryInterface.select(null, 'Roles');
const adminRoleId = roles.filter((role) => role.name === 'admin').map((role) => role.id).shift(); const adminRoleId = roles.filter((role) => role.name === 'admin').map((role) => role.id).shift();
const playerRoleId = roles.filter((role) => role.name === 'player').map((role) => role.id).shift();
const coachRoleId = roles.filter((role) => role.name === 'coach').map((role) => role.id).shift();
await queryInterface.bulkInsert('UserRoles', [ await queryInterface.bulkInsert('UserRoles', [
{ userId: adminUserId, roleId: adminRoleId, createdAt: new Date(), updatedAt: new Date() } { userId: adminUserId, roleId: adminRoleId, createdAt: new Date(), updatedAt: new Date() },
{ userId: julianUserId, roleId: playerRoleId, createdAt: new Date(), updatedAt: new Date() },
{ userId: clemensUserId, roleId: coachRoleId, createdAt: new Date(), updatedAt: new Date() },
{ userId: clemensUserId, roleId: playerRoleId, createdAt: new Date(), updatedAt: new Date() },
]); ]);
}, },
async down (queryInterface, /*Sequelize*/) { async down (queryInterface, /*Sequelize*/) {

View File

@ -73,7 +73,7 @@ describe("Test bullpen session", () => {
const user = response.body; const user = response.body;
response = await request(app) response = await request(app)
.get("/api/bullpen_session") .get("/api/bullpen_session", { params: { user: user.id } })
.set('x-access-token', user.accessToken) .set('x-access-token', user.accessToken)
.query({ user: user.id }); .query({ user: user.id });