initial commit

This commit is contained in:
Omer Sabic 2022-11-01 17:49:53 +01:00
commit 28a1ab8841
21 changed files with 6710 additions and 0 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
PORT=
DATABASE_URL=

130
.gitignore vendored Normal file
View File

@ -0,0 +1,130 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

5474
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "course-system",
"version": "1.0.0",
"description": "",
"main": "src/main.js",
"scripts": {
"start": "node src/main.js",
"dev": "nodemon src/main.js",
"db-push": "prisma db push"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@fastify/cookie": "^8.3.0",
"@fastify/cors": "^8.1.0",
"@fastify/jwt": "^6.3.2",
"@fastify/multipart": "^7.3.0",
"@fastify/reply-from": "^8.3.0",
"@fastify/static": "^6.5.0",
"@fastify/swagger": "^8.0.0",
"@fastify/swagger-ui": "^1.1.0",
"@prisma/client": "^4.5.0",
"cuid": "^2.1.8",
"dotenv": "^16.0.3",
"fastify": "^4.9.2",
"fastify-keycloak-adapter": "^1.6.2",
"minio": "^7.0.32"
},
"devDependencies": {
"nodemon": "^2.0.20",
"prisma": "^4.5.0"
}
}

74
prisma/schema.prisma Normal file
View File

