initial commit
This commit is contained in:
commit
28a1ab8841
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
PORT=
|
||||||
|
DATABASE_URL=
|
130
.gitignore
vendored
Normal file
130
.gitignore
vendored
Normal 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
5474
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal 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
74
prisma/schema.prisma
Normal 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
|
||||||
|
}
|
87
src/api/controllers/api.auth.cotroller.js
Normal file
87
src/api/controllers/api.auth.cotroller.js
Normal 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
|
||||||
|
}
|
||||||
|
}();
|
10
src/api/controllers/api.controller.js
Normal file
10
src/api/controllers/api.controller.js
Normal 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;
|
||||||
|
}
|
||||||
|
}();
|
73
src/api/controllers/api.courses.controller.js
Normal file
73
src/api/controllers/api.courses.controller.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
72
src/api/controllers/api.lesson.controller.js
Normal file
72
src/api/controllers/api.lesson.controller.js
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
82
src/api/controllers/api.organization.controller.js
Normal file
82
src/api/controllers/api.organization.controller.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
20
src/api/helpers/globalData.helper.js
Normal file
20
src/api/helpers/globalData.helper.js
Normal 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;
|
||||||
|
};
|
10
src/api/helpers/password.helper.js
Normal file
10
src/api/helpers/password.helper.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
10
src/api/helpers/validator.helper.js
Normal file
10
src/api/helpers/validator.helper.js
Normal 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,}$/);
|
||||||
|
}
|
||||||
|
}
|
19
src/api/middlewares/auth.middleware.js
Normal file
19
src/api/middlewares/auth.middleware.js
Normal 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' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
189
src/api/routes/api.auth.routes.js
Normal file
189
src/api/routes/api.auth.routes.js
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
99
src/api/routes/api.courses.routes.js
Normal file
99
src/api/routes/api.courses.routes.js
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
92
src/api/routes/api.lesson.routes.js
Normal file
92
src/api/routes/api.lesson.routes.js
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
84
src/api/routes/api.organization.routes.js
Normal file
84
src/api/routes/api.organization.routes.js
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
43
src/api/routes/api.routes.js
Normal file
43
src/api/routes/api.routes.js
Normal 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: '/'
|
||||||
|
}
|
||||||
|
}
|
87
src/loaders/fastifyLoader.js
Normal file
87
src/loaders/fastifyLoader.js
Normal 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
19
src/main.js
Normal 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();
|
Loading…
Reference in New Issue
Block a user