Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da85d1487c | |||
|
dc65b49219
|
|||
|
9c45fb11ee
|
|||
|
45dfc22c60
|
|||
|
a38fb20e5a
|
|||
|
cb85e81d67
|
|||
|
59fee19a76
|
|||
|
a79e2186a2
|
|||
|
8f93e1ab7d
|
|||
|
34a4a6664f
|
@@ -0,0 +1,66 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # Compile TypeScript → dist/
|
||||||
|
npm run start # Build and start (tsc && node ./dist/app.js)
|
||||||
|
npm run debug # Start with DEBUG=* environment variable
|
||||||
|
npm run test # Run Jest tests with coverage (outputs sonar-report.xml)
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a single test file:
|
||||||
|
```bash
|
||||||
|
npx jest test/some.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Express.js REST API in TypeScript with a service-oriented layering. The sole domain is `Calendar`, which organises **events** and **users**.
|
||||||
|
|
||||||
|
**Request path:**
|
||||||
|
1. `app.ts` mounts `Calendar.router.ts` at `/calendar`
|
||||||
|
2. `Calendar.router.ts` delegates to `events.router.ts` and `users.router.ts`
|
||||||
|
3. Routers call services; services call the MariaDB pool in `Calendar.db.ts`
|
||||||
|
|
||||||
|
**Key layers:**
|
||||||
|
|
||||||
|
| Layer | Location |
|
||||||
|
|---|---|
|
||||||
|
| Router | `src/models/calendar/Calendar.router.ts`, `…/events/events.router.ts`, `…/users/users.router.ts` |
|
||||||
|
| Services | `…/events/events.service.ts`, `…/users/users.service.ts`, `…/events/credentials.service.ts`, `…/events/icalgenerator.service.ts` |
|
||||||
|
| DB pool | `src/models/calendar/Calendar.db.ts` (MariaDB, pool size 5) |
|
||||||
|
| Shared | `src/common/` (base route class, nodemailer wrapper), `src/middleware/logger.ts` (Winston) |
|
||||||
|
|
||||||
|
**Auth model:** Users must have a `@nachklang.art` email. After activation they receive a session token (30-day window); the token hash + IP are stored in the DB. Credentials for non-user calendar access (`MEMBER_CREDENTIAL`, `CHOIR_CREDENTIAL`, `MANAGEMENT_CREDENTIAL`) come from `.env`.
|
||||||
|
|
||||||
|
**Event versioning:** Events have a companion `event_versions` table. `events.service.ts` manages writes to both.
|
||||||
|
|
||||||
|
**Calendar types and IDs:** `public` (1), `members` (2), `management` (3), `choir` (4), `birthdays` (5). `credentials.service.ts` enforces which session/credential can read each calendar.
|
||||||
|
|
||||||
|
**iCal export:** `icalgenerator.service.ts` converts DB events to RFC 5545 format; reachable via `GET /calendar/events/{calendar}/ical`.
|
||||||
|
|
||||||
|
**API docs:** Swagger UI served at `/docs`, generated from JSDoc annotations in the router files.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
Copy `.env.example` (or create `.env`) with:
|
||||||
|
```
|
||||||
|
PORT=
|
||||||
|
DB_HOST=
|
||||||
|
DB_USER=
|
||||||
|
DB_PASSWORD=
|
||||||
|
CALENDAR_DB=
|
||||||
|
EMAIL_HOST=
|
||||||
|
EMAIL_USERNAME=
|
||||||
|
EMAIL_PASSWORD=
|
||||||
|
MEMBER_CREDENTIAL=
|
||||||
|
CHOIR_CREDENTIAL=
|
||||||
|
MANAGEMENT_CREDENTIAL=
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript config
|
||||||
|
|
||||||
|
Strict mode enabled, target ES2016, compiled output in `./dist`, inline source maps. Tests run through `ts-jest` directly against `.ts` sources.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Deferred Security Issues
|
||||||
|
|
||||||
|
These items were identified during a security review on 2026-05-02 and consciously deferred.
|
||||||
|
**Must be addressed before opening the application to a larger or public userbase.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Session credentials in URL query parameters (logged-in users)
|
||||||
|
|
||||||
|
**Files:** `src/models/calendar/events/events.router.ts` — all GET/PUT/DELETE handlers
|
||||||
|
|
||||||
|
`sessionId` and `sessionKey` are currently read from query parameters, which means they appear in server access logs, browser history, proxy logs, and `Referer` headers.
|
||||||
|
|
||||||
|
**Fix:** Move to request headers (`X-Session-Id` / `X-Session-Key`) or the request body. Requires a corresponding frontend update.
|
||||||
|
|
||||||
|
> Note: the shared calendar `password` parameter in query params is intentional (iCal clients don't support headers) and is acceptable for the current setup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. No event ownership check
|
||||||
|
|
||||||
|
**Files:** `src/models/calendar/events/events.router.ts`
|
||||||
|
- `PUT /:eventId` (update)
|
||||||
|
- `PUT /move/:eventId` (move)
|
||||||
|
- `DELETE /:eventId` (delete)
|
||||||
|
|
||||||
|
Currently any active user can edit, move, or delete any event regardless of who created it. This is acceptable while all users are trusted admins.
|
||||||
|
|
||||||
|
**Fix:** When non-admin users are introduced, fetch the event first and verify `event.createdById === user.userId` before allowing the mutation. Add an `isAdmin` flag to the user model to let admins bypass the check.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Activation token has no expiry
|
||||||
|
|
||||||
|
**File:** `src/models/calendar/users/users.service.ts` — `createUser` / `activateUser`
|
||||||
|
|
||||||
|
The email activation link is valid indefinitely. Acceptable for a small, trusted userbase.
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. Add an `activation_expires` column to the `users` table (e.g. `DATETIME`).
|
||||||
|
2. Set it to `NOW() + INTERVAL 24 HOUR` in `createUser`.
|
||||||
|
3. Check `activation_expires > NOW()` in `activateUser` before accepting the token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Password reset token has no expiry
|
||||||
|
|
||||||
|
**File:** `src/models/calendar/users/users.service.ts` — `initiatePasswordReset` / `finalizePasswordReset`
|
||||||
|
|
||||||
|
The reset token stored in `pw_reset_token_hash` never expires. Acceptable for a small, trusted userbase.
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. Add a `pw_reset_expires` column to the `users` table (e.g. `DATETIME`).
|
||||||
|
2. Set it to `NOW() + INTERVAL 15 MINUTE` in `initiatePasswordReset`.
|
||||||
|
3. Check `pw_reset_expires > NOW()` in `finalizePasswordReset` before accepting the token.
|
||||||
@@ -67,6 +67,7 @@ const options = {
|
|||||||
swaggerDefinition,
|
swaggerDefinition,
|
||||||
// Paths to files containing OpenAPI definitions
|
// Paths to files containing OpenAPI definitions
|
||||||
apis: [
|
apis: [
|
||||||
|
'./src/models/**/*.interface.ts',
|
||||||
'./src/models/**/*.router.ts'
|
'./src/models/**/*.router.ts'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+37
-3
@@ -15,9 +15,10 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.18.2",
|
||||||
"guid-typescript": "^1.0.9",
|
"guid-typescript": "^1.0.9",
|
||||||
"mariadb": "^3.0.2",
|
"mariadb": "^3.0.2",
|
||||||
|
"nodemailer": "^6.9.8",
|
||||||
"random-words": "^1.1.1",
|
"random-words": "^1.1.1",
|
||||||
"swagger-jsdoc": "^6.1.0",
|
"swagger-jsdoc": "^6.1.0",
|
||||||
"swagger-ui-express": "^4.3.0",
|
"swagger-ui-express": "^4.3.0",
|
||||||
@@ -27,8 +28,10 @@
|
|||||||
"@types/app-root-path": "^1.2.4",
|
"@types/app-root-path": "^1.2.4",
|
||||||
"@types/bcrypt": "^3.0.1",
|
"@types/bcrypt": "^3.0.1",
|
||||||
"@types/debug": "^4.1.5",
|
"@types/debug": "^4.1.5",
|
||||||
"@types/express": "^4.17.11",
|
"@types/express": "^4.17.15",
|
||||||
"@types/jest": "^28.1.3",
|
"@types/jest": "^28.1.3",
|
||||||
|
"@types/node": "^18.11.17",
|
||||||
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/random-words": "^1.1.2",
|
"@types/random-words": "^1.1.2",
|
||||||
"@types/swagger-jsdoc": "^6.0.1",
|
"@types/swagger-jsdoc": "^6.0.1",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
@@ -39,7 +42,7 @@
|
|||||||
"source-map-support": "^0.5.19",
|
"source-map-support": "^0.5.19",
|
||||||
"ts-jest": "^28.0.5",
|
"ts-jest": "^28.0.5",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"typescript": "^4.1.5"
|
"typescript": "^4.9.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
@@ -1272,6 +1275,15 @@
|
|||||||
"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
|
"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/nodemailer": {
|
||||||
|
"version": "6.4.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz",
|
||||||
|
"integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/prettier": {
|
"node_modules/@types/prettier": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
||||||
@@ -4020,6 +4032,14 @@
|
|||||||
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
|
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "6.9.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz",
|
||||||
|
"integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nopt": {
|
"node_modules/nopt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||||
@@ -6592,6 +6612,15 @@
|
|||||||
"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
|
"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/nodemailer": {
|
||||||
|
"version": "6.4.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz",
|
||||||
|
"integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/prettier": {
|
"@types/prettier": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
||||||
@@ -8691,6 +8720,11 @@
|
|||||||
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
|
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"nodemailer": {
|
||||||
|
"version": "6.9.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz",
|
||||||
|
"integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ=="
|
||||||
|
},
|
||||||
"nopt": {
|
"nopt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"guid-typescript": "^1.0.9",
|
"guid-typescript": "^1.0.9",
|
||||||
"mariadb": "^3.0.2",
|
"mariadb": "^3.0.2",
|
||||||
|
"nodemailer": "^6.9.8",
|
||||||
"random-words": "^1.1.1",
|
"random-words": "^1.1.1",
|
||||||
"swagger-jsdoc": "^6.1.0",
|
"swagger-jsdoc": "^6.1.0",
|
||||||
"swagger-ui-express": "^4.3.0",
|
"swagger-ui-express": "^4.3.0",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"@types/express": "^4.17.15",
|
"@types/express": "^4.17.15",
|
||||||
"@types/jest": "^28.1.3",
|
"@types/jest": "^28.1.3",
|
||||||
"@types/node": "^18.11.17",
|
"@types/node": "^18.11.17",
|
||||||
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/random-words": "^1.1.2",
|
"@types/random-words": "^1.1.2",
|
||||||
"@types/swagger-jsdoc": "^6.0.1",
|
"@types/swagger-jsdoc": "^6.0.1",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import * as nodemailer from 'nodemailer';
|
||||||
|
|
||||||
|
export namespace MailService {
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.EMAIL_HOST,
|
||||||
|
pool: true,
|
||||||
|
port: 465,
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USERNAME,
|
||||||
|
pass: process.env.EMAIL_PASSWORD
|
||||||
|
},
|
||||||
|
tls: {rejectUnauthorized: false}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mailConfigurations = {
|
||||||
|
|
||||||
|
// It should be a string of sender email
|
||||||
|
from: 'noreply@nachklang.art',
|
||||||
|
|
||||||
|
// Comma Separated list of mails
|
||||||
|
to: 'mail@pmueller.me',
|
||||||
|
|
||||||
|
// Subject of Email
|
||||||
|
subject: '',
|
||||||
|
|
||||||
|
// This would be the text of email body
|
||||||
|
text: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendMail = async (recipientAddress: string, subject: string, body: string) => {
|
||||||
|
mailConfigurations.to = recipientAddress;
|
||||||
|
mailConfigurations.subject = subject;
|
||||||
|
mailConfigurations.text = body;
|
||||||
|
|
||||||
|
await transporter.sendMail(mailConfigurations);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@ export namespace NachklangCalendarDB {
|
|||||||
user: process.env.DB_USER,
|
user: process.env.DB_USER,
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
database: process.env.CALENDAR_DB,
|
database: process.env.CALENDAR_DB,
|
||||||
connectionLimit: 5
|
connectionLimit: 5,
|
||||||
|
autoCommit: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getConnection = async () => {
|
export const getConnection = async () => {
|
||||||
|
|||||||
@@ -16,6 +16,39 @@ calendarRouter.use('/events', eventsRouter);
|
|||||||
calendarRouter.use('/users', usersRouter);
|
calendarRouter.use('/users', usersRouter);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar:
|
||||||
|
* get:
|
||||||
|
* summary: Calendar API root endpoint
|
||||||
|
* description: Returns a welcome message for the Nachklang e.V. Calendar API.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Success
|
||||||
|
* content:
|
||||||
|
* text/plain:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* example: Nachklang e.V. Calendar API Endpoint
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
calendarRouter.get('/', async (req: Request, res: Response) => {
|
calendarRouter.get('/', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
res.status(200).send('Nachklang e.V. Calendar API Endpoint');
|
res.status(200).send('Nachklang e.V. Calendar API Endpoint');
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ dotenv.config();
|
|||||||
export const checkAdminPrivileges = async (sessionId: string, sessionKey: string, ip: string) => {
|
export const checkAdminPrivileges = async (sessionId: string, sessionKey: string, ip: string) => {
|
||||||
if(sessionId) {
|
if(sessionId) {
|
||||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||||
return user.isActive;
|
return user?.isActive ?? false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -23,12 +23,25 @@ export const checkAdminPrivileges = async (sessionId: string, sessionKey: string
|
|||||||
export const checkMemberPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => {
|
export const checkMemberPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => {
|
||||||
if(sessionId) {
|
if(sessionId) {
|
||||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||||
return user.isActive;
|
return user?.isActive ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return password == process.env.MEMBER_CREDENTIAL;
|
return password == process.env.MEMBER_CREDENTIAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the password gives choir view privileges
|
||||||
|
* @param password
|
||||||
|
*/
|
||||||
|
export const checkChoirPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => {
|
||||||
|
if(sessionId) {
|
||||||
|
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||||
|
return user?.isActive ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return password == process.env.CHOIR_CREDENTIAL;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the password gives management view privileges
|
* Checks if the password gives management view privileges
|
||||||
* @param password
|
* @param password
|
||||||
@@ -36,7 +49,7 @@ export const checkMemberPrivileges = async (sessionId: string, sessionKey: strin
|
|||||||
export const checkManagementPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => {
|
export const checkManagementPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => {
|
||||||
if(sessionId) {
|
if(sessionId) {
|
||||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||||
return user.isActive;
|
return user?.isActive ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return password == process.env.MANAGEMENT_CREDENTIAL;
|
return password == process.env.MANAGEMENT_CREDENTIAL;
|
||||||
@@ -48,8 +61,12 @@ export const hasAccess = async (calendarName: string, sessionId: string, session
|
|||||||
return true;
|
return true;
|
||||||
case 'members':
|
case 'members':
|
||||||
return await checkMemberPrivileges(sessionId, sessionKey, password, ip);
|
return await checkMemberPrivileges(sessionId, sessionKey, password, ip);
|
||||||
|
case 'choir':
|
||||||
|
return await checkChoirPrivileges(sessionId, sessionKey, password, ip);
|
||||||
case 'management':
|
case 'management':
|
||||||
return await checkManagementPrivileges(sessionId, sessionKey, password, ip);
|
return await checkManagementPrivileges(sessionId, sessionKey, password, ip);
|
||||||
|
case 'birthdays':
|
||||||
|
return await checkChoirPrivileges(sessionId, sessionKey, password, ip);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* Event:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - eventId
|
||||||
|
* - calendarId
|
||||||
|
* - uuid
|
||||||
|
* - name
|
||||||
|
* - description
|
||||||
|
* - startDateTime
|
||||||
|
* - endDateTime
|
||||||
|
* - createdDate
|
||||||
|
* - location
|
||||||
|
* - createdById
|
||||||
|
* - url
|
||||||
|
* - wholeDay
|
||||||
|
* properties:
|
||||||
|
* eventId:
|
||||||
|
* type: integer
|
||||||
|
* description: The unique identifier for the event
|
||||||
|
* example: 123
|
||||||
|
* calendarId:
|
||||||
|
* type: integer
|
||||||
|
* description: The ID of the calendar this event belongs to
|
||||||
|
* example: 1
|
||||||
|
* uuid:
|
||||||
|
* type: string
|
||||||
|
* description: A unique UUID for the event
|
||||||
|
* example: "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* description: The name/title of the event
|
||||||
|
* example: "Concert at Musikhochschule"
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* description: A detailed description of the event
|
||||||
|
* example: "Annual concert at the Musikhochschule"
|
||||||
|
* startDateTime:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: The start date and time of the event
|
||||||
|
* example: "2023-06-15T19:00:00.000Z"
|
||||||
|
* endDateTime:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: The end date and time of the event
|
||||||
|
* example: "2023-06-15T21:00:00.000Z"
|
||||||
|
* createdDate:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: The date and time when the event was created
|
||||||
|
* example: "2023-05-01T10:00:00.000Z"
|
||||||
|
* lastModifiedDate:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* example: "2023-05-01T10:00:00.000Z"
|
||||||
|
* location:
|
||||||
|
* type: string
|
||||||
|
* description: The location of the event
|
||||||
|
* example: "Musikhochschule, Karlsruhe"
|
||||||
|
* createdBy:
|
||||||
|
* type: string
|
||||||
|
* description: The name of the user who created the event
|
||||||
|
* example: "John Doe"
|
||||||
|
* createdById:
|
||||||
|
* type: integer
|
||||||
|
* description: The ID of the user who created the event
|
||||||
|
* example: 456
|
||||||
|
* lastModifiedBy:
|
||||||
|
* type: string
|
||||||
|
* description: The name of the user who last modified the event
|
||||||
|
* example: "John Doe"
|
||||||
|
* lastModifiedById:
|
||||||
|
* type: integer
|
||||||
|
* description: The ID of the user who last modified the event
|
||||||
|
* example: 456
|
||||||
|
* url:
|
||||||
|
* type: string
|
||||||
|
* description: A URL with more information about the event
|
||||||
|
* example: "https://www.nachklang.art/events/concert"
|
||||||
|
* wholeDay:
|
||||||
|
* type: boolean
|
||||||
|
* description: Whether the event lasts the whole day
|
||||||
|
* example: false
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* description: The status of the event
|
||||||
|
* enum: [PUBLIC, PRIVATE, DRAFT, DELETED]
|
||||||
|
* example: "PUBLIC"
|
||||||
|
*/
|
||||||
export interface Event {
|
export interface Event {
|
||||||
eventId: number;
|
eventId: number;
|
||||||
calendarId: number;
|
calendarId: number;
|
||||||
@@ -7,10 +100,14 @@ export interface Event {
|
|||||||
startDateTime: Date;
|
startDateTime: Date;
|
||||||
endDateTime: Date;
|
endDateTime: Date;
|
||||||
createdDate: Date;
|
createdDate: Date;
|
||||||
|
lastModifiedDate?: Date;
|
||||||
location: string;
|
location: string;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
createdById: number;
|
createdById: number;
|
||||||
|
lastModifiedBy?: string;
|
||||||
|
lastModifiedById?: number;
|
||||||
url: string;
|
url: string;
|
||||||
wholeDay: boolean;
|
wholeDay: boolean;
|
||||||
|
repeatFrequency: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export const eventsRouter = express.Router();
|
|||||||
export const calendarNames = new Map<string, any>([
|
export const calendarNames = new Map<string, any>([
|
||||||
['public', {id: 1, name: 'Nachklang_calendar'}],
|
['public', {id: 1, name: 'Nachklang_calendar'}],
|
||||||
['members', {id: 2, name: 'Nachklang_internal_calendar'}],
|
['members', {id: 2, name: 'Nachklang_internal_calendar'}],
|
||||||
['management', {id: 3, name: 'Nachklang_management_calendar'}]
|
['choir', {id: 4, name: 'Nachklang_choir_calendar'}],
|
||||||
|
['management', {id: 3, name: 'Nachklang_management_calendar'}],
|
||||||
|
['birthdays', {id: 5, name: 'Nachklang_birthday_calendar'}]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
@@ -32,6 +34,77 @@ export const calendarNames = new Map<string, any>([
|
|||||||
* Controller Definitions
|
* Controller Definitions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/events/{calendar}/json:
|
||||||
|
* get:
|
||||||
|
* summary: Get all events from a specific calendar in JSON format
|
||||||
|
* description: Returns all events from the specified calendar in JSON format. Authentication required.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: calendar
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* enum: [public, members, choir, management]
|
||||||
|
* description: The name of the calendar to get events from
|
||||||
|
* - in: query
|
||||||
|
* name: sessionId
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session ID for authentication
|
||||||
|
* - in: query
|
||||||
|
* name: sessionKey
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session key for authentication
|
||||||
|
* - in: query
|
||||||
|
* name: password
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Password for calendar access (if not using session authentication)
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Success
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/Event'
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - missing or invalid parameters
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Please state the name of the calendar you want events from.
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden - no access to the calendar
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: You do not have access to the specified calendar.
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
*/
|
||||||
eventsRouter.get('/:calendar/json', async (req: Request, res: Response) => {
|
eventsRouter.get('/:calendar/json', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Get request params
|
// Get request params
|
||||||
@@ -55,6 +128,7 @@ eventsRouter.get('/:calendar/json', async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||||
|
|
||||||
|
// If no user was found, check if the password gives access to the calendar
|
||||||
if(user === null || !user.isActive) {
|
if(user === null || !user.isActive) {
|
||||||
if (! await CredentialService.hasAccess(calendarName, sessionId, sessionKey, password, ip)) {
|
if (! await CredentialService.hasAccess(calendarName, sessionId, sessionKey, password, ip)) {
|
||||||
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
||||||
@@ -64,7 +138,7 @@ eventsRouter.get('/:calendar/json', async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
let events: Event[];
|
let events: Event[];
|
||||||
|
|
||||||
if(user.isActive) {
|
if(user?.isActive) {
|
||||||
events = await EventService.getAllEventsAdmin(calendarId);
|
events = await EventService.getAllEventsAdmin(calendarId);
|
||||||
} else {
|
} else {
|
||||||
events = await EventService.getAllEvents(calendarId);
|
events = await EventService.getAllEvents(calendarId);
|
||||||
@@ -73,11 +147,220 @@ eventsRouter.get('/:calendar/json', async (req: Request, res: Response) => {
|
|||||||
// Send the events back
|
// Send the events back
|
||||||
res.status(200).send(events);
|
res.status(200).send(events);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('Error handling a request: ' + e.message);
|
let errorGuid = Guid.create().toString();
|
||||||
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
||||||
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
|
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/events/{calendar}/json/next:
|
||||||
|
* get:
|
||||||
|
* summary: Get the next upcoming event from a calendar
|
||||||
|
* description: Returns the next upcoming event from the specified calendar. Authentication required.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: calendar
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* enum: [public, members, choir, management]
|
||||||
|
* description: The name of the calendar to get the next event from
|
||||||
|
* - in: query
|
||||||
|
* name: sessionId
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session ID for authentication
|
||||||
|
* - in: query
|
||||||
|
* name: sessionKey
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session key for authentication
|
||||||
|
* - in: query
|
||||||
|
* name: password
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Password for calendar access (if not using session authentication)
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Success
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Event'
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - missing or invalid parameters
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Please state the name of the calendar you want events from.
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden - no access to the calendar
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: You do not have access to the specified calendar.
|
||||||
|
* 404:
|
||||||
|
* description: Not found - no upcoming events
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: No upcoming events found.
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
|
eventsRouter.get('/:calendar/json/next', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
// Get request params
|
||||||
|
let calendarName: string = req.params.calendar as string ?? '';
|
||||||
|
let sessionId: string = req.query.sessionId as string ?? '';
|
||||||
|
let sessionKey: string = req.query.sessionKey as string ?? '';
|
||||||
|
let password: string = req.query.password as string ?? '';
|
||||||
|
let ip: string = req.socket.remoteAddress ?? '';
|
||||||
|
|
||||||
|
if (calendarName.length < 1) {
|
||||||
|
res.status(400).send({'message': 'Please state the name of the calendar you want events from.'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.from(calendarNames.keys()).includes(calendarName)) {
|
||||||
|
res.status(400).send({'message': 'Unknown calendar.'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let calendarId: number = calendarNames.get(calendarName)!.id;
|
||||||
|
|
||||||
|
if (! await CredentialService.hasAccess(calendarName, sessionId, sessionKey, password, ip)) {
|
||||||
|
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next upcoming event
|
||||||
|
let event = await EventService.getNextUpcomingEvent(calendarId);
|
||||||
|
|
||||||
|
if (event === null) {
|
||||||
|
res.status(404).send({'message': 'No upcoming events found.'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the event back
|
||||||
|
res.status(200).send(event);
|
||||||
|
} catch (e: any) {
|
||||||
|
let errorGuid = Guid.create().toString();
|
||||||
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
||||||
|
res.status(500).send({
|
||||||
|
'status': 'PROCESSING_ERROR',
|
||||||
|
'message': 'Internal Server Error. Try again later.',
|
||||||
|
'reference': errorGuid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/events/{calendar}/ical:
|
||||||
|
* get:
|
||||||
|
* summary: Get all events from a specific calendar in iCal format
|
||||||
|
* description: Returns all events from the specified calendar in iCal format for calendar applications. Authentication required.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: calendar
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* enum: [public, members, choir, management]
|
||||||
|
* description: The name of the calendar to get events from
|
||||||
|
* - in: query
|
||||||
|
* name: sessionId
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session ID for authentication
|
||||||
|
* - in: query
|
||||||
|
* name: sessionKey
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session key for authentication
|
||||||
|
* - in: query
|
||||||
|
* name: password
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Password for calendar access (if not using session authentication)
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Success - returns iCal file
|
||||||
|
* content:
|
||||||
|
* text/calendar:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - missing or invalid parameters
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Please state the name of the calendar you want events from.
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden - no access to the calendar
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: You do not have access to the specified calendar.
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
eventsRouter.get('/:calendar/ical', async (req: Request, res: Response) => {
|
eventsRouter.get('/:calendar/ical', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Get request params
|
// Get request params
|
||||||
@@ -125,6 +408,120 @@ eventsRouter.get('/:calendar/ical', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/events:
|
||||||
|
* post:
|
||||||
|
* summary: Create a new event
|
||||||
|
* description: Creates a new event in the specified calendar. Authentication required.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: sessionId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session ID for authentication
|
||||||
|
* - in: query
|
||||||
|
* name: sessionKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session key for authentication
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - calendarId
|
||||||
|
* - name
|
||||||
|
* - startDateTime
|
||||||
|
* - endDateTime
|
||||||
|
* properties:
|
||||||
|
* calendarId:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* example: "Concert at Musikhochschule"
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* example: "Annual concert at the Musikhochschule"
|
||||||
|
* startDateTime:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* example: "2023-06-15T19:00:00.000Z"
|
||||||
|
* endDateTime:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* example: "2023-06-15T21:00:00.000Z"
|
||||||
|
* location:
|
||||||
|
* type: string
|
||||||
|
* example: "Musikhochschule, Karlsruhe"
|
||||||
|
* url:
|
||||||
|
* type: string
|
||||||
|
* example: "https://www.nachklang.art/events/concert"
|
||||||
|
* wholeDay:
|
||||||
|
* type: boolean
|
||||||
|
* example: false
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* enum: [PUBLIC, PRIVATE, DRAFT, DELETED]
|
||||||
|
* example: "PUBLIC"
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: Event created successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Event with id 123 was created successfully.
|
||||||
|
* eventId:
|
||||||
|
* type: integer
|
||||||
|
* example: 123
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - missing or invalid parameters
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Required parameters missing
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden - no access to create events
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: You do not have access to the specified calendar.
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
eventsRouter.post('/', async (req: Request, res: Response) => {
|
eventsRouter.post('/', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Get params
|
// Get params
|
||||||
@@ -134,7 +531,7 @@ eventsRouter.post('/', async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||||
|
|
||||||
if (!user.isActive) {
|
if (!user?.isActive) {
|
||||||
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -162,6 +559,7 @@ eventsRouter.post('/', async (req: Request, res: Response) => {
|
|||||||
createdById: user.userId ?? -1,
|
createdById: user.userId ?? -1,
|
||||||
url: req.body.url ?? '',
|
url: req.body.url ?? '',
|
||||||
wholeDay: req.body.wholeDay ?? false,
|
wholeDay: req.body.wholeDay ?? false,
|
||||||
|
repeatFrequency: req.body.repeatFrequency ?? '',
|
||||||
status: req.body.status ?? 'PUBLIC'
|
status: req.body.status ?? 'PUBLIC'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,6 +580,126 @@ eventsRouter.post('/', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/events/{eventId}:
|
||||||
|
* put:
|
||||||
|
* summary: Update an existing event
|
||||||
|
* description: Updates an existing event with the provided data. Authentication required.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: eventId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: The ID of the event to update
|
||||||
|
* - in: query
|
||||||
|
* name: sessionId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session ID for authentication
|
||||||
|
* - in: query
|
||||||
|
* name: sessionKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session key for authentication
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - calendarId
|
||||||
|
* - name
|
||||||
|
* - startDateTime
|
||||||
|
* - endDateTime
|
||||||
|
* properties:
|
||||||
|
* calendarId:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* example: "Updated Concert at Musikhochschule"
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* example: "Updated annual concert at the Musikhochschule"
|
||||||
|
* startDateTime:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* example: "2023-06-15T19:00:00.000Z"
|
||||||
|
* endDateTime:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* example: "2023-06-15T21:00:00.000Z"
|
||||||
|
* location:
|
||||||
|
* type: string
|
||||||
|
* example: "Musikhochschule, Karlsruhe"
|
||||||
|
* createdBy:
|
||||||
|
* type: string
|
||||||
|
* example: "John Doe"
|
||||||
|
* url:
|
||||||
|
* type: string
|
||||||
|
* example: "https://www.nachklang.art/events/concert"
|
||||||
|
* wholeDay:
|
||||||
|
* type: boolean
|
||||||
|
* example: false
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* enum: [PUBLIC, PRIVATE, DRAFT, DELETED]
|
||||||
|
* example: "PUBLIC"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Event updated successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Event was successfully updated
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - missing or invalid parameters
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Required parameters missing
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden - no access to update events
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: You do not have access to the specified calendar.
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
eventsRouter.put('/:eventId', async (req: Request, res: Response) => {
|
eventsRouter.put('/:eventId', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Get params
|
// Get params
|
||||||
@@ -191,7 +709,7 @@ eventsRouter.put('/:eventId', async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||||
|
|
||||||
if (!user.isActive) {
|
if (!user?.isActive) {
|
||||||
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -221,6 +739,7 @@ eventsRouter.put('/:eventId', async (req: Request, res: Response) => {
|
|||||||
createdById: user.userId ?? -1,
|
createdById: user.userId ?? -1,
|
||||||
url: req.body.url ?? '',
|
url: req.body.url ?? '',
|
||||||
wholeDay: req.body.wholeDay ?? false,
|
wholeDay: req.body.wholeDay ?? false,
|
||||||
|
repeatFrequency: req.body.repeatFrequency ?? '',
|
||||||
status: req.body.status ?? 'PUBLIC'
|
status: req.body.status ?? 'PUBLIC'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -244,6 +763,257 @@ eventsRouter.put('/:eventId', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/events/move/{eventId}:
|
||||||
|
* put:
|
||||||
|
* summary: Move an event to a different calendar
|
||||||
|
* description: Moves an existing event to a different calendar. Authentication required.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: eventId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: The ID of the event to move
|
||||||
|
* - in: query
|
||||||
|
* name: sessionId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session ID for authentication
|
||||||
|
* - in: query
|
||||||
|
* name: sessionKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session key for authentication
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - calendarId
|
||||||
|
* properties:
|
||||||
|
* calendarId:
|
||||||
|
* type: integer
|
||||||
|
* example: 2
|
||||||
|
* description: The ID of the calendar to move the event to
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* example: "Concert at Musikhochschule"
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* example: "Annual concert at the Musikhochschule"
|
||||||
|
* startDateTime:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* example: "2023-06-15T19:00:00.000Z"
|
||||||
|
* endDateTime:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* example: "2023-06-15T21:00:00.000Z"
|
||||||
|
* location:
|
||||||
|
* type: string
|
||||||
|
* example: "Musikhochschule, Karlsruhe"
|
||||||
|
* createdBy:
|
||||||
|
* type: string
|
||||||
|
* example: "John Doe"
|
||||||
|
* url:
|
||||||
|
* type: string
|
||||||
|
* example: "https://www.nachklang.art/events/concert"
|
||||||
|
* wholeDay:
|
||||||
|
* type: boolean
|
||||||
|
* example: false
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* enum: [PUBLIC, PRIVATE, DRAFT, DELETED]
|
||||||
|
* example: "PUBLIC"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Event moved successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Event was successfully moved
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - missing or invalid parameters
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Required parameters missing
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden - no access to move events
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: You do not have access to the specified calendar.
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
|
eventsRouter.put('/move/:eventId', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
// Get params
|
||||||
|
let sessionId: string = req.query.sessionId as string ?? '';
|
||||||
|
let sessionKey: string = req.query.sessionKey as string ?? '';
|
||||||
|
let ip: string = req.socket.remoteAddress ?? '';
|
||||||
|
|
||||||
|
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||||
|
|
||||||
|
if (!user?.isActive) {
|
||||||
|
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.params.eventId === undefined ||
|
||||||
|
req.body.calendarId === undefined
|
||||||
|
) {
|
||||||
|
res.status(400).send({'message': 'Required parameters missing'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let event: Event = {
|
||||||
|
eventId: parseInt(req.params.eventId, 10),
|
||||||
|
calendarId: req.body.calendarId,
|
||||||
|
uuid: '',
|
||||||
|
name: req.body.name,
|
||||||
|
description: req.body.description ?? '',
|
||||||
|
startDateTime: new Date(req.body.startDateTime),
|
||||||
|
endDateTime: new Date(req.body.endDateTime),
|
||||||
|
createdDate: new Date(),
|
||||||
|
location: req.body.location ?? '',
|
||||||
|
createdBy: req.body.createdBy ?? '',
|
||||||
|
createdById: user.userId ?? -1,
|
||||||
|
url: req.body.url ?? '',
|
||||||
|
wholeDay: req.body.wholeDay ?? false,
|
||||||
|
repeatFrequency: req.body.repeatFrequency ?? '',
|
||||||
|
status: req.body.status ?? 'PUBLIC'
|
||||||
|
};
|
||||||
|
|
||||||
|
let success = await EventService.moveEvent(event);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
res.status(200).send({'message': 'Event was successfully moved'});
|
||||||
|
} else {
|
||||||
|
res.status(500).send({'message': 'An error occurred during the moving process. Please try again.'});
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
let errorGuid = Guid.create().toString();
|
||||||
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
||||||
|
res.status(500).send({
|
||||||
|
'status': 'PROCESSING_ERROR',
|
||||||
|
'message': 'Internal Server Error. Try again later.',
|
||||||
|
'reference': errorGuid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/events/{eventId}:
|
||||||
|
* delete:
|
||||||
|
* summary: Delete an event
|
||||||
|
* description: Deletes an existing event. Authentication required.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: eventId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: The ID of the event to delete
|
||||||
|
* - in: query
|
||||||
|
* name: sessionId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session ID for authentication
|
||||||
|
* - in: query
|
||||||
|
* name: sessionKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Session key for authentication
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Event deleted successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Event was successfully deleted
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - missing or invalid parameters
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Required parameters missing
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden - no access to delete events
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: You do not have access to the specified calendar.
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
eventsRouter.delete('/:eventId', async (req: Request, res: Response) => {
|
eventsRouter.delete('/:eventId', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Get params
|
// Get params
|
||||||
@@ -253,7 +1023,7 @@ eventsRouter.delete('/:eventId', async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||||
|
|
||||||
if (!user.isActive) {
|
if (!user?.isActive) {
|
||||||
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -279,6 +1049,7 @@ eventsRouter.delete('/:eventId', async (req: Request, res: Response) => {
|
|||||||
createdById: user.userId ?? -1,
|
createdById: user.userId ?? -1,
|
||||||
url: '',
|
url: '',
|
||||||
wholeDay: false,
|
wholeDay: false,
|
||||||
|
repeatFrequency: '',
|
||||||
status: 'DELETED'
|
status: 'DELETED'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,16 @@ export const getAllEvents = async (calendarId: number): Promise<Event[]> => {
|
|||||||
let conn = await NachklangCalendarDB.getConnection();
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
let eventRows: Event[] = [];
|
let eventRows: Event[] = [];
|
||||||
try {
|
try {
|
||||||
|
const calendarQuery = 'SELECT calendar_id, includes_calendars FROM calendars WHERE calendar_id = ?';
|
||||||
|
const calendarRes = await conn.query(calendarQuery, calendarId);
|
||||||
|
let calendarsToFetch: number[] = [calendarId];
|
||||||
|
for(let row of calendarRes) {
|
||||||
|
let includes: number[] = JSON.parse(row.includes_calendars);
|
||||||
|
calendarsToFetch = [...calendarsToFetch, ...includes];
|
||||||
|
}
|
||||||
|
|
||||||
const eventsQuery = `
|
const eventsQuery = `
|
||||||
SELECT e.calendar_id, u.full_name, v.* FROM events e
|
SELECT e.calendar_id, e.uuid, e.created_date, e.created_by_id, u.full_name as created_by_name, u2.full_name as last_modified_by_name, v.* FROM events e
|
||||||
INNER JOIN (
|
INNER JOIN (
|
||||||
SELECT event_id, MAX(event_version_id) AS latest_version
|
SELECT event_id, MAX(event_version_id) AS latest_version
|
||||||
FROM event_versions
|
FROM event_versions
|
||||||
@@ -25,9 +33,10 @@ export const getAllEvents = async (calendarId: number): Promise<Event[]> => {
|
|||||||
INNER JOIN event_versions v
|
INNER JOIN event_versions v
|
||||||
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
||||||
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
||||||
WHERE e.calendar_id = ? AND v.status = 'PUBLIC'
|
LEFT OUTER JOIN users u2 ON u2.user_id = v.version_created_by_id
|
||||||
|
WHERE e.calendar_id IN (?) AND v.status = 'PUBLIC'
|
||||||
ORDER BY e.event_id`;
|
ORDER BY e.event_id`;
|
||||||
const eventsRes = await conn.query(eventsQuery, calendarId);
|
const eventsRes = await conn.query(eventsQuery, [calendarsToFetch]);
|
||||||
|
|
||||||
for (let row of eventsRes) {
|
for (let row of eventsRes) {
|
||||||
eventRows.push({
|
eventRows.push({
|
||||||
@@ -39,11 +48,15 @@ export const getAllEvents = async (calendarId: number): Promise<Event[]> => {
|
|||||||
startDateTime: row.start_datetime,
|
startDateTime: row.start_datetime,
|
||||||
endDateTime: row.end_datetime,
|
endDateTime: row.end_datetime,
|
||||||
createdDate: row.created_date,
|
createdDate: row.created_date,
|
||||||
|
lastModifiedDate: row.version_created_at,
|
||||||
location: row.location,
|
location: row.location,
|
||||||
createdBy: row.full_name,
|
createdBy: row.created_by_name,
|
||||||
createdById: row.created_by_id,
|
createdById: row.created_by_id,
|
||||||
|
lastModifiedBy: row.last_modified_by_name,
|
||||||
|
lastModifiedById: row.version_created_by_id,
|
||||||
url: row.url,
|
url: row.url,
|
||||||
wholeDay: row.whole_day
|
wholeDay: row.whole_day,
|
||||||
|
repeatFrequency: row.repeat_frequency
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,12 +69,17 @@ export const getAllEvents = async (calendarId: number): Promise<Event[]> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all events for the given calendar for the admin UI (therefore includes admin relevant information and
|
||||||
|
* ignores the calendar includes
|
||||||
|
* @param calendarId
|
||||||
|
*/
|
||||||
export const getAllEventsAdmin = async (calendarId: number): Promise<Event[]> => {
|
export const getAllEventsAdmin = async (calendarId: number): Promise<Event[]> => {
|
||||||
let conn = await NachklangCalendarDB.getConnection();
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
let eventRows: Event[] = [];
|
let eventRows: Event[] = [];
|
||||||
try {
|
try {
|
||||||
const eventsQuery = `
|
const eventsQuery = `
|
||||||
SELECT e.calendar_id, u.full_name, v.* FROM events e
|
SELECT e.calendar_id, e.uuid, e.created_date, e.created_by_id, u.full_name as created_by_name, u2.full_name as last_modified_by_name, v.* FROM events e
|
||||||
INNER JOIN (
|
INNER JOIN (
|
||||||
SELECT event_id, MAX(event_version_id) AS latest_version
|
SELECT event_id, MAX(event_version_id) AS latest_version
|
||||||
FROM event_versions
|
FROM event_versions
|
||||||
@@ -71,6 +89,7 @@ export const getAllEventsAdmin = async (calendarId: number): Promise<Event[]> =>
|
|||||||
INNER JOIN event_versions v
|
INNER JOIN event_versions v
|
||||||
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
||||||
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
||||||
|
LEFT OUTER JOIN users u2 ON u2.user_id = v.version_created_by_id
|
||||||
WHERE e.calendar_id = ?
|
WHERE e.calendar_id = ?
|
||||||
ORDER BY e.event_id`;
|
ORDER BY e.event_id`;
|
||||||
const eventsRes = await conn.query(eventsQuery, calendarId);
|
const eventsRes = await conn.query(eventsQuery, calendarId);
|
||||||
@@ -85,11 +104,15 @@ export const getAllEventsAdmin = async (calendarId: number): Promise<Event[]> =>
|
|||||||
startDateTime: row.start_datetime,
|
startDateTime: row.start_datetime,
|
||||||
endDateTime: row.end_datetime,
|
endDateTime: row.end_datetime,
|
||||||
createdDate: row.created_date,
|
createdDate: row.created_date,
|
||||||
|
lastModifiedDate: row.version_created_at,
|
||||||
location: row.location,
|
location: row.location,
|
||||||
createdBy: row.full_name,
|
createdBy: row.created_by_name,
|
||||||
createdById: row.created_by_id,
|
createdById: row.created_by_id,
|
||||||
|
lastModifiedBy: row.last_modified_by_name,
|
||||||
|
lastModifiedById: row.version_created_by_id,
|
||||||
url: row.url,
|
url: row.url,
|
||||||
wholeDay: row.whole_day,
|
wholeDay: row.whole_day,
|
||||||
|
repeatFrequency: row.repeat_frequency,
|
||||||
status: row.status
|
status: row.status
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,12 +133,13 @@ export const getAllEventsAdmin = async (calendarId: number): Promise<Event[]> =>
|
|||||||
export const createEvent = async (event: Event): Promise<number> => {
|
export const createEvent = async (event: Event): Promise<number> => {
|
||||||
let conn = await NachklangCalendarDB.getConnection();
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
try {
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
let eventUUID = Guid.create().toString();
|
let eventUUID = Guid.create().toString();
|
||||||
const eventsQuery = 'INSERT INTO events (calendar_id, uuid, created_by_id) VALUES (?,?,?) RETURNING event_id';
|
const eventsQuery = 'INSERT INTO events (calendar_id, uuid, created_by_id) VALUES (?,?,?) RETURNING event_id';
|
||||||
const eventsRes = await conn.execute(eventsQuery, [event.calendarId, eventUUID, event.createdById]);
|
const eventsRes = await conn.execute(eventsQuery, [event.calendarId, eventUUID, event.createdById]);
|
||||||
|
|
||||||
const versionQuery = 'INSERT INTO event_versions (event_id, name, description, start_datetime, end_datetime, whole_day, location, url, status, version_created_by_id) VALUES (?,?,?,?,?,?,?,?,?,?);'
|
const versionQuery = 'INSERT INTO event_versions (event_id, name, description, start_datetime, end_datetime, whole_day, repeat_frequency, location, url, status, version_created_by_id) VALUES (?,?,?,?,?,?,?,?,?,?,?);'
|
||||||
const versionRes = await conn.execute(versionQuery, [eventsRes[0].event_id, event.name, event.description, event.startDateTime, event.endDateTime, event.wholeDay, event.location, event.url, event.status, event.createdById]);
|
await conn.execute(versionQuery, [eventsRes[0].event_id, event.name, event.description, event.startDateTime, event.endDateTime, event.wholeDay, event.repeatFrequency, event.location, event.url, event.status, event.createdById]);
|
||||||
|
|
||||||
await conn.commit();
|
await conn.commit();
|
||||||
|
|
||||||
@@ -124,8 +148,6 @@ export const createEvent = async (event: Event): Promise<number> => {
|
|||||||
await conn.rollback();
|
await conn.rollback();
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
// Return connection
|
|
||||||
await conn.commit();
|
|
||||||
await conn.end();
|
await conn.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -137,8 +159,9 @@ export const createEvent = async (event: Event): Promise<number> => {
|
|||||||
export const updateEvent = async (event: Event): Promise<number> => {
|
export const updateEvent = async (event: Event): Promise<number> => {
|
||||||
let conn = await NachklangCalendarDB.getConnection();
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
try {
|
try {
|
||||||
const versionQuery = 'INSERT INTO event_versions (event_id, name, description, start_datetime, end_datetime, whole_day, location, url, status, version_created_by_id) VALUES (?,?,?,?,?,?,?,?,?,?);'
|
await conn.beginTransaction();
|
||||||
const versionRes = await conn.execute(versionQuery, [event.eventId, event.name, event.description, event.startDateTime, event.endDateTime, event.wholeDay, event.location, event.url, event.status, event.createdById]);
|
const versionQuery = 'INSERT INTO event_versions (event_id, name, description, start_datetime, end_datetime, whole_day, repeat_frequency, location, url, status, version_created_by_id) VALUES (?,?,?,?,?,?,?,?,?,?,?);'
|
||||||
|
const versionRes = await conn.execute(versionQuery, [event.eventId, event.name, event.description, event.startDateTime, event.endDateTime, event.wholeDay, event.repeatFrequency, event.location, event.url, event.status, event.createdById]);
|
||||||
|
|
||||||
await conn.commit();
|
await conn.commit();
|
||||||
|
|
||||||
@@ -147,8 +170,6 @@ export const updateEvent = async (event: Event): Promise<number> => {
|
|||||||
await conn.rollback();
|
await conn.rollback();
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
// Return connection
|
|
||||||
await conn.commit();
|
|
||||||
await conn.end();
|
await conn.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -160,6 +181,7 @@ export const updateEvent = async (event: Event): Promise<number> => {
|
|||||||
export const deleteEvent = async (event: Event): Promise<boolean> => {
|
export const deleteEvent = async (event: Event): Promise<boolean> => {
|
||||||
let conn = await NachklangCalendarDB.getConnection();
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
try {
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
const versionQuery = 'INSERT INTO event_versions (event_id, status, version_created_by_id) VALUES (?,?,?);'
|
const versionQuery = 'INSERT INTO event_versions (event_id, status, version_created_by_id) VALUES (?,?,?);'
|
||||||
const versionRes = await conn.execute(versionQuery, [event.eventId, 'DELETED', event.createdById]);
|
const versionRes = await conn.execute(versionQuery, [event.eventId, 'DELETED', event.createdById]);
|
||||||
|
|
||||||
@@ -170,8 +192,93 @@ export const deleteEvent = async (event: Event): Promise<boolean> => {
|
|||||||
await conn.rollback();
|
await conn.rollback();
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
// Return connection
|
|
||||||
await conn.commit();
|
|
||||||
await conn.end();
|
await conn.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves an event to the specified calendar
|
||||||
|
* @param event The event to move. Has to have the target calendar set already.
|
||||||
|
*/
|
||||||
|
export const moveEvent = async (event: Event): Promise<boolean> => {
|
||||||
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
const eventQuery = 'UPDATE events SET calendar_id = ? WHERE event_id = ?';
|
||||||
|
const eventRes = await conn.execute(eventQuery, [event.calendarId, event.eventId]);
|
||||||
|
|
||||||
|
await conn.commit();
|
||||||
|
|
||||||
|
return eventRes.affectedRows === 1;
|
||||||
|
} catch (err) {
|
||||||
|
await conn.rollback();
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
await conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next upcoming event for the given calendar
|
||||||
|
* @param calendarId The calendar Id
|
||||||
|
*/
|
||||||
|
export const getNextUpcomingEvent = async (calendarId: number): Promise<Event | null> => {
|
||||||
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
|
try {
|
||||||
|
const calendarQuery = 'SELECT calendar_id, includes_calendars FROM calendars WHERE calendar_id = ?';
|
||||||
|
const calendarRes = await conn.query(calendarQuery, calendarId);
|
||||||
|
let calendarsToFetch: number[] = [calendarId];
|
||||||
|
for(let row of calendarRes) {
|
||||||
|
let includes: number[] = JSON.parse(row.includes_calendars);
|
||||||
|
calendarsToFetch = [...calendarsToFetch, ...includes];
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const eventsQuery = `
|
||||||
|
SELECT e.calendar_id, e.uuid, e.created_date, e.created_by_id, u.full_name as created_by_name, u2.full_name as last_modified_by_name, v.* FROM events e
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT event_id, MAX(event_version_id) AS latest_version
|
||||||
|
FROM event_versions
|
||||||
|
GROUP BY event_id
|
||||||
|
) latest_versions
|
||||||
|
ON e.event_id = latest_versions.event_id
|
||||||
|
INNER JOIN event_versions v
|
||||||
|
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
||||||
|
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
||||||
|
LEFT OUTER JOIN users u2 ON u2.user_id = v.version_created_by_id
|
||||||
|
WHERE e.calendar_id IN (?) AND v.status = 'PUBLIC' AND v.start_datetime > ?
|
||||||
|
ORDER BY v.start_datetime ASC
|
||||||
|
LIMIT 1`;
|
||||||
|
const eventsRes = await conn.query(eventsQuery, [calendarsToFetch, now]);
|
||||||
|
|
||||||
|
if (eventsRes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = eventsRes[0];
|
||||||
|
return {
|
||||||
|
eventId: row.event_id,
|
||||||
|
calendarId: row.calendar_id,
|
||||||
|
uuid: row.uuid,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
startDateTime: row.start_datetime,
|
||||||
|
endDateTime: row.end_datetime,
|
||||||
|
createdDate: row.created_date,
|
||||||
|
lastModifiedDate: row.version_created_at,
|
||||||
|
location: row.location,
|
||||||
|
createdBy: row.created_by_name,
|
||||||
|
createdById: row.created_by_id,
|
||||||
|
lastModifiedBy: row.last_modified_by_name,
|
||||||
|
lastModifiedById: row.version_created_by_id,
|
||||||
|
url: row.url,
|
||||||
|
wholeDay: row.whole_day,
|
||||||
|
repeatFrequency: row.repeat_frequency
|
||||||
|
} as Event;
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
// Return connection
|
||||||
|
await conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import {Event} from './event.interface';
|
import {Event} from './event.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to external classes - Turns the given events into an ical string
|
||||||
|
* @param events
|
||||||
|
*/
|
||||||
export const convertToIcal = async (events: Event[]): Promise<string> => {
|
export const convertToIcal = async (events: Event[]): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
let ical: iCalFile = {body: []};
|
let ical: iCalFile = {body: []};
|
||||||
@@ -15,6 +19,10 @@ export const convertToIcal = async (events: Event[]): Promise<string> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to serialize an iCalFile object into an ical string
|
||||||
|
* @param ical
|
||||||
|
*/
|
||||||
const serializeIcalFile = (ical: iCalFile): string => {
|
const serializeIcalFile = (ical: iCalFile): string => {
|
||||||
let returnString = '';
|
let returnString = '';
|
||||||
|
|
||||||
@@ -27,6 +35,10 @@ const serializeIcalFile = (ical: iCalFile): string => {
|
|||||||
return returnString;
|
return returnString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to serialize a single ical event into an ical event string
|
||||||
|
* @param icalevent
|
||||||
|
*/
|
||||||
const serializeIcalEvent = (icalevent: iCalEvent): string => {
|
const serializeIcalEvent = (icalevent: iCalEvent): string => {
|
||||||
let returnString = '';
|
let returnString = '';
|
||||||
|
|
||||||
@@ -41,6 +53,7 @@ const serializeIcalEvent = (icalevent: iCalEvent): string => {
|
|||||||
returnString += 'DTSTART;TZID=Europe/Berlin:' + icalevent.start;
|
returnString += 'DTSTART;TZID=Europe/Berlin:' + icalevent.start;
|
||||||
returnString += 'DTEND;TZID=Europe/Berlin:' + icalevent.end;
|
returnString += 'DTEND;TZID=Europe/Berlin:' + icalevent.end;
|
||||||
}
|
}
|
||||||
|
if(!isNullOrBlank(icalevent.repeatFrequency)) returnString += 'RRULE:FREQ=' + icalevent.repeatFrequency;
|
||||||
returnString += 'SUMMARY:' + icalevent.summary;
|
returnString += 'SUMMARY:' + icalevent.summary;
|
||||||
if(!isNullOrBlank(icalevent.description)) returnString += 'DESCRIPTION:' + icalevent.description;
|
if(!isNullOrBlank(icalevent.description)) returnString += 'DESCRIPTION:' + icalevent.description;
|
||||||
if(!isNullOrBlank(icalevent.location)) returnString += 'LOCATION:' + icalevent.location;
|
if(!isNullOrBlank(icalevent.location)) returnString += 'LOCATION:' + icalevent.location;
|
||||||
@@ -50,7 +63,10 @@ const serializeIcalEvent = (icalevent: iCalEvent): string => {
|
|||||||
return returnString;
|
return returnString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to generate the ical header string
|
||||||
|
* @param ical
|
||||||
|
*/
|
||||||
const generateHeaderInfo = (ical: iCalFile) => {
|
const generateHeaderInfo = (ical: iCalFile) => {
|
||||||
ical.header = 'BEGIN:VCALENDAR\n' +
|
ical.header = 'BEGIN:VCALENDAR\n' +
|
||||||
'VERSION:2.0\n' +
|
'VERSION:2.0\n' +
|
||||||
@@ -78,14 +94,27 @@ const generateHeaderInfo = (ical: iCalFile) => {
|
|||||||
'END:VTIMEZONE\n';
|
'END:VTIMEZONE\n';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to generate the ical footer info
|
||||||
|
* @param ical
|
||||||
|
*/
|
||||||
const generateFooterInfo = (ical: iCalFile) => {
|
const generateFooterInfo = (ical: iCalFile) => {
|
||||||
ical.footer = 'END:VCALENDAR';
|
ical.footer = 'END:VCALENDAR';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to add events to the iCalFile object
|
||||||
|
* @param ical
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
const addEventToFile = (ical: iCalFile, event: Event) => {
|
const addEventToFile = (ical: iCalFile, event: Event) => {
|
||||||
ical.body.push(createIcalEvent(event));
|
ical.body.push(createIcalEvent(event));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to turn an event object into an iCalEvent object
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
const createIcalEvent = (event: Event): iCalEvent => {
|
const createIcalEvent = (event: Event): iCalEvent => {
|
||||||
let description = event.description ? event.description + '\n' : '';
|
let description = event.description ? event.description + '\n' : '';
|
||||||
let location = event.location ? event.location + '\n' : '';
|
let location = event.location ? event.location + '\n' : '';
|
||||||
@@ -98,6 +127,7 @@ const createIcalEvent = (event: Event): iCalEvent => {
|
|||||||
organizer: event.createdBy + '\n',
|
organizer: event.createdBy + '\n',
|
||||||
start: formatDate(event.startDateTime, event.wholeDay) + '\n',
|
start: formatDate(event.startDateTime, event.wholeDay) + '\n',
|
||||||
end: formatDate(event.endDateTime, event.wholeDay, true) + '\n',
|
end: formatDate(event.endDateTime, event.wholeDay, true) + '\n',
|
||||||
|
repeatFrequency: event.repeatFrequency ? event.repeatFrequency + '\n' : '',
|
||||||
summary: event.name + '\n',
|
summary: event.name + '\n',
|
||||||
description: description,
|
description: description,
|
||||||
location: location,
|
location: location,
|
||||||
@@ -107,6 +137,12 @@ const createIcalEvent = (event: Event): iCalEvent => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to format dates in a valid iCal format
|
||||||
|
* @param date
|
||||||
|
* @param wholeDayFormat
|
||||||
|
* @param isEndDate
|
||||||
|
*/
|
||||||
const formatDate = (date: Date, wholeDayFormat: boolean = false, isEndDate: boolean = false): string => {
|
const formatDate = (date: Date, wholeDayFormat: boolean = false, isEndDate: boolean = false): string => {
|
||||||
let returnString = '';
|
let returnString = '';
|
||||||
|
|
||||||
@@ -144,6 +180,7 @@ export interface iCalEvent {
|
|||||||
location: string;
|
location: string;
|
||||||
url: string;
|
url: string;
|
||||||
wholeDay: boolean;
|
wholeDay: boolean;
|
||||||
|
repeatFrequency: string;
|
||||||
footer: string;
|
footer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* Session:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - sessionId
|
||||||
|
* - userId
|
||||||
|
* - sessionKey
|
||||||
|
* - sessionKeyHash
|
||||||
|
* - lastIP
|
||||||
|
* properties:
|
||||||
|
* sessionId:
|
||||||
|
* type: integer
|
||||||
|
* description: The unique identifier for the session
|
||||||
|
* example: 789
|
||||||
|
* userId:
|
||||||
|
* type: integer
|
||||||
|
* description: The ID of the user this session belongs to
|
||||||
|
* example: 456
|
||||||
|
* sessionKey:
|
||||||
|
* type: string
|
||||||
|
* description: The session key used for authentication
|
||||||
|
* example: "abc123def456"
|
||||||
|
* sessionKeyHash:
|
||||||
|
* type: string
|
||||||
|
* description: The hashed session key (not returned in API responses)
|
||||||
|
* example: "$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG"
|
||||||
|
* createdDate:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: The date and time when the session was created
|
||||||
|
* example: "2023-05-01T10:00:00.000Z"
|
||||||
|
* validUntil:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: The date and time until when the session is valid
|
||||||
|
* example: "2023-05-08T10:00:00.000Z"
|
||||||
|
* lastIP:
|
||||||
|
* type: string
|
||||||
|
* description: The last IP address used with this session
|
||||||
|
* example: "192.168.1.1"
|
||||||
|
*/
|
||||||
export interface Session {
|
export interface Session {
|
||||||
sessionId: number;
|
sessionId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
|
|||||||
@@ -1,3 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* User:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - userId
|
||||||
|
* - fullName
|
||||||
|
* - passwordHash
|
||||||
|
* - email
|
||||||
|
* - isActive
|
||||||
|
* properties:
|
||||||
|
* userId:
|
||||||
|
* type: integer
|
||||||
|
* description: The unique identifier for the user
|
||||||
|
* example: 456
|
||||||
|
* fullName:
|
||||||
|
* type: string
|
||||||
|
* description: The full name of the user
|
||||||
|
* example: "John Doe"
|
||||||
|
* passwordHash:
|
||||||
|
* type: string
|
||||||
|
* description: The hashed password of the user (not returned in API responses)
|
||||||
|
* example: "$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG"
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* description: The email address of the user
|
||||||
|
* example: "john.doe@nachklang.art"
|
||||||
|
* isActive:
|
||||||
|
* type: boolean
|
||||||
|
* description: Whether the user account is active
|
||||||
|
* example: true
|
||||||
|
*/
|
||||||
export interface User {
|
export interface User {
|
||||||
userId: number;
|
userId: number;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
|
|||||||
@@ -20,6 +20,78 @@ export const usersRouter = express.Router();
|
|||||||
* Controller Definitions
|
* Controller Definitions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/users/register:
|
||||||
|
* post:
|
||||||
|
* summary: Register a new user
|
||||||
|
* description: Creates a new user account with the provided email, password, and full name. Only accepts official Nachklang email addresses.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - email
|
||||||
|
* - password
|
||||||
|
* - fullName
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* example: john.doe@nachklang.art
|
||||||
|
* description: Must be an official Nachklang email address
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* format: password
|
||||||
|
* example: securePassword123
|
||||||
|
* fullName:
|
||||||
|
* type: string
|
||||||
|
* example: John Doe
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: User registered successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* sessionId:
|
||||||
|
* type: integer
|
||||||
|
* example: 123
|
||||||
|
* sessionKey:
|
||||||
|
* type: string
|
||||||
|
* example: abc123def456
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - missing or invalid parameters
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Missing parameters
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
// POST users/register
|
// POST users/register
|
||||||
usersRouter.post('/register', async (req: Request, res: Response) => {
|
usersRouter.post('/register', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
@@ -28,12 +100,19 @@ usersRouter.post('/register', async (req: Request, res: Response) => {
|
|||||||
const fullName: string = req.body.fullName;
|
const fullName: string = req.body.fullName;
|
||||||
const ip: string = req.socket.remoteAddress ?? '';
|
const ip: string = req.socket.remoteAddress ?? '';
|
||||||
|
|
||||||
if (!password || !email) {
|
if (!password || !email || !fullName) {
|
||||||
// Missing
|
// Missing
|
||||||
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emailRegex = /^[a-zA-Z0-9\_\-\.]+@nachklang\.art$/;
|
||||||
|
|
||||||
|
if(!emailRegex.test(email)) {
|
||||||
|
res.status(400).send(JSON.stringify({message: 'Must use an official Nachklang email address'}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create the user and a session
|
// Create the user and a session
|
||||||
const session: Session = await UserService.createUser(email, password, fullName, ip);
|
const session: Session = await UserService.createUser(email, password, fullName, ip);
|
||||||
|
|
||||||
@@ -53,6 +132,189 @@ usersRouter.post('/register', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/users/activate:
|
||||||
|
* get:
|
||||||
|
* summary: Activate a user account
|
||||||
|
* description: Activates a user account using the provided user ID and activation token.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: The ID of the user to activate
|
||||||
|
* - in: query
|
||||||
|
* name: token
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: The activation token sent to the user's email
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: User activated successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: OK
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: User activated
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - missing parameters or activation failed
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Error activating user. Please contact your administrator.
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
|
// GET /users/activate
|
||||||
|
usersRouter.get('/activate', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const userId: number = parseInt(req.query.id as string ?? '-1', 10);
|
||||||
|
const token: string = req.query.token as string ?? '';
|
||||||
|
|
||||||
|
if (!userId || !token) {
|
||||||
|
// Missing
|
||||||
|
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the user and a session
|
||||||
|
const success: boolean = await UserService.activateUser(userId, token);
|
||||||
|
|
||||||
|
// Send the session details back to the user
|
||||||
|
if(success) {
|
||||||
|
res.status(200).send({
|
||||||
|
'status': 'OK',
|
||||||
|
'message': 'User activated'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(400).send({'status': 'PROCESSING_ERROR','message': 'Error activating user. Please contact your administrator.'});
|
||||||
|
} catch (e: any) {
|
||||||
|
let errorGuid = Guid.create().toString();
|
||||||
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
||||||
|
res.status(500).send({
|
||||||
|
'status': 'PROCESSING_ERROR',
|
||||||
|
'message': 'Internal Server Error. Try again later.',
|
||||||
|
'reference': errorGuid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/users/login:
|
||||||
|
* post:
|
||||||
|
* summary: Login a user
|
||||||
|
* description: Authenticates a user with the provided email and password and returns a session.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - email
|
||||||
|
* - password
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* example: john.doe@nachklang.art
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* format: password
|
||||||
|
* example: securePassword123
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Login successful
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* sessionId:
|
||||||
|
* type: integer
|
||||||
|
* example: 123
|
||||||
|
* sessionKey:
|
||||||
|
* type: string
|
||||||
|
* example: abc123def456
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - missing parameters
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Missing parameters
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized - invalid credentials
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Wrong username and / or password
|
||||||
|
* sessionId:
|
||||||
|
* type: integer
|
||||||
|
* example: -1
|
||||||
|
* sessionKey:
|
||||||
|
* type: string
|
||||||
|
* example: ""
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
// POST users/login
|
// POST users/login
|
||||||
usersRouter.post('/login', async (req: Request, res: Response) => {
|
usersRouter.post('/login', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
@@ -67,9 +329,9 @@ usersRouter.post('/login', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a session
|
// Create a session
|
||||||
const session: Session = await UserService.login(email, password, ip);
|
const session: Session | null = await UserService.login(email, password, ip);
|
||||||
|
|
||||||
if (!session.sessionId) {
|
if (!session || !session.sessionId) {
|
||||||
// Error logging in, probably wrong username / password
|
// Error logging in, probably wrong username / password
|
||||||
res.status(401).send(JSON.stringify({message: 'Wrong username and / or password', sessionId: -1, sessionKey: ''}));
|
res.status(401).send(JSON.stringify({message: 'Wrong username and / or password', sessionId: -1, sessionKey: ''}));
|
||||||
return;
|
return;
|
||||||
@@ -91,6 +353,66 @@ usersRouter.post('/login', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/users/checkSessionValid:
|
||||||
|
* post:
|
||||||
|
* summary: Check if a session is valid
|
||||||
|
* description: Checks if the provided session is valid and returns the user information if it is.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - sessionId
|
||||||
|
* - sessionKey
|
||||||
|
* properties:
|
||||||
|
* sessionId:
|
||||||
|
* type: integer
|
||||||
|
* example: 123
|
||||||
|
* sessionKey:
|
||||||
|
* type: string
|
||||||
|
* example: abc123def456
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Session is valid
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/User'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized - invalid session
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: ["Invalid session"]
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
*/
|
||||||
// POST users/checkSessionValid
|
// POST users/checkSessionValid
|
||||||
usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
|
usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
@@ -104,9 +426,9 @@ usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user: User = await UserService.checkSession(session_id, session_key, ip);
|
const user: User | null = await UserService.checkSession(session_id, session_key, ip);
|
||||||
|
|
||||||
if (!user.userId) {
|
if (!user || !user.userId) {
|
||||||
// Error logging in, probably wrong username / password
|
// Error logging in, probably wrong username / password
|
||||||
res.status(401).send(JSON.stringify({messages: ['Invalid session']}));
|
res.status(401).send(JSON.stringify({messages: ['Invalid session']}));
|
||||||
return;
|
return;
|
||||||
@@ -123,3 +445,227 @@ usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/users/initiatePasswordReset:
|
||||||
|
* post:
|
||||||
|
* summary: Initiates a password reset
|
||||||
|
* description: Checks if the user exists and if so, initiates a password reset by sending an email to the user.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Success
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Success
|
||||||
|
* description: A list of status messages
|
||||||
|
* 400:
|
||||||
|
* description: Problem with the request. Please consider the returned detailed error.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Missing parameters
|
||||||
|
* description: A list of error messages
|
||||||
|
* 401:
|
||||||
|
* description: Problem with authorizing the user. Please check the provided credentials.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Invalid session
|
||||||
|
* description: A list of error messages
|
||||||
|
* 500:
|
||||||
|
* description: A server error occurred. Please try again. If this issue persists, contact the admin.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* description: The response status
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* description: The detailed error message
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* description: An error reference for getting support concerning this error.
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* example: patrick@nachklang.art
|
||||||
|
*/
|
||||||
|
usersRouter.post('/initiatePasswordReset', async(req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const username = req.body.username;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
// Error logging in, probably wrong username / password
|
||||||
|
res.status(400).send(JSON.stringify({messages: ['No username given']}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success: boolean = await UserService.initiatePasswordReset(username);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
// Error logging in, probably wrong username / password
|
||||||
|
res.status(401).send(JSON.stringify({messages: ['Error']}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(JSON.stringify({messages: ['Success']}));
|
||||||
|
} catch (e: any) {
|
||||||
|
let errorGuid = Guid.create().toString();
|
||||||
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
||||||
|
res.status(500).send({
|
||||||
|
'status': 'PROCESSING_ERROR',
|
||||||
|
'message': 'Internal Server Error. Try again later.',
|
||||||
|
'reference': errorGuid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/users/finalizePasswordReset:
|
||||||
|
* post:
|
||||||
|
* summary: Finalizes the password reset
|
||||||
|
* description: Checks if the given token is valid and if so, finalizes the password reset by setting the new password.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Success
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Success
|
||||||
|
* description: A list of status messages
|
||||||
|
* 400:
|
||||||
|
* description: Problem with the request. Please consider the returned detailed error.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Missing parameters
|
||||||
|
* description: A list of error messages
|
||||||
|
* 401:
|
||||||
|
* description: Problem with authorizing the user. Please check the provided credentials.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Invalid session
|
||||||
|
* description: A list of error messages
|
||||||
|
* 500:
|
||||||
|
* description: A server error occurred. Please try again. If this issue persists, contact the admin.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* description: The response status
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* description: The detailed error message
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* description: An error reference for getting support concerning this error.
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* example: patrick@nachklang.art
|
||||||
|
* token:
|
||||||
|
* type: string
|
||||||
|
* example: 3ccd147f-720b-4e29-a8b7-46b63de31555
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* example: ExtremelyBadPassword
|
||||||
|
*/
|
||||||
|
usersRouter.post('/finalizePasswordReset', async(req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const username = req.body.username;
|
||||||
|
const token = req.body.token;
|
||||||
|
const newPassword = req.body.password;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
// Error logging in, probably wrong username / password
|
||||||
|
res.status(400).send(JSON.stringify({messages: ['No username, token or password given']}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success: boolean = await UserService.finalizePasswordReset(username, token, newPassword);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
// Error logging in, probably wrong username / password
|
||||||
|
res.status(401).send(JSON.stringify({messages: ['Error']}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(JSON.stringify({messages: ['Success']}));
|
||||||
|
} catch (e: any) {
|
||||||
|
let errorGuid = Guid.create().toString();
|
||||||
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
||||||
|
res.status(500).send({
|
||||||
|
'status': 'PROCESSING_ERROR',
|
||||||
|
'message': 'Internal Server Error. Try again later.',
|
||||||
|
'reference': errorGuid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {Guid} from 'guid-typescript';
|
|||||||
import {User} from './user.interface';
|
import {User} from './user.interface';
|
||||||
import {Session} from './session.interface';
|
import {Session} from './session.interface';
|
||||||
import {NachklangCalendarDB} from '../Calendar.db';
|
import {NachklangCalendarDB} from '../Calendar.db';
|
||||||
|
import {MailService} from "../../../common/common.mail.nodemailer";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -22,14 +23,19 @@ dotenv.config();
|
|||||||
export const createUser = async (email: string, password: string, fullName: string, ip: string): Promise<Session> => {
|
export const createUser = async (email: string, password: string, fullName: string, ip: string): Promise<Session> => {
|
||||||
let conn = await NachklangCalendarDB.getConnection();
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
try {
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
|
||||||
// Hash password and generate + hash session key
|
// Hash password and generate + hash session key
|
||||||
const pwHash = bcrypt.hashSync(password, 10);
|
const pwHash = bcrypt.hashSync(password, 10);
|
||||||
const sessionKey = Guid.create().toString();
|
const sessionKey = Guid.create().toString();
|
||||||
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
|
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
|
||||||
|
|
||||||
|
const activationToken = Guid.create().toString();
|
||||||
|
const activationTokenHash = bcrypt.hashSync(activationToken, 10);
|
||||||
|
|
||||||
// Create user entry in SQL
|
// Create user entry in SQL
|
||||||
const userQuery = 'INSERT INTO users (email, password_hash, full_name) VALUES (?, ?, ?) RETURNING user_id';
|
const userQuery = 'INSERT INTO users (email, password_hash, full_name, activation_token) VALUES (?, ?, ?, ?) RETURNING user_id';
|
||||||
const userIdRes = await conn.query(userQuery, [email, pwHash, fullName]);
|
const userIdRes = await conn.query(userQuery, [email, pwHash, fullName, activationTokenHash]);
|
||||||
|
|
||||||
// Get user id of the created user
|
// Get user id of the created user
|
||||||
let userId: number = -1;
|
let userId: number = -1;
|
||||||
@@ -48,6 +54,9 @@ export const createUser = async (email: string, password: string, fullName: stri
|
|||||||
sessionId = row.session_id;
|
sessionId = row.session_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send email with activation link (after commit so we don't block on email delivery)
|
||||||
|
await MailService.sendMail(email, 'Activate your Nachklang account', `Hi ${fullName},\n\nPlease click on the following link to activate your account:\n\nhttps://api.nachklang.art/calendar/users/activate?id=${userId}&token=${activationToken}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
@@ -56,20 +65,48 @@ export const createUser = async (email: string, password: string, fullName: stri
|
|||||||
lastIP: ip
|
lastIP: ip
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
await conn.rollback();
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
// Return connection
|
|
||||||
await conn.end();
|
await conn.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const activateUser = async (userId: number, token: string): Promise<boolean> => {
|
||||||
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
|
||||||
|
const checkTokenQuery = 'SELECT user_id, activation_token FROM users WHERE user_id = ? AND is_active = 0';
|
||||||
|
const userNameRes = await conn.query(checkTokenQuery, [userId]);
|
||||||
|
let storedTokenHash = '';
|
||||||
|
for (const row of userNameRes) {
|
||||||
|
storedTokenHash = row.activation_token;
|
||||||
|
}
|
||||||
|
if (!storedTokenHash || !bcrypt.compareSync(token, storedTokenHash)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const activateQuery = 'UPDATE users SET is_active = 1, activation_token = null WHERE user_id = ?';
|
||||||
|
const activateRes = await conn.execute(activateQuery, [userId]);
|
||||||
|
await conn.commit();
|
||||||
|
return activateRes.affectedRows !== 0;
|
||||||
|
} catch (err) {
|
||||||
|
await conn.rollback();
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
await conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given credentials are valid and creates a new session if they are.
|
* Checks if the given credentials are valid and creates a new session if they are.
|
||||||
* Returns the session information in case of a successful login
|
* Returns the session information in case of a successful login
|
||||||
*/
|
*/
|
||||||
export const login = async (email: string, password: string, ip: string): Promise<Session> => {
|
export const login = async (email: string, password: string, ip: string): Promise<Session | null> => {
|
||||||
let conn = await NachklangCalendarDB.getConnection();
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
try {
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
|
||||||
// Get saved password hash
|
// Get saved password hash
|
||||||
const query = 'SELECT user_id, password_hash FROM users WHERE email = ?';
|
const query = 'SELECT user_id, password_hash FROM users WHERE email = ?';
|
||||||
const userRows = await conn.query(query, email);
|
const userRows = await conn.query(query, email);
|
||||||
@@ -82,8 +119,7 @@ export const login = async (email: string, password: string, ip: string): Promis
|
|||||||
|
|
||||||
// Check for correct password
|
// Check for correct password
|
||||||
if (!bcrypt.compareSync(password, savedHash)) {
|
if (!bcrypt.compareSync(password, savedHash)) {
|
||||||
// Wrong password, return invalid
|
return null;
|
||||||
return {} as Session;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate + hash session key
|
// Generate + hash session key
|
||||||
@@ -109,9 +145,9 @@ export const login = async (email: string, password: string, ip: string): Promis
|
|||||||
lastIP: ip
|
lastIP: ip
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
await conn.rollback();
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
// Return connection
|
|
||||||
await conn.end();
|
await conn.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -119,9 +155,11 @@ export const login = async (email: string, password: string, ip: string): Promis
|
|||||||
/**
|
/**
|
||||||
* Checks if the given session information are valid and returns the user information if they are
|
* Checks if the given session information are valid and returns the user information if they are
|
||||||
*/
|
*/
|
||||||
export const checkSession = async (sessionId: string, sessionKey: string, ip: string): Promise<User> => {
|
export const checkSession = async (sessionId: string, sessionKey: string, ip: string): Promise<User | null> => {
|
||||||
let conn = await NachklangCalendarDB.getConnection();
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
try {
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
|
||||||
// Get saved session key hash
|
// Get saved session key hash
|
||||||
const query = 'SELECT user_id, session_key_hash, valid_until FROM sessions WHERE session_id = ?';
|
const query = 'SELECT user_id, session_key_hash, valid_until FROM sessions WHERE session_id = ?';
|
||||||
const sessionRows = await conn.query(query, sessionId);
|
const sessionRows = await conn.query(query, sessionId);
|
||||||
@@ -136,30 +174,26 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
|
|||||||
|
|
||||||
// Check for correct key
|
// Check for correct key
|
||||||
if (!bcrypt.compareSync(sessionKey, savedHash)) {
|
if (!bcrypt.compareSync(sessionKey, savedHash)) {
|
||||||
// Wrong key, return invalid
|
return null;
|
||||||
return {} as User;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the session is still valid
|
// Check if the session is still valid
|
||||||
if (validUntil <= new Date()) {
|
if (validUntil <= new Date()) {
|
||||||
// Session expired, return invalid
|
return null;
|
||||||
return {} as User;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update session entry in SQL
|
// Update session entry in SQL
|
||||||
const updateSessionsQuery = 'UPDATE sessions SET last_IP = ? WHERE session_id = ?';
|
const updateSessionsQuery = 'UPDATE sessions SET last_IP = ? WHERE session_id = ?';
|
||||||
const userIdRes = await conn.query(updateSessionsQuery, [ip, sessionId]);
|
await conn.query(updateSessionsQuery, [ip, sessionId]);
|
||||||
await conn.commit();
|
await conn.commit();
|
||||||
|
|
||||||
// Get the other required user information
|
// Get the other required user information
|
||||||
const userQuery = 'SELECT user_id, email, full_name, is_active FROM users WHERE user_id = ?';
|
const userQuery = 'SELECT user_id, email, full_name, is_active FROM users WHERE user_id = ?';
|
||||||
const userRows = await conn.query(userQuery, userId);
|
const userRows = await conn.query(userQuery, userId);
|
||||||
let username = '';
|
|
||||||
let email = '';
|
let email = '';
|
||||||
let fullName = '';
|
let fullName = '';
|
||||||
let is_active = false;
|
let is_active = false;
|
||||||
for (const row of userRows) {
|
for (const row of userRows) {
|
||||||
username = row.username;
|
|
||||||
email = row.email;
|
email = row.email;
|
||||||
fullName = row.full_name;
|
fullName = row.full_name;
|
||||||
is_active = row.is_active;
|
is_active = row.is_active;
|
||||||
@@ -174,9 +208,86 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
|
|||||||
isActive: is_active
|
isActive: is_active
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
await conn.rollback();
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
// Return connection
|
|
||||||
await conn.end();
|
await conn.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initiatePasswordReset = async (email: string): Promise<boolean> => {
|
||||||
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
|
||||||
|
const checkUsernameQuery = 'SELECT user_id, full_Name FROM users WHERE email = ?';
|
||||||
|
const userNameRes = await conn.query(checkUsernameQuery, [email]);
|
||||||
|
if (userNameRes.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let userId: number = -1;
|
||||||
|
let fullName: string = '';
|
||||||
|
for(let row of userNameRes) {
|
||||||
|
userId = row.user_id;
|
||||||
|
fullName = row.full_Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resetToken = Guid.create().toString();
|
||||||
|
let resetTokenHash = bcrypt.hashSync(resetToken, 10);
|
||||||
|
|
||||||
|
const updateQuery = 'UPDATE users SET pw_reset_token_hash = ? WHERE user_id = ?';
|
||||||
|
const updateRes = await conn.execute(updateQuery, [resetTokenHash, userId]);
|
||||||
|
|
||||||
|
if(updateRes.affectedRows === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await conn.commit();
|
||||||
|
|
||||||
|
await MailService.sendMail(email, 'Password Reset', `Hello ${fullName},\n\nYou requested a password reset for your BonkApp account. If you did not request this, please ignore this email.\n\nTo reset your password, please use the following reset token:\n\n${resetToken}`);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
await conn.rollback();
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
await conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const finalizePasswordReset = async (email: string, token: string, newPassword: string): Promise<boolean> => {
|
||||||
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
|
||||||
|
const checkTokenQuery = 'SELECT user_id, pw_reset_token_hash FROM users WHERE email = ?';
|
||||||
|
const userNameRes = await conn.query(checkTokenQuery, [email]);
|
||||||
|
if (userNameRes.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let userId: string = '';
|
||||||
|
let tokenHash: string = '';
|
||||||
|
for(let row of userNameRes) {
|
||||||
|
userId = row.user_id;
|
||||||
|
tokenHash = row.pw_reset_token_hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!bcrypt.compareSync(token, tokenHash)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pwHash = bcrypt.hashSync(newPassword, 10);
|
||||||
|
const updatePasswordQuery = 'UPDATE users SET password_hash = ?, pw_reset_token_hash = NULL WHERE user_id = ?';
|
||||||
|
const updateRes = await conn.execute(updatePasswordQuery, [pwHash, userId]);
|
||||||
|
|
||||||
|
if(updateRes.affectedRows > 0) {
|
||||||
|
await conn.commit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
await conn.rollback();
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
await conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user