Skip to content

Commit

Permalink
Revert "moved to firebase, scaffolding for challenges, new courses"
Browse files Browse the repository at this point in the history
This reverts commit 717119f.
  • Loading branch information
nsjames authored and nsjames committed Oct 2, 2023
1 parent 1cb88f7 commit 794469a
Show file tree
Hide file tree
Showing 49 changed files with 3,739 additions and 7,482 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,3 @@ dist
*.tar
*.tar.gz
*.zip

sensitive/firebase.json
8 changes: 4 additions & 4 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@

FIRESTORE_COLLECTION_NAME=learnhub

COUCHBASE_HOST=couchbase://localhost
COUCHBASE_USERNAME=test
COUCHBASE_PASSWORD=testtest

AUTH0_CLIENT_ID=test
AUTH0_CLIENT_SECRET=test
AUTH0_AUDIENCE=https://dev.eosnetwork.com
AUTH0_ISSUER_URL=https://devhub.us.auth0.com/
USER_CREATE_KEY=
2 changes: 1 addition & 1 deletion backend/INDEXES
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
CREATE INDEX course_progress_agg ON devhub(`finished`,`course_slug_hash`) WHERE doc_type = 'course_progress' USING GSI
CREATE INDEX idx_challenges ON devhub(doc_type) WHERE doc_type = 'challenge' USING GSI
CREATE INDEX idx_course_progress ON devhub(doc_type) WHERE doc_type = 'course_progress' USING GSI
CREATE INDEX course_difficulty ON devhub(`difficulty`) WHERE doc_type = 'course' USING GSI
CREATE INDEX course_search ON devhub(`title`,`description`) WHERE doc_type = 'course' USING GSI
CREATE INDEX bounty_search ON devhub(`type`) WHERE doc_type = 'bounty' USING GSI
CREATE INDEX doctype ON devhub(`doc_type`) USING GSI
11 changes: 8 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "backend",
"version": "1.2.0",
"version": "1.1.0",
"description": "EOS Network Foundation DevHub portal website backend",
"main": "index.js",
"repository": "https://github.com/eosnetworkfoundation/devhub",
Expand All @@ -11,6 +11,11 @@
"url": "https://eosnetwork.com"
},
"license": "MIT",
"engineStrict": true,
"engines": {
"node": "16",
"yarn": "1"
},
"scripts": {
"build": "npx tsc",
"run": "node dist/index.js",
Expand All @@ -25,12 +30,12 @@
"compression": "^1.7.4",
"concurrently": "^7.2.2",
"cors": "^2.8.5",
"couchbase": "^3.2.5",
"dotenv": "^16.0.1",
"eosjs-ecc": "^4.0.7",
"express": "^4.18.1",
"express-oauth2-jwt-bearer": "^1.5.0",
"express-oauth2-jwt-bearer": "^1.1.0",
"express-openid-connect": "^2.7.2",
"firebase-admin": "^11.10.1",
"helmet": "^5.1.1",
"isomorphic-fetch": "^3.0.0",
"morgan": "^1.10.0"
Expand Down
161 changes: 114 additions & 47 deletions backend/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,112 @@ import "isomorphic-fetch";
import CourseService from "./services/course.service";
import UserService from "./services/user.service";
import Errors, {CustomError} from "./util/errors";
import ChallengeService from "./services/challenge.service";
import BountyService from "./services/bounty.service";
import {sha256} from 'eosjs-ecc';
import CourseProgressService from "./services/course_progress.service";
import {CourseProgress} from "@eosn/devhub-structures/course_progress";
import {FireAuth} from "./util/firebase";

const { auth, requiredScopes } = require('express-oauth2-jwt-bearer');

const checkJwt = async (req:any, res:any, next:any) => {
try {
auth({
issuerBaseURL: process.env.AUTH0_ISSUER_URL,
audience: process.env.AUTH0_AUDIENCE
})(req, res, x => {
if(x === undefined) return next();
return res.json(Errors.authenticationError())
});
} catch(err){
console.error("Error checking JWT", err);
return res.json(Errors.authenticationError())
}
}


let managementToken: { token:string, expires_at:number}|null = null;

const authOnly = async (req:any, res:any, next:any) => {
// get bearer token
const authorization = req.headers['authorization'];
if(!authorization) return res.json(Errors.authenticationError());
const getManagementToken = async () => {
if(managementToken && managementToken.expires_at > Date.now()){
return managementToken.token;
}

const options = {
grant_type: 'client_credentials',
client_id: process.env.AUTH0_CLIENT_ID,
client_secret: process.env.AUTH0_CLIENT_SECRET,
audience: `${process.env.AUTH0_ISSUER_URL}api/v2/`,
};

const token:any = await fetch(`${process.env.AUTH0_ISSUER_URL}/oauth/token`.replace(/\/\//g, "/"), {
method: 'POST',
headers: {'content-type': 'application/json'},
body:JSON.stringify(options)
}).then(res => res.json()).catch(err => {
console.error("Error getting management token", err);
return null;
});

// validate token
const token = authorization.split(' ')[1];
if(!token || !token.length) return res.json(Errors.authenticationError());
if(token){
managementToken = {
token:token.access_token,
expires_at: Date.now() + (token.expires_in * 1000)
}
}

// decode token
const decodedToken = await FireAuth.decodeToken(token);
if(!decodedToken) return res.json(Errors.authenticationError());
if(decodedToken === "token_expired") return res.json(Errors.tokenExpired());
if(managementToken){
return managementToken.token;
}

// get user
const user = await UserService.get(UserService.subToUserID(decodedToken.uid));
if(!user) return res.json(Errors.authenticationError());
return null;
}

req.user = user;
const checkRole = async (authObject:any, requiredRole:string) => {
try {
const mgmtToken = await getManagementToken();
const result = await fetch(`${process.env.AUTH0_ISSUER_URL}api/v2/users/${authObject.payload.sub}/roles`, {
method: 'GET',
headers: {
'authorization': `Bearer ${mgmtToken}`,
}
}).then(res => res.json());

return !!result.find(role => role.name === requiredRole);

} catch (err) {
console.error("Error checking role", err);
return false;
}
}

const adminOnly = async (req:any, res:any, next:any) => {
const isAdmin = await checkRole(req.auth, "admin");
if(!isAdmin) return res.status(500).send("Admin only");
next();
}

const userIdFromReq = (req:any): string => {
const { sub } = req.auth.payload;
return UserService.subToUserID(sub);
}

const routes = Router();

const senderIp = (req:any) => req.headers['x-forwarded-for'] || req.connection.remoteAddress;

routes.get('/test', (req, res) => {
res.send(true);
});

routes.get('/auth/status', authOnly, async (req:any, res) => {
routes.get('/auth/status', checkJwt, async (req:any, res) => {
// Always true if this route succeeds.
res.send(true);
});

routes.get('/user', authOnly, async (req:any, res) => {
res.send(req.user);
routes.get('/user', checkJwt, async (req:any, res) => {
const user_id = userIdFromReq(req);
const user = await UserService.get(user_id);
res.send(user ? user.asPublic() : Errors.noSuchUser());
});

routes.get('/user/:user_id', async (req:any, res) => {
Expand All @@ -59,7 +122,6 @@ routes.post('/user', async (req, res) => {
});

routes.post('/course', async (req, res) => {
if(!process.env.IS_ADMIN_API) return res.status(500).send("Admin only");
const course = await CourseService.create(req.body);
res.json(course);
});
Expand Down Expand Up @@ -94,11 +156,27 @@ routes.get('/courses/:search', async (req, res) => {
res.json(courses);
});

routes.get('/progress/:slug_hash', authOnly, async (req:any, res) => {
const progress = await CourseProgressService.get(req.params.slug_hash, req.user.id);
routes.post('/bounty', checkJwt, adminOnly, async (req, res) => {
const bounty = await BountyService.create(req.body);
res.json(bounty);
});

routes.get('/bounty/:slug', async (req, res) => {
const bounty = await BountyService.get(req.params.slug);
res.json(bounty);
});

routes.get('/bounties/:type', async (req, res) => {
const bounties = await BountyService.getByType(parseInt(req.params.type));
res.json(bounties);
});

routes.get('/progress/:slug_hash', checkJwt, async (req, res) => {
const user_id = userIdFromReq(req);
const progress = await CourseProgressService.get(req.params.slug_hash, user_id);
res.json(progress || new CourseProgress({
course_slug_hash: req.params.slug_hash,
user_id:req.user.id,
user_id,
}));
});

Expand All @@ -107,25 +185,29 @@ routes.get('/progresses/:user_id', async (req, res) => {
res.json(progress || []);
});

routes.post('/progress/timestamp', authOnly, async (req:any, res) => {
routes.post('/progress/timestamp', checkJwt, async (req, res) => {
const user_id = userIdFromReq(req);
const {course_slug_hash, timestamp} = req.body;
const result = await CourseProgressService.setTimestamp(req.user.id, course_slug_hash, timestamp);
const result = await CourseProgressService.setTimestamp(user_id, course_slug_hash, timestamp);
res.json(result);
});

routes.post('/progress/answers', authOnly, async (req:any, res) => {
routes.post('/progress/answers', checkJwt, async (req, res) => {
const user_id = userIdFromReq(req);
const {course_slug_hash, answers} = req.body;
const result = await CourseProgressService.setAnswers(course_slug_hash, req.user.id, answers);
const result = await CourseProgressService.setAnswers(course_slug_hash, user_id, answers);
res.json(result);
});

routes.get('/progress/finished/:course_slug_hash', authOnly, async (req:any, res) => {
const result = await CourseProgressService.finalize(req.params.course_slug_hash, req.user.id);
routes.get('/progress/finished/:course_slug_hash', checkJwt, async (req, res) => {
const user_id = userIdFromReq(req);
const result = await CourseProgressService.finalize(req.params.course_slug_hash, user_id);
res.json(result);
});

routes.get('/progress/bad-answers/:course_slug_hash', authOnly, async (req:any, res) => {
const result = await CourseProgressService.getBadAnswers(req.params.course_slug_hash, req.user.id);
routes.get('/progress/bad-answers/:course_slug_hash', checkJwt, async (req, res) => {
const user_id = userIdFromReq(req);
const result = await CourseProgressService.getBadAnswers(req.params.course_slug_hash, user_id);
res.json(result);
});

Expand All @@ -134,21 +216,6 @@ routes.get('/finished-courses-count', async (req, res) => {
res.json(result);
});

//
routes.post('/challenge', async (req, res) => {
if(!process.env.IS_ADMIN_API) return res.status(500).send("Admin only");
const challenge = await ChallengeService.create(req.body);
res.json(challenge);
});

routes.get('/challenge/:id', async (req, res) => {
res.json(await ChallengeService.get(req.params.id));
});

routes.get('/challenges', async (req, res) => {
res.json(await ChallengeService.getAll());
});

routes.all('*', (req, res) => {
console.log('hit bad route', req.method, req.url, req.body);
res.sendStatus(403)
Expand Down
27 changes: 27 additions & 0 deletions backend/src/services/bounty.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {Bounty} from "@eosn/devhub-structures/bounty";
import {sha256} from "eosjs-ecc";
import ORM from "../util/orm";

export default class BountyService {

static async get(slug:string): Promise<Bounty | null> {
const hash = sha256(slug);
const bounty = await ORM.get(Bounty.key(hash), Bounty);
if(!bounty) return null;
return <Bounty>bounty;
}

static async set(bounty:Bounty): Promise<boolean> {
return await ORM.update(bounty);
}

static async create(json:any): Promise<boolean> {
json.slug = Bounty.titleToSlug(json.localizations['en'].title);
json.slug_hash = sha256(json.slug);
return await this.set(new Bounty(json));
}

static async getByType(type:number): Promise<Array<Bounty>> {
return ORM.query(`SELECT * FROM BUCKET_NAME WHERE doc_type = 'bounty' AND type = ${type}`, Bounty)
}
}
36 changes: 0 additions & 36 deletions backend/src/services/challenge.service.ts

This file was deleted.

Loading

0 comments on commit 794469a

Please sign in to comment.