Skip to content

Commit 0e0eed9

Browse files
authored
Merge pull request #18 from sensebox/development
Add: native login and new tutorial system
2 parents 1238f14 + c59a819 commit 0e0eed9

File tree

21 files changed

+2178
-204
lines changed

21 files changed

+2178
-204
lines changed

.env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
MONGO_USERNAME=admin
2+
MONGO_PASSWORD=password
3+
MONGO_DBNAME=blockly
4+
PORT=8080
5+
6+
APP_ORIGIN=http://localhost:3000
7+
8+
# time in seconds
9+
SHARE_EXPIRES_IN=
10+
11+
JWT_SECRET=
12+
REFRESH_TOKEN_SECRET=
13+
14+
15+
SMTP_HOST=
16+
SMTP_FROM=
17+
SMTP_USER=
18+
SMTP_PASS=
19+
CLIENT_URL=

.github/workflows/docker-build.yml

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,14 @@ name: Build and Publish Docker Image to GHCR
22

33
on:
44
push:
5-
branches:
6-
- development
75
tags:
8-
- "v*"
9-
pull_request:
10-
branches:
11-
- main
12-
workflow_dispatch:
6+
- "v*" # Triggers on version tags like v1.0.0
7+
workflow_dispatch: # Allow manual trigger
138

149
jobs:
1510
build:
1611
runs-on: ubuntu-latest
1712

18-
permissions:
19-
contents: read
20-
packages: write # notwendig für GHCR push
21-
pull-requests: read
22-
2313
steps:
2414
- name: Checkout code
2515
uses: actions/checkout@v3
@@ -28,7 +18,6 @@ jobs:
2818
uses: docker/setup-buildx-action@v2
2919