@ -0,0 +1,74 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String
password String
organization Organization? @relation(fields: [organizationId], references: [id])
organizationId String?
purchasedCourses Course[]
}
model Teacher {
id String @id @default(cuid())
username String @unique
first_name String
last_name String
email String @unique
password String
organizations Organization[]
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model Organization {
id String @id @default(cuid())
name String @unique
teacherId String
teacher Teacher @relation(fields: [teacherId], references: [id])
courses Course[]
users User[]
}
model Course {
id String @id @default(cuid())
name String
price Int
organization Organization @relation(fields: [organizationId], references: [id])
organizationId String
lessons Lesson[]
users User[]
@@unique([name, organizationId])
}
model Lesson {
id String @id @default(cuid())
name String
description String
type LessonTypes
course Course @relation(fields: [courseId], references: [id])
courseId String
}
enum LessonTypes {
VIDEO
AUDIO
TEXT
}

View File

@ -0,0 +1,87 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient()
module.exports = new class ApiController {
async login(req, reply) {
const user = await prisma.teacher.findFirst({
where: {
username: req.body.username,
password: req.body.password
},
});
if (!user) {
return reply
.code(401)
.send("Incorrect username/password combo");
}
else {
const token = await reply.jwtSign(user)
reply
.setCookie('token', token, {
path: '/',
secure: false,
httpOnly: true,
sameSite: 'strict'
})
.code(200)
.send(user);
}
}
async register(req, reply) {
const userWithSameInfo = await prisma.teacher.findFirst({
where: {
OR: [
{
username: {
equals: req.body.username
}
},
{
email: {
equals: req.body.email
}
}
]
}
});
if (userWithSameInfo) {
if (userWithSameInfo.username === req.body.username)
return reply
.code(409)
.send({ message: "Username already taken" })
if (userWithSameInfo.email === req.body.email)
return reply
.code(409)
.send({ message: "Email already taken" })
}
const data = await prisma.teacher.create({
data: {
username: req.body.username,
first_name: req.body.first_name,
last_name: req.body.last_name,
email: req.body.email,
password: req.body.password
}
});
reply
.setCookie('token', token, {
path: '/',
secure: true,
httpOnly: true,
sameSite: true
})
.code(200)
.send(data)
return
}
async me(req, reply) {
return req.user
}
}();

View File

@ -0,0 +1,10 @@
module.exports = new class ApiController {
async lessons(req, reply) {
reply.header('Content-Type', 'application/json');
let data = await squidexHelper.contents.query('courses');
reply.send(data.items.map(i => {
return {id: i.id, ...i.data}
}));
return;
}
}();

View File

@ -0,0 +1,73 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient()
module.exports = new class ApiController {
async createCourse(req, reply) {
let course = prisma.course.create({
data: {
name: req.body.name,
price: req.body.price,
organizationId: req.body.organizationId
}
});
return course;
}
async getCourse(req, reply) {
let course = prisma.course.findUnique({
where: {
id: req.params.courseId
},
include: {
organization: true,
_count: {
select: {
lessons: true
}
}
},
});
return course;
}
async getCourses(req, reply) {
let courses = await prisma.course.findMany({
where: {
organizationId: req.query.organization,
},
include: {
_count: {
select: {
lessons: true
}
}
}
});
let organization = await prisma.organization.findUnique({
where: {
id: courses[0].organizationId
}
})
return { ...organization, courses: courses };
}
async deleteCourse(req, reply) {
try {
let orgs = await prisma.course.deleteMany({
where: {
id: req.query.id,
organization: { is: { teacherId: req.user.id } }
}
});
return reply.send(orgs)
}
catch (err) {
return reply.code(500).send(err);
}
}
}();

View File

@ -0,0 +1,72 @@
const { PrismaClient, LessonTypes } = require('@prisma/client');
const prisma = new PrismaClient();
const { getMinio } = require('../helpers/globalData.helper')
module.exports = new class ApiController {
async createLesson(req, reply) {
try {
let body = req.body;
let organization = prisma.course.findFirst({
where: {
id: body.courseId,
course: {
id: req.user.id
}
}
});
if(!organization) {
return reply.code(400).send({ message: "Could not find course." })
}
let lesson = prisma.lesson.create({
data: {
name: body.name,
description: body.description,
courseId: body.courseId,
type: LessonTypes[body.type.toUpperCase()]
}
});
return lesson;
}
catch (err) {
return reply.code(500).send({ error: err })
}
}
async getLesson(req, reply) {
try {
let lesson = prisma.lesson.findFirst({
where: {
id: req.params.lessonId,
course: { is: { organization: { teacherId: req.user.id } }}
},
include: {
course: true
}
});
if (lesson) return lesson
else return reply.code(404).send({ message: `Lesson with id ${req.params.lessonId} not found.` })
}
catch (err) {
return reply.code(500).send({ error: err })
}
}
async getLessons(req, reply) {
try {
let lessons = prisma.lesson.findMany({
where: {
course: {is: {id: req.query.courseId, organization: { teacherId: req.user.id } }}
}
});
return lessons
}
catch (err) {
return reply.code(500).send({ error: err })
}
}
}();

View File

@ -0,0 +1,82 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient()
module.exports = new class ApiController {
async createOrganization(req, reply) {
let possibleTaken = await prisma.organization.findUnique({
where: {
name: req.body.name
}
})
if (possibleTaken) {
return reply
.code(409)
.send({ message: "Organization name already taken" })
}
let org = prisma.organization.create({
data: {
name: req.body.name,
teacherId: req.user.id
}
});
return org;
}
async getOrganizationInfo(req, reply) {
let org = await prisma.organization.findUnique({
where: {
id: req.params.organizationId
},
include: {
courses: req.query.courses || false
}
});
if (!org) {
return reply
.code(404)
.send({ message: `Organization with ID ${req.params.id} not found` });
}
return org;
}
async getAllOrganizations(req, reply) {
try {
let orgs = await prisma.organization.findMany({
where: {
teacherId: req.user.id
}
});
return reply.send(orgs)
}
catch (err) {
return reply.code(500).send(err);
}
}
async deleteOrganization(req, reply) {
try {
let org = await prisma.organization.deleteMany({
where: {
AND: [
{
name: req.query.id,
},
{
teacherId: req.user.id
}
]
}
});
return org
}
catch (err) {
return reply.code(500).send(err)
}
}
}();

View File

@ -0,0 +1,20 @@
var _redis = null;
var _minio = null;
exports.getRedis = function() {
return _redis;
};
exports.setRedis = function(redis) {
//validate the data...
_redis = redis;
};
exports.getMinio = function() {
return _minio;
};
exports.setMinio = function(minio) {
//validate the data...
_minio = minio;
};

View File

@ -0,0 +1,10 @@
const bcrypt = require('bcrypt');
module.exports = {
encrypt: (password) => {
return bcrypt.hashSync(password, bcrypt.genSaltSync(10));
},
compare: (password, hash) => {
return bcrypt.compareSync(password, hash);
}
}

View File

@ -0,0 +1,10 @@
module.exports = {
email: (email) => {
// validate email format
return !!email.match(/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/);
},
password: (password) => {
// validate password format
return !!password.match(/^[a-zA-Z\d!@#$&\.]{6,}$/);
}
}

View File

@ -0,0 +1,19 @@
// const authHelper = require('../helpers/auth.helper');
module.exports = {
auth: async (req, reply) => {
try {
await req.jwtVerify()
}
catch (err) {
console.log(err)
return reply.code(401).send({ message: "Not authenticated" })
}
},
isAdmin: async (req, res, next) => {
if (!req.user.is_admin) {
return res.status(403).send({ error: 'You are not allowed to access this resource' });
}
},
};

View File

@ -0,0 +1,189 @@
const controller = require('../controllers/api.auth.cotroller');
const auth = require('../middlewares/auth.middleware')
function router(fastify, options, next) {
fastify.post('/register', {
schema: {
tags: ['auth'],
summary: 'Register account',
body: {
type: 'object',
properties: {
username: {
type: 'string',
pattern: '^[a-zA-Z-_1-9]{6,12}$'
},
first_name: {
type: 'string'
},
last_name: {
type: 'string'
},
email: {
type: 'string',
format: 'email'
},
password: {
type: 'string'
},
}
},
description: 'Endpoint to register a new user',
response: {
200: {
type: 'object',
properties: {
properties: {
id: {
type: 'string'
},
username: {
type: 'string'
},
first_name: {
type: 'string'
},
last_name: {
type: 'string'
},
email: {
type: 'string',
format: 'email'
},
created_at: {
type: 'string',
format: 'date-time'
},
}
}
},
409: {
type: 'object',
properties: {
message: {
type: 'string'
}
}
}
},
},
handler: controller.register
});
fastify.post('/login', {
schema: {
tags: ['auth'],
summary: 'Login to existing account',
body: {
type: 'object',
properties: {
username: {
type: 'string',
pattern: '^[a-zA-Z-_.1-9]{6,16}$'
},
password: {
type: 'string'
},
}
},
description: 'Endpoint to register a new user',
response: {
200: {
type: 'object',
properties: {
id: {
type: 'string'
},
username: {
type: 'string'
},
first_name: {
type: 'string'
},
last_name: {
type: 'string'
},
email: {
type: 'string',
format: 'email'
},
created_at: {
type: 'string',
format: 'date-time'
},
}
},
409: {
type: 'object',
properties: {
message: {
type: 'string'
}
}
}
},
},
handler: controller.login
});
fastify.get('/me', {
schema: {
tags: ['auth'],
summary: 'Get User info',
description: 'Endpoint get currently logged in user\'s account info from their jwt',
response: {
200: {
type: 'object',
properties: {
id: {
type: 'string'
},
username: {
type: 'string'
},
first_name: {
type: 'string'
},
last_name: {
type: 'string'
},
email: {
type: 'string',
format: 'email'
},
created_at: {
type: 'string',
format: 'date-time'
},
updated_at: {
type: 'string',
format: 'date-time'
},
iat: {
type: 'number'
}
}
},
409: {
type: 'object',
properties: {
message: {
type: 'string'
}
}
}
},
},
onRequest: [auth.auth],
handler: controller.me
});
next();
}
module.exports = {
function: router,
options: {
prefix: '/api/auth'
}
}

View File

@ -0,0 +1,99 @@
const controller = require('../controllers/api.courses.controller');
const auth = require('../middlewares/auth.middleware');
function router(fastify, options, next) {
fastify.get('/:courseId', {
schema: {
tags: ['course'],
summary: 'Get a course',
description: 'Retrieve course data by ID',
// response: {
// 200: {
// type: 'object',
// properties: {
// message: { type: 'string' }
// }
// }
// },
},
handler: controller.getCourse
})
fastify.post('/', {
schema: {
tags: ['course'],
summary: 'Create a course',
description: 'Endpoint to test the server',
// response: {
// 200: {
// type: 'object',
// properties: {
// message: { type: 'string' }
// }
// }
// },
},
handler: controller.createCourse
});
fastify.get('/', {
schema: {
tags: ['course'],
summary: 'Get all courses from organization',
querystring: {
type: 'object',
properties: {
organization: {
type: 'string'
}
}
},
description: 'Endpoint to test the server',
// response: {
// 200: {
// type: 'object',
// properties: {
// message: { type: 'string' }
// }
// }
// },
},
handler: controller.getCourses
});
fastify.delete('/', {
schema: {
tags: ['course'],
summary: 'Delete a course by ID',
querystring: {
type: 'object',
properties: {
id: {
type: 'string'
}
}
},
description: 'Endpoint to test the server',
// response: {
// 200: {
// type: 'object',
// properties: {
// message: { type: 'string' }
// }
// }
// },
},
preHandler: [auth.auth],
handler: controller.deleteCourse
});
next();
}
module.exports = {
function: router,
options: {
prefix: '/api/course'
}
}

View File

@ -0,0 +1,92 @@
const controller = require('../controllers/api.lesson.controller');
const { auth } = require('../middlewares/auth.middleware');
function router(fastify, options, next) {
fastify.get('/:lessonId', {
schema: {
tags: ['lessons'],
summary: 'Test endpoint',
description: 'Endpoint to test the server',
// response: {
// 200: {
// type: 'object',
// properties: {
// message: { type: 'string' }
// }
// }
// },
},
preHandler: [auth],
handler: controller.getLesson
});
fastify.get('/', {
schema: {
tags: ['lessons'],
summary: 'Get all lessons in a course',
description: 'Get lessons from a course, whose id is provided in the query',
querystring: {
type: 'object',
properties: {
courseId: {
type: 'string'
}
}
}
// response: {
// 200: {
// type: 'object',
// properties: {
// message: { type: 'string' }
// }
// }
// },
},
preHandler: [auth],
handler: controller.getLessons
});
fastify.post('/', {
schema: {
tags: ['lessons'],
summary: 'Create a lesson',
description: 'Endpoint to create a lesson',
body: {
type: 'object',
properties: {
name: {
type: 'string'
},
description: {
type: 'string'
},
type: {
type: 'string',
enum: ['video', 'audio', 'text']
}
}
}
// response: {
// 200: {
// type: 'object',
// properties: {
// message: { type: 'string' }
// }
// }
// },
},
preHandler: [auth],
handler: controller.createLesson
})
next();
}
module.exports = {
function: router,
options: {
prefix: '/api/lesson'
}
}

View File

@ -0,0 +1,84 @@
const controller = require('../controllers/api.organization.controller');
const authMiddleware = require('../middlewares/auth.middleware')
function router(fastify, options, next) {
fastify.get('/:organizationId', {
schema: {
tags: ['organization'],
summary: 'Test endpoint',
description: 'Endpoint to test the server',
// response: {
// 200: {
// type: 'object',
// properties: {
// message: { type: 'string' }
// }
// }
// },
},
handler: controller.getOrganizationInfo
});
fastify.post('/', {
schema: {
tags: ['organization'],
summary: 'Create a new organization',
description: 'Endpoint for creating a new organization by providing a name',
body: {
type: 'object',
properties: {
name: {
type: 'string',
pattern: '^[a-zA-Z-_]+$'
}
}
}
// response: {
// 200: {
// type: 'object'
// }
// },
},
preHandler: [authMiddleware.auth],
handler: controller.createOrganization
});
fastify.get('/', {
schema: {
tags: ['organization'],
summary: 'Get all organizations',
description: 'Get all organizations of currently signed in user',
// response: {
// 200: {
// type: 'object'
// }
// },
},
preHandler: [authMiddleware.auth],
handler: controller.getAllOrganizations
});
fastify.delete('/', {
schema: {
tags: ['organization'],
summary: 'Get all organizations',
description: 'Get all organizations of currently signed in user',
// response: {
// 200: {
// type: 'object'
// }
// },
},
preHandler: [authMiddleware.auth],
handler: controller.deleteOrganization
})
next();
}
module.exports = {
function: router,
options: {
prefix: '/api/organization'
}
}

View File

@ -0,0 +1,43 @@
const controller = require('../controllers/api.controller');
function router(fastify, options, next) {
// fastify.get('/', {
// schema: {
// tags: ['test'],
// summary: 'Test endpoint',
// description: 'Endpoint to test the server',
// response: {
// 200: {
// type: 'object',
// properties: {
// message: { type: 'string' }
// }
// }
// },
// },
// handler: controller.root
// });
fastify.get('/query', {
schema: {
tags: ['test'],
summary: 'Test endpoint',
description: 'Endpoint to test the server',
// response: {
// 200: {
// type: 'object'
// }
// },
},
handler: controller.lessons
});
next();
}
module.exports = {
function: router,
options: {
prefix: '/'
}
}

View File

@ -0,0 +1,87 @@
function main() {
require('dotenv').config();
const globalData = require('../api/helpers/globalData.helper')
const minio = require('minio')
var minioClient = new minio.Client({
endPoint: 's3-s3.omersabic.com',
useSSL: true,
accessKey: 'sedj2oatulc90nDv',
secretKey: 'JVoEdWA5gvSP63ENIySv8yW9P9WH5xx5'
});
globalData.setMinio(minioClient)
const fastify = require('fastify')({
logger: false,
ignoreTrailingSlash: true,
cors: true
});
const path = require('path');
const fs = require('fs');
// Initialize fastify plugins
fastify.register(require('@fastify/cors'), {
origin: 'http://localhost:3001',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
credentials: true
});
fastify.register(require('@fastify/swagger'), {
swagger: {
info: {
title: 'API',
description: 'API documentation',
version: '1.0.0'
},
externalDocs: {
url: 'https://swagger.io',
description: 'Find more info here'
},
host: 'localhost:3000',
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json']
}
});
fastify.register(require('@fastify/swagger-ui'), {
routePrefix: '/api/v1/docs',
docExpansion: 'full',
deepLinking: false
})
fastify.register(require('@fastify/static'), {
root: path.join(__dirname, '../api/public'),
prefix: '/public/'
});
fastify.register(require('@fastify/jwt'), {
// secret: (Math.random() + 1).toString(36).substring(2),
secret: 'secret',
cookie: {
cookieName: 'token',
signed: false
}
});
fastify.register(require('@fastify/cookie'))
// fastify.addHook('preValidation', (request, reply, done) => {
// request.body = JSON.parse(request.body);
// done();
// })
// Load all routes
fs.readdirSync(path.join(__dirname, '../api/routes')).forEach(file => {
if (!file.endsWith('.routes.js')) return;
const route = require(path.join(__dirname, '../api/routes', file));
fastify.register(route.function, route.options);
});
return fastify
}
module.exports = main;

19
src/main.js Normal file
View File

@ -0,0 +1,19 @@
require('dotenv').config()
const PORT = process.env.PORT || 3000;
// Running the fastify loader
const fastify = require('./loaders/fastifyLoader')();
async function start() {
fastify.listen({ port: PORT, address: '0.0.0.0' }, (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.info(`server listening on ${address}`);
console.info(`API Docs running on ${address}/api/v1/docs`)
});
}
start();