3020
- name: Log in to GitHub Container Registry
31-
if: github.event.pull_request.head.repo.full_name == github.repository # verhindert push bei externen PRs
3221
uses: docker/login-action@v2
3322
with:
3423
registry: ghcr.io
@@ -38,7 +27,7 @@ jobs:
3827
- name: Build and push Docker image
3928
uses: docker/build-push-action@v5
4029
with:
41-
push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
30+
push: true
4231
tags: |
4332
ghcr.io/${{ github.repository_owner }}/react-ardublockly-backend:latest
44-
ghcr.io/${{ github.repository_owner }}/react-ardublockly-backend:${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}
33+
ghcr.io/${{ github.repository_owner }}/react-ardublockly-backend:${{ github.ref_name }}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ package-lock.json
44
upload/*.jpeg
55
upload/*.jpg
66
upload/*.png
7-
7+
.env

docker-compose.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version: '3'
1+
version: "4.1"
22
services:
33
api:
44
build: .
@@ -9,12 +9,24 @@ services:
99
depends_on:
1010
- mongo
1111
mongo:
12-
image: mongo:4.2
12+
image: mongo:noble
1313
volumes:
1414
- ./data:/data/db
1515
- ./init-mongo-user.js:/docker-entrypoint-initdb.d/init-mongo-user.js:ro
1616
environment:
1717
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USERNAME}
1818
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
1919
- MONGO_INITDB_DATABASE=${MONGO_DBNAME}
20-
20+
mongo-express:
21+
image: mongo-express
22+
ports:
23+
- "8081:8081"
24+
environment:
25+
- ME_CONFIG_MONGODB_SERVER=mongo
26+
- ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_USERNAME}
27+
- ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_PASSWORD}
28+
- ME_CONFIG_BASICAUTH_USERNAME=admin
29+
- ME_CONFIG_BASICAUTH_PASSWORD=admin123
30+
depends_on:
31+
- mongo
32+
restart: always

helper/mailer.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// utils/mailer.js
2+
const nodemailer = require("nodemailer");
3+
4+
const transporter = nodemailer.createTransport({
5+
host: process.env.SMTP_HOST,
6+
port: process.env.SMTP_PORT,
7+
secure: false,
8+
auth: {
9+
user: process.env.SMTP_USER,
10+
pass: process.env.SMTP_PASS,
11+
},
12+
});
13+
14+
// 🔥 NEU: Funktion, die req als Parameter nimmt, um dynamische URL zu ermitteln
15+
const sendResetPasswordEmail = async (req, email, resetToken) => {
16+
// 1. Versuche, die Basis-URL aus dem Referer-Header abzuleiten
17+
let clientBaseUrl = process.env.CLIENT_URL; // Fallback aus .env
18+
19+
if (req && req.get("Referer")) {
20+
try {
21+
const refererUrl = new URL(req.get("Referer"));
22+
clientBaseUrl = `${refererUrl.protocol}//${refererUrl.host}`;
23+
} catch (err) {
24+
console.warn("Konnte Referer nicht parsen, verwende CLIENT_URL aus .env");
25+
}
26+
}
27+
28+
// 2. Erstelle den Reset-Link
29+
const resetUrl = `${clientBaseUrl}/user/reset-password?token=${resetToken}`;
30+
31+
const mailOptions = {
32+
from: process.env.SMTP_USER,
33+
to: email,
34+
subject: "Passwort zurücksetzen",
35+
text: `Du hast angefordert, dein Passwort zurückzusetzen. Klicke auf den folgenden Link, um ein neues Passwort zu setzen: ${resetUrl}`,
36+
html: `<p>Du hast angefordert, dein Passwort zurückzusetzen.</p><p>Klicke <a href="${resetUrl}">hier</a>, um ein neues Passwort zu setzen.</p><p>Der Link ist nur für kurze Zeit gültig.</p>`,
37+
};
38+
39+
try {
40+
await transporter.sendMail(mailOptions);
41+
console.log("Reset-Email gesendet an:", email);
42+
} catch (error) {
43+
console.error("Fehler beim Senden der Reset-Email:", error);
44+
throw new Error("E-Mail konnte nicht gesendet werden.");
45+
}
46+
};
47+
48+
module.exports = { sendResetPasswordEmail };

helper/userAuthorization.js

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,71 @@
22
// jshint node: true
33
"use strict";
44

5+
const jwt = require("jsonwebtoken");
6+
const mongoose = require("mongoose");
7+
const User = require("../models/user"); // Passe Pfad
58

6-
const request = require('request');
9+
const userAuthorization = async (req, res, next) => {
10+
const authHeader = req.headers.authorization;
711

12+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
13+
return res.status(401).json({ message: "No token provided." });
14+
}
815

9-
const userAuthorization = function(req, res, next){
10-
var options = {
11-
headers: {
12-
'Content-type': 'application/json',
13-
'Authorization': req.header('authorization')
16+
const token = authHeader.split(" ")[1];
17+
18+
try {
19+
// 🔍 Versuche zuerst, das Token als **native JWT** zu verifizieren
20+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
21+
22+
// Hole Nutzer aus deiner DB
23+
const user = await User.findOne({
24+
_id: decoded.id,
25+
authProvider: "native",
26+
});
27+
if (user) {
28+
req.user = user;
29+
return next();
1430
}
31+
// Wenn kein nativer Nutzer gefunden → falle zu openSenseMap zurück
32+
} catch (jwtError) {
33+
// Token ist kein gültiges natives JWT → weiter mit openSenseMap
34+
}
35+
36+
// 🔁 Fallback: openSenseMap-Auth (wie bisher)
37+
const options = {
38+
headers: {
39+
"Content-Type": "application/json",
40+
Authorization: authHeader,
41+
},
1542
};
16-
request.get('https://api.opensensemap.org/users/me', options)
17-
.on('response', function(response) {
18-
// concatenate updates from datastream
19-
var body = '';
20-
response.on('data', function(chunk){
21-
body += chunk;
22-
});
23-
response.on('end', async function(){
24-
if(response.statusCode !== 200){
25-
return res.status(401).send({
26-
message: 'Unauthorized',
27-
});
43+
44+
const { get } = require("request");
45+
get("https://api.opensensemap.org/users/me", options)
46+
.on("response", function (response) {
47+
let body = "";
48+
response.on("data", (chunk) => (body += chunk));
49+
response.on("end", () => {
50+
if (response.statusCode !== 200) {
51+
return res.status(401).json({ message: "Unauthorized" });
52+
}
53+
try {
54+
const osemUser = JSON.parse(body).data.me;
55+
// Optional: Nutzer in deiner DB anlegen/aktualisieren
56+
req.user = osemUser;
57+
next();
58+
} catch (e) {
59+
return res
60+
.status(401)
61+
.json({ message: "Invalid openSenseMap response" });
2862
}
29-
req.user = JSON.parse(body).data.me;
30-
next();
3163
});
3264
})
33-
.on('error', function(err) {
34-
return res.status(401).send({
35-
message: 'Unauthorized',
36-
});
65+
.on("error", () => {
66+
return res.status(401).json({ message: "openSenseMap unreachable" });
3767
});
3868
};
3969

40-
4170
module.exports = {
42-
userAuthorization
71+
userAuthorization,
4372
};

models/gallery.js

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,18 @@
1-
// jshint esversion: 6
2-
// jshint node: true
3-
"use strict";
1+
const mongoose = require("mongoose");
2+
const { Schema } = mongoose;
43

5-
const mongoose = require('mongoose');
6-
7-
const GallerySchema = new mongoose.Schema({
8-
title: {
9-
type: String,
10-
required: true
11-
},
12-
description: {
13-
type: String,
14-
required: true
4+
const gallerySchema = new Schema(
5+
{
6+
_id: { type: String, required: true },
7+
title: { type: String, required: true },
8+
description: { type: String },
9+
xml: { type: String },
10+
creator: { type: String },
1511
},
16-
creator: {
17-
type: String,
18-
ref: 'User',
19-
required: true
20-
},
21-
xml: {
22-
type: String
12+
{
13+
timestamps: true,
14+
collection: "galleries",
2315
}
24-
},{
25-
timestamps: true
26-
});
27-
16+
);
2817

29-
module.exports = mongoose.model('Gallery', GallerySchema);
18+
module.exports = mongoose.model("Gallery", gallerySchema);

models/tutorial.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,32 @@ const mongoose = require("mongoose");
77
const StepSchema = new mongoose.Schema({
88
type: {
99
type: String,
10-
enum: ["instruction", "task"],
10+
enum: [
11+
"instruction",
12+
"task",
13+
"blockly",
14+
"finish",
15+
"question",
16+
"blocklyExample",
17+
],
1118
required: true,
1219
},
13-
headline: {
20+
title: {
21+
type: String,
22+
},
23+
subtitle: {
1424
type: String,
15-
required: true,
1625
},
1726
text: {
1827
type: String,
19-
required: true,
2028
},
2129
requirements: {
2230
type: [mongoose.Schema.Types.ObjectId],
2331
ref: "Tutorial",
2432
default: undefined,
2533
},
26-
hardware: {
27-
type: [String],
34+
questionData: {
35+
type: [Object],
2836
default: undefined,
2937
},
3038
xml: {
@@ -41,7 +49,9 @@ const TutorialSchema = new mongoose.Schema(
4149
},
4250
title: {
4351
type: String,
44-
required: true,
52+
},
53+
subtitle: {
54+
type: String,
4555
},
4656
public: {
4757
type: Boolean,
@@ -57,6 +67,24 @@ const TutorialSchema = new mongoose.Schema(
5767
type: Number,
5868
required: true,
5969
},
70+
learnings: {
71+
type: [Object],
72+
},
73+
hardware: {
74+
type: [String],
75+
},
76+
duration: {
77+
type: String,
78+
},
79+
year: {
80+
type: String,
81+
},
82+
subjects: {
83+
type: [String],
84+
},
85+
topics: {
86+
type: [String],
87+
},
6088
steps: [
6189
{
6290
type: StepSchema,

0 commit comments

Comments
 (0)