├── .env.example ├── .gitignore ├── README.md ├── assets └── app.css ├── controllers ├── AuthController.ts ├── BaseSurveyController.ts ├── QuestionController.ts ├── SiteController.ts └── SurveyController.ts ├── deps.ts ├── helpers.ts ├── lock.json ├── middleware ├── authMiddleware.ts └── staticFileMiddleware.ts ├── models ├── BaseModel.ts ├── Question.ts ├── Survey.ts └── User.ts ├── mongo.ts ├── rest.http.example ├── router.ts ├── server.ts └── views ├── notfound.ejs ├── partials ├── footer.ejs └── header.ejs ├── survey.ejs ├── surveyFinish.ejs └── surveys.ejs /.env.example: -------------------------------------------------------------------------------- 1 | # MongoDB connect URI 2 | MONGODB_URI = mongodb://localhost:27017 3 | # MondoDB database name 4 | DB_NAME = deno_survey 5 | # JWT encryption/decription secret key 6 | JWT_SECRET_KEY = some-random-key 7 | # JWT expiration duration 8 | JWT_EXP_DURATION = 3600000 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .deno_plugins 3 | /*.http -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deno Survey App 2 | Survey application with REST API to manage surveys and questions and website, where all surveys are outputted. 3 | 4 | ## Installation 5 | 6 | You need to have [deno installed](https://deno.land/#installation) in order to run this application.
7 | Install also [denon](https://deno.land/x/denon) which watches your file changes and automatically restarts server. 8 | 9 | 1. Clone the repository 10 | 1. Go to the project root folder 11 | 1. Copy `.env.example` into `.env` file and adjust the values 12 | 13 | ```dotenv 14 | # MongoDB connect URI 15 | MONGODB_URI = mongodb://localhost:27017 16 | # MondoDB database name 17 | DB_NAME = deno_survey 18 | # JWT encryption/decription secret key 19 | JWT_SECRET_KEY = some-random-key 20 | # JWT expiration duration 21 | JWT_EXP_DURATION = 3600000 22 | ``` 23 | 1. Run the application by executing 24 | 25 | ```dotenv 26 | denon run --allow-net --allow-write --allow-read --allow-env --allow-plugin --unstable server.ts 27 | ``` 28 | 29 | ## Usage 30 | 31 | In REST API the following endpoints are supported. 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 57 | 58 | 59 | 60 | 61 | 62 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 96 | 97 | 98 | 99 | 100 | 101 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 |
METHODURLDescriptionRequest
POST/api/registerRegister 48 |
 49 | json
 50 | {
 51 |   "name": "test",
 52 |   "email": "test@example.com",
 53 |   "password": "test"
 54 | }
 55 |             
56 |
POST/api/loginLogin 63 |
 64 | json
 65 | {
 66 |   "email": "test@example.com",
 67 |   "password": "test"
 68 | }
 69 |             
70 |
GET/api/surveyGet surveys for authentication user(Empty)
GET/api/survey/:idGet single survey(Empty)
POST/api/surveyCreate survey 89 |
 90 | {
 91 |   "name": "Survey name",
 92 |   "description": "Survey description"
 93 | }
 94 |             
95 |
PUT/api/survey/:idUpdate survey 102 |
{
103 |   "name": "Survey name",
104 |   "description": "Survey description"
105 | }
106 |
DELETE/api/survey/:idDelete survey(Empty)
GET/api/survey/:surveyId/questionGet questions for survey(Empty)
GET/api/question/:idGet single question(Empty)
POST/api/question/:surveyIdCreate question for survey 131 |
Single choice question
132 | {
133 |   "text": "How much you liked the Deno Course?",
134 |   "type": "choice",
135 |   "required": true,
136 |   "data": {
137 |     "multiple": false,
138 |     "answers": [
139 |       "I liked it very much",
140 |       "I liked it",
141 |       "I did not like it",
142 |       "I hate it"
143 |     ]
144 |   }
145 | }
146 | Multiple choice question
147 | {
148 |   "text": "Which features do you like in Deno?",
149 |   "type": "choice",
150 |   "required": true,
151 |   "data": {
152 |     "multiple": true,
153 |     "answers": [
154 |       "Typescript",
155 |       "Security",
156 |       "Import from URL",
157 |       "ES6 modules"
158 |     ]
159 |   }
160 | }
161 | Free text question
162 | {
163 |   "text": "Any other comments?",
164 |   "type": "text",
165 |   "required": false
166 | }
167 |
PUT/api/question/:idUpdate question
DELETE/api/question/:idDelete question(Empty)
183 | -------------------------------------------------------------------------------- /assets/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | background-color: rgb(223, 223, 223); 4 | } 5 | 6 | .survey-card { 7 | transition: all 0.3s;; 8 | } 9 | .survey-card a { 10 | text-decoration: none; 11 | color: #212529; 12 | } 13 | 14 | .survey-card:hover { 15 | box-shadow: 0 0 0 4px var(--blue); 16 | } -------------------------------------------------------------------------------- /controllers/AuthController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RouterContext, 3 | makeJwt, 4 | Jose, 5 | Payload, 6 | setExpiration, 7 | } from "../deps.ts"; 8 | import { userCollection } from "../mongo.ts"; 9 | import { hashSync, compareSync } from "../deps.ts"; 10 | import User from "../models/User.ts"; 11 | 12 | const header: Jose = { 13 | alg: "HS256", 14 | typ: "JWT", 15 | }; 16 | export class AuthController { 17 | async register(ctx: RouterContext) { 18 | const { value: { name, email, password } } = await ctx.request.body(); 19 | 20 | let user = await User.findOne({ email }); 21 | if (user) { 22 | ctx.response.status = 422; 23 | ctx.response.body = { message: "Email is already used" }; 24 | return; 25 | } 26 | const hashedPassword = hashSync(password); 27 | user = new User({ name, email, password: hashedPassword }); 28 | await user.save(); 29 | ctx.response.status = 201; 30 | ctx.response.body = { 31 | id: user.id, 32 | name: user.name, 33 | email: user.email 34 | }; 35 | } 36 | async login(ctx: RouterContext) { 37 | const { value: { email, password } } = await ctx.request.body(); 38 | if (!email || !password) { 39 | ctx.response.status = 422; 40 | ctx.response.body = { message: "Please provide email and password" }; 41 | return; 42 | } 43 | let user = await User.findOne({ email }); 44 | if (!user) { 45 | ctx.response.status = 422; 46 | ctx.response.body = { message: "Incorrect email" }; 47 | return; 48 | } 49 | if (!compareSync(password, user.password)) { 50 | ctx.response.status = 422; 51 | ctx.response.body = { message: "Incorrect password" }; 52 | return; 53 | } 54 | 55 | const payload: Payload = { 56 | iss: user.email, 57 | exp: setExpiration( 58 | Date.now() + parseInt(Deno.env.get("JWT_EXP_DURATION") || "0"), 59 | ), 60 | }; 61 | const jwt = makeJwt( 62 | { key: Deno.env.get("JWT_SECRET_KEY") || "", payload, header }, 63 | ); 64 | 65 | ctx.response.body = { 66 | id: user.id, 67 | name: user.name, 68 | email: user.email, 69 | jwt, 70 | }; 71 | } 72 | } 73 | 74 | export default new AuthController(); 75 | -------------------------------------------------------------------------------- /controllers/BaseSurveyController.ts: -------------------------------------------------------------------------------- 1 | import { RouterContext } from "../deps.ts"; 2 | import Survey from "../models/Survey.ts"; 3 | import User from "../models/User.ts"; 4 | 5 | export default class BaseSurveyController { 6 | async findSurveyOrFail( 7 | id: string, 8 | ctx: RouterContext, 9 | ): Promise { 10 | const survey: Survey | null = await Survey.findOne(id); 11 | // If the survey does not exist return with 404 12 | if (!survey) { 13 | ctx.response.status = 404; 14 | ctx.response.body = { message: "Invalid Survey ID" }; 15 | return null; 16 | } 17 | const user = ctx.state.user as User; 18 | // If survey does not belong to current user, return with 403 19 | if (survey.userId !== user.id) { 20 | ctx.response.status = 403; 21 | ctx.response.body = { 22 | message: "You don't have permission to view this survey", 23 | }; 24 | return null; 25 | } 26 | return survey; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /controllers/QuestionController.ts: -------------------------------------------------------------------------------- 1 | import { RouterContext } from "../deps.ts"; 2 | import Question from "../models/Question.ts"; 3 | import BaseSurveyController from "./BaseSurveyController.ts"; 4 | 5 | export class QuestionController extends BaseSurveyController { 6 | async getBySurvey(ctx: RouterContext) { 7 | const surveyId: string = ctx.params.surveyId!; 8 | const survey = await this.findSurveyOrFail(surveyId, ctx); 9 | if (survey) { 10 | ctx.response.body = await Question.findBySurvey(surveyId); 11 | } 12 | } 13 | 14 | async getSingle(ctx: RouterContext) { 15 | const id: string = ctx.params.id!; 16 | const question: Question | null = await Question.findOne(id); 17 | if (!question) { 18 | ctx.response.status = 404; 19 | ctx.response.body = { message: "Invalid Question ID" }; 20 | return; 21 | } 22 | ctx.response.body = question; 23 | } 24 | 25 | async create(ctx: RouterContext) { 26 | const surveyId: string = ctx.params.surveyId!; 27 | const survey = this.findSurveyOrFail(surveyId, ctx); 28 | if (!survey) { 29 | return; 30 | } 31 | const { value: {text, type, required, data} } = await ctx.request.body(); 32 | const question = new Question(surveyId, text, type, required, data); 33 | await question.create(); 34 | ctx.response.status = 201; 35 | ctx.response.body = question; 36 | } 37 | 38 | async update(ctx: RouterContext) { 39 | const id: string = ctx.params.id!; 40 | const { value: {text, type, required, data} } = await ctx.request.body(); 41 | const question: Question | null = await Question.findOne(id); 42 | if (!question) { 43 | ctx.response.status = 404; 44 | ctx.response.body = { message: "Invalid Question ID" }; 45 | return; 46 | } 47 | await question.update(text, type, required, data); 48 | ctx.response.body = question; 49 | } 50 | 51 | async delete(ctx: RouterContext) { 52 | const id: string = ctx.params.id!; 53 | const question: Question | null = await Question.findOne(id); 54 | if (!question) { 55 | ctx.response.status = 404; 56 | ctx.response.body = { message: "Invalid Question ID" }; 57 | return; 58 | } 59 | await question.delete(); 60 | ctx.response.status = 204; 61 | } 62 | } 63 | 64 | export default new QuestionController(); 65 | -------------------------------------------------------------------------------- /controllers/SiteController.ts: -------------------------------------------------------------------------------- 1 | import { RouterContext } from "../deps.ts"; 2 | import { renderView } from "../helpers.ts"; 3 | import Survey from "../models/Survey.ts"; 4 | import Question, { QuestionType } from "../models/Question.ts"; 5 | import { answerCollection } from "../mongo.ts"; 6 | 7 | export class SiteController { 8 | async surveys(ctx: RouterContext) { 9 | const surveys = await Survey.findAll(); 10 | ctx.response.body = await renderView("surveys", { 11 | surveys: surveys, 12 | }); 13 | } 14 | async viewSurvey(ctx: RouterContext) { 15 | const id: string = ctx.params.id!; 16 | const survey = await Survey.findOne(id); 17 | if (!survey) { 18 | ctx.response.body = await renderView("notfound"); 19 | return; 20 | } 21 | const questions = await Question.findBySurvey(id); 22 | 23 | ctx.response.body = await renderView("survey", { 24 | survey, 25 | questions, 26 | errors: {}, 27 | answers: {}, 28 | }); 29 | } 30 | 31 | async submitSurvey(ctx: RouterContext) { 32 | const id: string = ctx.params.id!; 33 | const survey = await Survey.findOne(id); 34 | if (!survey) { 35 | ctx.response.body = await renderView("notfound"); 36 | return; 37 | } 38 | const { value: formData } = await ctx.request.body(); 39 | const questions: Question[] = await Question.findBySurvey(id); 40 | const answers: any = {}; 41 | const errors: any = {}; 42 | console.log(formData); 43 | for (const question of questions) { 44 | let value = formData.get(question.id); 45 | if (question.isChoice() && question.data.multiple) { 46 | value = formData.getAll(question.id); 47 | } 48 | if (question.required) { 49 | if (!value || question.isChoice() && question.data.multiple && !value.length) { 50 | errors[question.id] = "This field is required"; 51 | } 52 | } 53 | answers[question.id] = value; 54 | } 55 | if (Object.keys(errors).length > 0) { 56 | ctx.response.body = await renderView("survey", { 57 | survey, 58 | questions, 59 | errors, 60 | answers, 61 | }); 62 | return; 63 | } 64 | const { $oid } = await answerCollection.insertOne({ 65 | surveyId: id, 66 | date: new Date(), 67 | userAgent: ctx.request.headers.get("User-Agent"), 68 | answers, 69 | }); 70 | ctx.response.body = await renderView("surveyFinish", { 71 | answerId: $oid, 72 | }); 73 | } 74 | } 75 | 76 | export default new SiteController(); 77 | -------------------------------------------------------------------------------- /controllers/SurveyController.ts: -------------------------------------------------------------------------------- 1 | import { RouterContext } from "../deps.ts"; 2 | import Survey from "../models/Survey.ts"; 3 | import User from "../models/User.ts"; 4 | import BaseSurveyController from "./BaseSurveyController.ts"; 5 | 6 | export class SurveyController extends BaseSurveyController { 7 | async getAll(ctx: RouterContext) { 8 | const user: User = ctx.state.user as User; 9 | const surveys = await Survey.findByUser(user.id); 10 | ctx.response.body = surveys; 11 | } 12 | 13 | async getSingle(ctx: RouterContext) { 14 | const id: string = ctx.params.id!; 15 | let survey: Survey | null = await this.findSurveyOrFail(id, ctx); 16 | if (survey) { 17 | ctx.response.body = survey; 18 | } 19 | } 20 | 21 | async create(ctx: RouterContext) { 22 | const { value: { name, description } } = await ctx.request.body(); 23 | 24 | const user = ctx.state.user as User; 25 | const survey = new Survey(user.id, name, description); 26 | await survey.create(); 27 | ctx.response.status = 201; 28 | ctx.response.body = survey; 29 | } 30 | 31 | async update(ctx: RouterContext) { 32 | const id: string = ctx.params.id!; 33 | const { value: { name, description } } = await ctx.request.body(); 34 | const survey: Survey | null = await this.findSurveyOrFail(id, ctx); 35 | if (survey) { 36 | await survey.update({ name, description }); 37 | ctx.response.body = survey; 38 | } 39 | } 40 | 41 | async delete(ctx: RouterContext) { 42 | const id: string = ctx.params.id!; 43 | const survey: Survey | null = await this.findSurveyOrFail(id, ctx); 44 | if (survey) { 45 | await survey.delete(); 46 | ctx.response.status = 204; 47 | } 48 | } 49 | } 50 | 51 | export default new SurveyController(); 52 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Application, 3 | Router, 4 | RouterContext, 5 | Context, 6 | send, 7 | } from "https://deno.land/x/oak@v5.2.0/mod.ts"; 8 | export { MongoClient } from "https://deno.land/x/mongo@v0.8.0/mod.ts"; 9 | export { 10 | hashSync, 11 | compareSync, 12 | } from "https://deno.land/x/bcrypt@v0.2.1/mod.ts"; 13 | import "https://deno.land/x/dotenv@v0.4.3/load.ts"; 14 | export { 15 | makeJwt, 16 | setExpiration, 17 | Jose, 18 | Payload, 19 | } from "https://deno.land/x/djwt@v0.9.0/create.ts"; 20 | export { 21 | validateJwt, 22 | JwtObject, 23 | } from "https://deno.land/x/djwt@v0.9.0/validate.ts"; 24 | -------------------------------------------------------------------------------- /helpers.ts: -------------------------------------------------------------------------------- 1 | import { renderFileToString } from "https://deno.land/x/dejs@0.7.0/mod.ts"; 2 | 3 | 4 | export const renderView = (view: string, params: object = {}) => { 5 | return renderFileToString(`${Deno.cwd()}/views/${view}.ejs`, params) 6 | } 7 | 8 | export const fileExists = async (filename: string): Promise => { 9 | try { 10 | const stats = await Deno.stat(filename); 11 | return stats && stats.isFile; 12 | } catch (error) { 13 | if (error && error instanceof Deno.errors.NotFound) { 14 | // file or directory does not exist 15 | return false; 16 | } else { 17 | // unexpected error, maybe permissions, pass it along 18 | throw error; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://deno.land/x/bcrypt@v0.2.1/main.ts": "4ac3ccc2247ce729f6c7d57654909949b62aafdabd996483c97f34aa1afac82a", 3 | "https://deno.land/std@0.57.0/testing/diff.ts": "77338e2b479626c096278d7b4a95f750753ee39558f314db6b794e8d8a98d515", 4 | "https://deno.land/x/oak@v5.2.0/buf_reader.ts": "ced6a04ac8f529a18e1e85182ffe0cf82ee9477e1a8f5441a02e923f62bebc54", 5 | "https://raw.githubusercontent.com/chiefbiiko/sha256/v1.0.2/deps.ts": "2e1af51a48c8507017fdb057b950366601b177fb7e73d5de54c1b3e0e115d72e", 6 | "https://deno.land/std@v0.50.0/path/interface.ts": "89f6e68b0e3bba1401a740c8d688290957de028ed86f95eafe76fe93790ae450", 7 | "https://deno.land/x/mongo@v0.8.0/ts/types.ts": "2dca2a49354fe98a8c4795000eee7c956f3699ff00a146d46b3c1f85ccc30415", 8 | "https://deno.land/std@0.57.0/path/_util.ts": "feacfbe104df3baf2a08ca80263d6a9cf63f9c32e7c9cbe54602ec12864a72e9", 9 | "https://deno.land/std@0.57.0/path/_constants.ts": "e11f32a36644e04dce243f3c1f30c02cc5d149827f48a755628176f0707cfc70", 10 | "https://deno.land/x/djwt@v0.9.0/create.ts": "b9dae152468f2c47a88e45f8462ac8add6fcfc66391668590bf617b83a4e41b1", 11 | "https://deno.land/std@0.57.0/fmt/colors.ts": "06444b6ebc3842a4b2340d804bfa81fc5452a03513cbb81bd7f6bf8f4b8f3ac4", 12 | "https://deno.land/std@0.57.0/_util/has_own_property.ts": "91a2ef2fae5d941643a6472b97cac7ed8060a96deb66f4fafb4a2750c8af18e5", 13 | "https://deno.land/std@0.57.0/testing/asserts.ts": "a9f22a8e078b943f6b71a66de7f8afd3428cb6fa6fd7171e79b713b47b374ae4", 14 | "https://deno.land/std@0.57.0/hash/sha1.ts": "c1a97bde767b98b88495470f39c30c37e44ac3984409f120ef65fd84c9d27608", 15 | "https://deno.land/x/base64/base.ts": "5c03afb3d48787006eb15dba6aa3045f65d4da8be80573fc7731c8d9b187a27d", 16 | "https://deno.land/std@v0.50.0/path/_util.ts": "b678a7ecbac6b04c1166832ae54e1024c0431dd2b340b013c46eb2956ab24d4c", 17 | "https://deno.land/std@v0.50.0/path/separator.ts": "7bdb45c19c5c934c49c69faae861b592ef17e6699a923449d3eaaf83ec4e7919", 18 | "https://deno.land/std@v0.50.0/testing/diff.ts": "8f591074fad5d35c0cafa63b1c5334dc3a17d5b934f3b9e07172eed9d5b55553", 19 | "https://raw.githubusercontent.com/chiefbiiko/hmac/master/deps.ts": "fb061e6fa0fd96f455ef647c57e9e6309a86fb0bf484447fc6cb38e57262192f", 20 | "https://deno.land/std@v0.50.0/path/mod.ts": "a789541f8df9170311daa98313c5a76c06b5988f2948647957b3ec6e017d963e", 21 | "https://deno.land/x/mongo@v0.8.0/ts/database.ts": "3dc54b0a4fe4e09d4f9457fb4a1d0cfb5d7a4611a2ad0b5efcf4d4737494a70b", 22 | "https://deno.land/x/mongo@v0.8.0/mod.ts": "bb1054073b6526126336b9dadcc89366facca6ce0bddb9cf3dc5fcf80dd677b7", 23 | "https://deno.land/x/oak@v5.2.0/deps.ts": "b320f13ae98427845c50b960b192847a2d007f72a51ab4b36a4936f81734a5da", 24 | "https://deno.land/std@0.57.0/_util/assert.ts": "e1f76e77c5ccb5a8e0dbbbe6cce3a56d2556c8cb5a9a8802fc9565af72462149", 25 | "https://deno.land/x/mongo@v0.8.0/ts/type_convert.ts": "6860a7acbbe0a950b0e0a811f668ac2708225c4891ff986a1d281cd963c9bbcb", 26 | "https://deno.land/x/oak@v5.2.0/send.ts": "3f937db7cbf2643ca8792b30b37400a57457385efa4a3cea6f484dd0c33b2877", 27 | "https://deno.land/std@0.57.0/io/ioutil.ts": "1e495019c3bf0510851cea66000b397929151a16bd1f158eff3767f157cb7781", 28 | "https://deno.land/x/oak@v5.2.0/isMediaType.ts": "77b94675822d7de7f6af21bb765ed9f25b956c7ac4d292dd66c2ecb9ea9478e5", 29 | "https://deno.land/x/oak@v5.2.0/cookies.ts": "af291cb64ae0ccc40e6d777980d89d694e971131222130b1b11e9200b9cae6f1", 30 | "https://deno.land/std/encoding/hex.ts": "fa2206fb59cd5098dacaa82f72d1400d763d1497be7e6d5bed3ef964e03961e1", 31 | "https://deno.land/std@0.57.0/encoding/utf8.ts": "8654fa820aa69a37ec5eb11908e20b39d056c9bf1c23ab294303ff467f3d50a1", 32 | "https://deno.land/x/oak@v5.2.0/mediaTyper.ts": "042b853fc8e9c3f6c628dd389e03ef481552bf07242efc3f8a1af042102a6105", 33 | "https://deno.land/std@0.57.0/path/posix.ts": "514c39f5f7ca45bee2ace2a2e83da74dde5ff08fba837c71f86f459abe846fd7", 34 | "https://raw.githubusercontent.com/pillarjs/path-to-regexp/v6.1.0/src/index.ts": "8747ddaf85a761df4059cbc48a78cf710106f6b4c9d6932feb4d35120ac0990a", 35 | "https://deno.land/x/oak@v5.2.0/content_disposition.ts": "1504bb85669e6c7e6dd30210fa7e73c55aa40195ed40a090f4ad58568c2b9882", 36 | "https://deno.land/x/oak@v5.2.0/negotiation/language.ts": "62ef13ea3146538dd52a4666611bd423ebb9a6438e7312398e17a4d16dbafb51", 37 | "https://deno.land/x/plugin_prepare@v0.6.0/mod.ts": "9362e4cae19b3c1b7abf8ecbec27062eeaad2a57aa133008fdfe982f3a96f03c", 38 | "https://deno.land/x/checksum@1.4.0/mod.ts": "df8282cc1ecfd0f148535337c8bec077a1630e96a2012f2d19b2ca380f2c88c2", 39 | "https://deno.land/std@0.57.0/async/mux_async_iterator.ts": "e2a4c2c53aee22374b493b88dfa08ad893bc352c8aeea34f1e543e938ec6ccc6", 40 | "https://deno.land/std@0.57.0/http/http_status.ts": "0ecc0799a208f49452023b1b927106ba5a2c13cc6cf6666345db028239d554ab", 41 | "https://deno.land/x/checksum@1.4.0/hash.ts": "89ebbf7c57576e20462badc05bfb4a7ac01fe7668b6908cd547c79067c4221b9", 42 | "https://deno.land/x/mongo@v0.8.0/deps.ts": "51355f8cb4f7c691c39eddaf65d5c879f1b35c9af80f9e7388b5702a8d470ddf", 43 | "https://deno.land/x/bcrypt@v0.2.1/bcrypt/base64.ts": "b7becddff81ffe026f8caca4128ed86ddc67ebd3d5c5eb7c92673f5f576cc383", 44 | "https://deno.land/x/dotenv@v0.4.3/mod.ts": "cc95dbb33a9cb3f29a69785f0e2eb8d1ad71a4e4895cd61290eeb81fb4be89ee", 45 | "https://deno.land/x/checksum@1.4.0/sha1.ts": "a608a6493694b7d734acfc4eee83e09bd9deaa4909cdac07f3eed3db427f97ae", 46 | "https://deno.land/x/oak@v5.2.0/multipart.ts": "562263735b910cf6927052afe492459e284b2073d3e04dd0bf45b863513c9be3", 47 | "https://deno.land/x/oak@v5.2.0/response.ts": "48629fd9c36978e1d61683055ad6fb5e01109623fb9d24f21ec7d921d5e4d9cb", 48 | "https://deno.land/x/bcrypt@v0.2.1/bcrypt/bcrypt.ts": "54cfa700780904dc207ec10d342c2f906d7e6dbf5f48e665f22cf939d47e20f9", 49 | "https://deno.land/std@0.57.0/path/mod.ts": "6de8885c2534757097818e302becd1cefcbc4c28ac022cc279e612ee04e8cfd1", 50 | "https://deno.land/x/oak@v5.2.0/application.ts": "9a6fa0c9f8fd94549d748c29d0030f70dc60d88c3e023d4eb7179f53adec6927", 51 | "https://deno.land/std@0.57.0/path/separator.ts": "9dd15d46ff84a16e13554f56af7fee1f85f8d0f379efbbe60ac066a60561f036", 52 | "https://raw.githubusercontent.com/chiefbiiko/sha256/v1.0.2/mod.ts": "f109aa5eeb201a0cbfaf44950b70a429838b187e8a84c5b810c1ac84953427cc", 53 | "https://deno.land/std@0.57.0/path/win32.ts": "91dfd12ddaabec7fa4a8052e16ec56a260722a505e3bb8dfc35041651af38322", 54 | "https://deno.land/x/media_types@v2.3.6/deps.ts": "12efcf144eb028dd7d6d8ada3f2df8098299fafaaeaed343ed9978ee08511190", 55 | "https://deno.land/x/mongo@v0.8.0/ts/util.ts": "f2b5097c239bd05a4d2665eb7b74bf74266fc5a9d1e9185ab2f5bdb9b5191b48", 56 | "https://deno.land/x/oak@v5.2.0/context.ts": "49febd7c7bef317077061147c3c9337d1d13f8681ef84059ea02accf47dd23a3", 57 | "https://raw.githubusercontent.com/chiefbiiko/std-encoding/v1.0.0/mod.ts": "4a927e5cd1d9b080d72881eb285b3b94edb6dadc1828aeb194117645f4481ac0", 58 | "https://deno.land/x/media_types@v2.3.6/db.ts": "56a9deab286b6283e1df021d74ee3319353f27f7827716b6443427fff2fc6e24", 59 | "https://raw.githubusercontent.com/chiefbiiko/sha1/v1.0.3/deps.ts": "2e1af51a48c8507017fdb057b950366601b177fb7e73d5de54c1b3e0e115d72e", 60 | "https://deno.land/x/djwt@v0.9.0/base64/base64.ts": "38ccd234d7b6146572adcd56dda73eb708725d89105182d3f3a975ba60b27387", 61 | "https://deno.land/x/dotenv@v0.4.3/load.ts": "4eba9b76084c302d714c34a4976bbd91d0015faf9f219d8c8f8e8da0adeccdd5", 62 | "https://deno.land/std@0.57.0/io/bufio.ts": "cf9f26b688d615f9e9fe51a43ca9aec67597dff3598642832672d47b269793bb", 63 | "https://deno.land/x/oak@v5.2.0/server_sent_event.ts": "7848724f2ed2e388a3531986c4c71468b86216b1ad494eb1a6e373ce1710c79a", 64 | "https://deno.land/x/mongo@v0.8.0/ts/collection.ts": "147f7d5f4f3003d748dc9f4f2125cb23ac9ade89f9db72cf29f5bfc273342b72", 65 | "https://deno.land/std@0.57.0/http/_io.ts": "7a31bf7d0167685d8d0ff4eca93b558cd26539695d1ddbd715ee28bf1dc7db17", 66 | "https://deno.land/x/oak@v5.2.0/router.ts": "24c0896fd36924850be1c4b4254177bf570003791bdf8f49bc8b271181e563ac", 67 | "https://deno.land/std@0.57.0/async/deferred.ts": "ac95025f46580cf5197928ba90995d87f26e202c19ad961bc4e3177310894cdc", 68 | "https://deno.land/std@v0.50.0/log/handlers.ts": "7bad686e2ca4148eb7edfd3b29dafdf0ff5dbf4a414d2e99f4003d13a6830827", 69 | "https://deno.land/x/djwt@v0.9.0/base64/base64url.ts": "f37fb7955239d84e1272ae45bf1316979d9e6c965e09f68f1e6b06fb601d4e47", 70 | "https://deno.land/x/dotenv@v0.4.3/util.ts": "15875ce1e2840d3d3c7b0f74d699218be065c23accd0737993cbf51ea216d0c5", 71 | "https://deno.land/std@0.57.0/hash/sha256.ts": "1aedfb09bcb067929de6ad78f3b074b802ec94ec96c8a07b80475d90942cb221", 72 | "https://deno.land/std@v0.50.0/path/common.ts": "95115757c9dc9e433a641f80ee213553b6752aa6fbb87eb9f16f6045898b6b14", 73 | "https://deno.land/std@0.57.0/http/server.ts": "a2b7212870fda43eb419dd86e4df66722fbc3b3c452d531264ad8a6745798678", 74 | "https://deno.land/x/oak@v5.2.0/request.ts": "546cc0fe0f6fca82da2a5dc2169ab85fde7c1e025a80b20a3acd685216af2cbf", 75 | "https://deno.land/std@0.57.0/ws/mod.ts": "bdc837695f305b8cb7dae8e81b1660a38745da2e1e6e2fb68ed6e9654fdaf809", 76 | "https://deno.land/x/oak@v5.2.0/negotiation/charset.ts": "b4c2e0c49dd5122f130f95bf29508448d983c424801b5bc304b00288b5ae3195", 77 | "https://deno.land/std@0.57.0/async/mod.ts": "bf46766747775d0fc4070940d20d45fb311c814989485861cdc8a8ef0e3bbbab", 78 | "https://deno.land/std@v0.50.0/path/win32.ts": "61248a2b252bb8534f54dafb4546863545e150d2016c74a32e2a4cfb8e061b3f", 79 | "https://deno.land/x/djwt@v0.9.0/validate.ts": "ebf2b7af381a0189d2f61035950ab5bb8dc585ba0988da55c2d521c1a79fae79", 80 | "https://deno.land/x/oak@v5.2.0/negotiation/mediaType.ts": "7e25cc34600beea3bf0b0879ff1783c752260fcb517dffea2e122830c36e8451", 81 | "https://deno.land/std@v0.50.0/log/logger.ts": "6328f806b20eeb0d36723aa2e785968a68c1e95ea6ac67757ed6047ac04ee150", 82 | "https://deno.land/std/encoding/utf8.ts": "8654fa820aa69a37ec5eb11908e20b39d056c9bf1c23ab294303ff467f3d50a1", 83 | "https://deno.land/x/oak@v5.2.0/headers.ts": "cd42a5d5521979d11feb3431f34a6c8bd5c39e3feb2b121163724e2cbaf5e25a", 84 | "https://deno.land/std@v0.50.0/log/levels.ts": "d274b41f2dc909c6eef4e71c8a50ccbc81ea4e1df28cb268f02942966a205593", 85 | "https://deno.land/x/checksum@1.4.0/md5.ts": "1bb0889eaec838d5c1f219e0533277fe5ea10806d022920b6f0f2f8c398fec77", 86 | "https://raw.githubusercontent.com/chiefbiiko/sha512/v1.0.3/mod.ts": "33190babd4c0e748bb2568fd003444487c8798c8c3a618f6593098c12805fe15", 87 | "https://deno.land/x/oak@v5.2.0/keyStack.ts": "a490066c90cbfea6b7de73303b9530e68ffbe86ff48b99ff17dab1cc9cf0751f", 88 | "https://deno.land/x/oak@v5.2.0/negotiation/common.ts": "f54d599d37408005f8c565d0f6505de51fed31feaa3654a7758e2359c006b02c", 89 | "https://deno.land/std@v0.50.0/testing/asserts.ts": "213fedbb90a60ae232932c45bd62668f0c5cd17fc0f2a273e96506cba416d181", 90 | "https://raw.githubusercontent.com/chiefbiiko/sha1/v1.0.3/mod.ts": "146a101c9776cc9c807053c93f23e4b321ade5251f65745df418f4a75d5fd27b", 91 | "https://deno.land/std@v0.50.0/path/posix.ts": "b742fe902d5d6821c39c02319eb32fc5a92b4d4424b533c47f1a50610afbf381", 92 | "https://deno.land/x/oak@v5.2.0/util.ts": "4db758c7ebc4902d79df495cc0c9aec3199f437d8fb190650703508dd7356ab6", 93 | "https://deno.land/std@0.57.0/textproto/mod.ts": "baf452274ed76c703845a6f48757bd107215bb513b424a0672549d4b6c879153", 94 | "https://deno.land/x/oak@v5.2.0/negotiation/encoding.ts": "b6b351150dfaa37d43c1e4679f0afa7ee70c2fa775d8bc8b9e9baf15bcf65888", 95 | "https://deno.land/x/mongo@v0.8.0/ts/client.ts": "ea54c268e2d0ac585dbae5ff8b7f1a43f19832223ef5631f2896c9473ef00846", 96 | "https://deno.land/x/bcrypt@v0.2.1/mod.ts": "6ed1ba413208fa23109f5055fc58eb5f62fc6cee863142b66e7b90be3f8bab4b", 97 | "https://deno.land/x/mongo@v0.8.0/ts/result.ts": "11ba6400f8e60b9981a7c2176311caedeae8c6197cc3e3a14575d220821e6795", 98 | "https://deno.land/std@v0.50.0/fmt/colors.ts": "127ce39ca2ad9714d4ada8d61367f540d76b5b0462263aa839166876b522d3de", 99 | "https://raw.githubusercontent.com/chiefbiiko/sha512/v1.0.3/deps.ts": "2e1af51a48c8507017fdb057b950366601b177fb7e73d5de54c1b3e0e115d72e", 100 | "https://deno.land/std@0.57.0/path/_interface.ts": "5876f91d35fd42624893a4aaddaee352144e1f46e719f1dde6511bab7c3c1029", 101 | "https://deno.land/std@v0.50.0/path/glob.ts": "ab85e98e4590eae10e561ce8266ad93ebe5af2b68c34dc68b85d9e25bccb4eb7", 102 | "https://deno.land/std@0.57.0/bytes/mod.ts": "5ad1325fc232f19b59fefbded30013b4bcd39fee4cf1eee73d6a6915ae46bdcd", 103 | "https://deno.land/std@0.57.0/path/glob.ts": "fbce75b7714db355dca9abd0df107177dd3b7b76110e7ab8b783020f1e6ef895", 104 | "https://deno.land/std@v0.50.0/path/_globrex.ts": "30146377e2c49ea49f049a18b7d3a0504150ff29509d28f4938ef2711cf38302", 105 | "https://deno.land/x/oak@v5.2.0/httpError.ts": "7e09654eb5e0843beca87934b9fea7a8af79f9da0cd15c220197e7c619a96708", 106 | "https://deno.land/x/media_types@v2.3.6/mod.ts": "94141d7c415fcdad350fec9d36d77c18334efe25766db6f37d34709c896881ed", 107 | "https://deno.land/std@0.57.0/path/_globrex.ts": "696996deed47bd98fcab9de0034233380ead443a5cd702631151c6a71e3d21a2", 108 | "https://deno.land/x/oak@v5.2.0/mod.ts": "e67bda9bef154da8dd1c93575cf69cc505f9f6f54317fc73017e6b2e1904d5e0", 109 | "https://deno.land/std@0.57.0/async/delay.ts": "35957d585a6e3dd87706858fb1d6b551cb278271b03f52c5a2cb70e65e00c26a", 110 | "https://deno.land/std@v0.50.0/log/mod.ts": "13060c69f4cc4413f2c9527d811a2ea2b186897f0680f04417d8ea6816e2da4f", 111 | "https://deno.land/x/plugin_prepare@v0.6.0/deps.ts": "6c56dcef29f508a9b94c0a74c68c534b9ebfc59bd1d76f5ca94675971a3da9dd", 112 | "https://deno.land/std@v0.50.0/path/_constants.ts": "f6c332625f21d49d5a69414ba0956ac784dbf4b26a278041308e4914ba1c7e2e", 113 | "https://deno.land/x/base64/base64url.ts": "08c12a021df3830eb21f97ab52a6b339650063d575f50af2d6c08c35a5f950dc", 114 | "https://raw.githubusercontent.com/chiefbiiko/hmac/master/mod.ts": "83590b95de468d0cf5398b7631b4fb62a64226786247fe265482045d799e8f97", 115 | "https://deno.land/x/oak@v5.2.0/tssCompare.ts": "e72cc0039d561bf2a56746194cf647376d686f9dcdc4a500285631ab6fb9c774", 116 | "https://deno.land/x/oak@v5.2.0/helpers.ts": "ffc907c49368dcbe591f0bfee122a354b3a66def9621de48e06bbbdefe913b3d", 117 | "https://deno.land/std@0.57.0/path/common.ts": "e4ec66a7416d56f60331b66e27a8a4f08c7b1cf48e350271cb69754a01cf5c04", 118 | "https://deno.land/x/oak@v5.2.0/middleware.ts": "b4959e727eee68aa3a15468044a7a7610bbfacbb0142232e0385dac36db1865e", 119 | "https://deno.land/std@v0.50.0/fs/exists.ts": "6012ed95a822629401133435111d58fb07c265d19d463bf3a31be8f0bf8f1dff", 120 | "https://deno.land/std@0.57.0/io/util.ts": "1a87070007cbfc279020d747952822901faea36126dab4bf1c7e004140a07b37" 121 | } -------------------------------------------------------------------------------- /middleware/authMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { RouterContext, validateJwt } from "../deps.ts"; 2 | import User from "../models/User.ts"; 3 | 4 | export const authMiddleware = async (ctx: RouterContext, next: Function) => { 5 | const headers = ctx.request.headers; 6 | 7 | const authHeader = headers.get("Authorization"); 8 | if (!authHeader) { 9 | ctx.response.status = 401; 10 | return; 11 | } 12 | const jwt = authHeader.split(" ")[1]; 13 | if (!jwt) { 14 | ctx.response.status = 401; 15 | return; 16 | } 17 | const data = await validateJwt( 18 | jwt, 19 | Deno.env.get("JWT_SECRET_KEY") || "", 20 | { isThrowing: false }, 21 | ); 22 | if (data) { 23 | const user = await User.findOne({ email: data.payload?.iss }); 24 | ctx.state.user = user; 25 | await next(); 26 | } else { 27 | ctx.response.status = 401; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /middleware/staticFileMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { send, Context } from "../deps.ts"; 2 | import { fileExists } from "../helpers.ts"; 3 | 4 | export const staticFileMiddleware = async ( 5 | context: Context, 6 | next: Function, 7 | ) => { 8 | const path = `${Deno.cwd()}/assets${context.request.url.pathname}`; 9 | if (await fileExists(path)) { 10 | await send(context, context.request.url.pathname, { 11 | root: `${Deno.cwd()}/assets`, 12 | }); 13 | } else { 14 | await next(); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /models/BaseModel.ts: -------------------------------------------------------------------------------- 1 | export default class BaseModel { 2 | public static prepare(data: any) { 3 | data.id = data._id.$oid; 4 | delete data._id; 5 | return data; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /models/Question.ts: -------------------------------------------------------------------------------- 1 | import { questionCollection } from "../mongo.ts"; 2 | import BaseModel from "./BaseModel.ts"; 3 | 4 | export default class Question extends BaseModel { 5 | public id: string = ""; 6 | constructor( 7 | public surveyId: string, 8 | public text: string, 9 | public type: QuestionType, 10 | public required: boolean, 11 | public data: any, 12 | ) { 13 | super(); 14 | } 15 | 16 | static async findBySurvey(surveyId: string): Promise { 17 | const questions = await questionCollection.find({ surveyId }); 18 | if (!questions) { 19 | return []; 20 | } 21 | return questions.map((q: object) => Question.prepare(q)); 22 | } 23 | 24 | static async findOne(id: string): Promise { 25 | const question = await questionCollection.findOne({ _id: { $oid: id } }); 26 | if (!question) { 27 | return null; 28 | } 29 | return Question.prepare(question); 30 | } 31 | 32 | async create() { 33 | delete this.id; 34 | const { $oid } = await questionCollection.insertOne(this); 35 | this.id = $oid; 36 | return this; 37 | } 38 | 39 | public async update( 40 | text: string, 41 | type: QuestionType, 42 | required: boolean, 43 | data: any, 44 | ) { 45 | this.text = text; 46 | this.type = type; 47 | this.required = required; 48 | this.data = data; 49 | await questionCollection.updateOne( 50 | { _id: { $oid: this.id } }, 51 | { 52 | $set: { 53 | text: this.text, 54 | type: this.type, 55 | required: this.required, 56 | data: this.data, 57 | }, 58 | }, 59 | ); 60 | return this; 61 | } 62 | 63 | async delete() { 64 | return questionCollection.deleteOne({ _id: { $oid: this.id } }); 65 | } 66 | 67 | isText() { 68 | return this.type === QuestionType.TEXT; 69 | } 70 | 71 | isChoice() { 72 | return this.type === QuestionType.CHOICE; 73 | } 74 | 75 | static prepare(data: any): Question { 76 | data = BaseModel.prepare(data); 77 | const question = new Question( 78 | data.surveyId, 79 | data.text, 80 | data.type, 81 | data.required, 82 | data.data, 83 | ); 84 | question.id = data.id; 85 | return question; 86 | } 87 | } 88 | 89 | export enum QuestionType { 90 | TEXT = "text", 91 | CHOICE = "choice", 92 | } 93 | -------------------------------------------------------------------------------- /models/Survey.ts: -------------------------------------------------------------------------------- 1 | import { surveyCollection } from "../mongo.ts"; 2 | import BaseModel from "./BaseModel.ts"; 3 | 4 | export default class Survey extends BaseModel { 5 | public id: string = ""; 6 | 7 | constructor( 8 | public userId: string, 9 | public name: string, 10 | public description: string, 11 | ) { 12 | super(); 13 | this.userId = userId; 14 | this.name = name; 15 | this.description = description; 16 | } 17 | 18 | static async findAll(): Promise { 19 | const surveys = await surveyCollection.find(); 20 | return surveys.map((survey: any) => Survey.prepare(survey)); 21 | } 22 | 23 | static async findByUser(userId: string): Promise { 24 | const surveys = await surveyCollection.find({ userId }); 25 | return surveys.map((survey: object) => Survey.prepare(survey)); 26 | } 27 | 28 | static async findOne(id: string): Promise { 29 | const survey = await surveyCollection.findOne({ _id: { $oid: id } }); 30 | if (!survey) { 31 | return null; 32 | } 33 | return Survey.prepare(survey); 34 | } 35 | 36 | async create() { 37 | delete this.id; 38 | const { $oid } = await surveyCollection.insertOne(this); 39 | this.id = $oid; 40 | return this; 41 | } 42 | 43 | async update({ name, description }: { name: string; description: string }) { 44 | const { modifiedCount } = await surveyCollection 45 | .updateOne({ _id: { $oid: this.id } }, { 46 | $set: { name, description }, 47 | }); 48 | 49 | if (modifiedCount > 0) { 50 | this.name = name; 51 | this.description = description; 52 | } 53 | return this; 54 | } 55 | 56 | delete() { 57 | return surveyCollection.deleteOne({ _id: { $oid: this.id } }); 58 | } 59 | 60 | static prepare(data: any): Survey { 61 | data = BaseModel.prepare(data); 62 | const survey = new Survey( 63 | data.userId, 64 | data.name, 65 | data.description, 66 | ); 67 | survey.id = data.id; 68 | return survey; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /models/User.ts: -------------------------------------------------------------------------------- 1 | import { userCollection } from "../mongo.ts"; 2 | import BaseModel from "./BaseModel.ts"; 3 | 4 | export default class User extends BaseModel { 5 | public id: string = ""; 6 | public name: string = ""; 7 | public email: string = ""; 8 | public password: string = ""; 9 | 10 | constructor({ id = "", name = "", email = "", password = "" }) { 11 | super(); 12 | this.id = id; 13 | this.name = name; 14 | this.email = email; 15 | this.password = password; 16 | } 17 | 18 | static async findOne(params: object): Promise { 19 | const user = await userCollection.findOne(params); 20 | if (!user) { 21 | return null; 22 | } 23 | return new User(User.prepare(user)); 24 | } 25 | 26 | async save() { 27 | delete this.id; 28 | const { $oid } = await userCollection.insertOne(this); 29 | this.id = $oid; 30 | return this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /mongo.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from "./deps.ts"; 2 | 3 | const client = new MongoClient(); 4 | client.connectWithUri(Deno.env.get("MONGODB_URI") || ""); 5 | 6 | const db = client.database(Deno.env.get("DB_NAME") || ""); 7 | export default db; 8 | 9 | export const userCollection = db.collection("users"); 10 | export const surveyCollection = db.collection("surveys"); 11 | export const questionCollection = db.collection("questions"); 12 | export const answerCollection = db.collection("answers"); 13 | -------------------------------------------------------------------------------- /rest.http.example: -------------------------------------------------------------------------------- 1 | ### Register 2 | 3 | POST http://localhost:8000/api/register HTTP/1.1 4 | content-type: application/json 5 | 6 | { 7 | "name": "freecodecamp", 8 | "email": "zura@freecodecamp.com", 9 | "password": "zura" 10 | } 11 | 12 | ### Login 13 | 14 | POST http://localhost:8000/api/login 15 | Content-Type: application/json 16 | 17 | { 18 | "email": "zura@freecodecamp.com", 19 | "password": "zura" 20 | } 21 | 22 | @token = PUT_ACCESS_TOKEN_HERE 23 | 24 | ############################################### 25 | ### SURVEYS 26 | ############################################### 27 | 28 | 29 | ### Get All Surveys 30 | GET http://localhost:8000/api/survey 31 | Authorization: Bearer {{token}} 32 | 33 | ### Get Single Survey 34 | GET http://localhost:8000/api/survey/5ee076c2001a4de900e82549 35 | Authorization: Bearer {{token}} 36 | 37 | ### Create Survey 38 | POST http://localhost:8000/api/survey 39 | Content-Type: application/json 40 | Authorization: Bearer {{token}} 41 | 42 | { 43 | "name": "Deno Course", 44 | "description": "We want to understand how much you liked the Deno course" 45 | } 46 | 47 | ### Update survey 48 | PUT http://localhost:8000/api/survey/5edfdfb000f3102800ad0218 49 | Content-Type: application/json 50 | Authorization: Bearer {{token}} 51 | 52 | { 53 | "name": "Deno Course", 54 | "description": "We want to understand how much you liked the Deno course" 55 | } 56 | 57 | ### Delete the survey 58 | DELETE http://localhost:8000/api/survey/5eddbe7900458e0100542d8c 59 | Authorization: Bearer {{token}} 60 | 61 | ############################################### 62 | ### QUESIONS 63 | ############################################### 64 | 65 | ### Get questions for survey 66 | GET http://localhost:8000/api/survey/5ee076f5005ae40c00e8254a/question 67 | Authorization: Bearer {{token}} 68 | 69 | ### Get Single Question 70 | GET http://localhost:8000/api/question/5ee06942002fed3200c67c60 71 | Authorization: Bearer {{token}} 72 | 73 | ### Create question for survey 74 | POST http://localhost:8000/api/question/5ee076f5005ae40c00e8254a 75 | Content-Type: application/json 76 | Authorization: Bearer {{token}} 77 | 78 | { 79 | "text": "How much you liked the Deno Course?", 80 | "type": "choice", 81 | "required": true, 82 | "data": { 83 | "multiple": false, 84 | "answers": [ 85 | "I liked it very much", 86 | "I liked it", 87 | "I did not like it", 88 | "I hate it" 89 | ] 90 | } 91 | } 92 | 93 | ### Update question 94 | put http://localhost:8000/api/question/5ee07892001a059c00e8254f 95 | Content-Type: application/json 96 | Authorization: Bearer {{token}} 97 | 98 | { 99 | "text": "To build apps with Flutter, you need to use the Dart programming language. Are you familiar with Dart?", 100 | "type": "choice", 101 | "required": true, 102 | "data": { 103 | "multiple": false, 104 | "answers": ["Yes", "No"] 105 | } 106 | } 107 | 108 | ### Get Single Question 109 | DELETE http://localhost:8000/api/question/5edb363d00d83531007c790c 110 | Authorization: Bearer {{token}} 111 | 112 | 113 | ############################################### 114 | ### ANSWERS 115 | ############################################### 116 | 117 | GET http://localhost:8000/api/survey/5edb333700fa6eb4007c7908/question 118 | Authorization: Bearer {{token}} 119 | -------------------------------------------------------------------------------- /router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "./deps.ts"; 2 | import { authMiddleware } from "./middleware/authMiddleware.ts"; 3 | import siteController from "./controllers/SiteController.ts"; 4 | import surveyController from "./controllers/SurveyController.ts"; 5 | import questionController from "./controllers/QuestionController.ts"; 6 | import authController from "./controllers/AuthController.ts"; 7 | 8 | const router = new Router(); 9 | 10 | router 11 | .get("/", siteController.surveys) 12 | .get("/survey/:id", siteController.viewSurvey) 13 | .post("/survey/:id", siteController.submitSurvey) 14 | .post("/api/register", authController.register) 15 | .post("/api/login", authController.login) 16 | // Survey CRUD 17 | .get( 18 | "/api/survey", 19 | authMiddleware, 20 | surveyController.getAll.bind(surveyController), 21 | ) 22 | .get( 23 | "/api/survey/:id", 24 | authMiddleware, 25 | surveyController.getSingle.bind(surveyController), 26 | ) 27 | .post( 28 | "/api/survey", 29 | authMiddleware, 30 | surveyController.create.bind(surveyController), 31 | ) 32 | .put( 33 | "/api/survey/:id", 34 | authMiddleware, 35 | surveyController.update.bind(surveyController), 36 | ) 37 | .delete( 38 | "/api/survey/:id", 39 | authMiddleware, 40 | surveyController.delete.bind(surveyController), 41 | ) 42 | // Survey Question CRUD 43 | .get( 44 | "/api/survey/:surveyId/question", 45 | authMiddleware, 46 | questionController.getBySurvey.bind(questionController), 47 | ) 48 | .get( 49 | "/api/question/:id", 50 | authMiddleware, 51 | questionController.getSingle.bind(questionController), 52 | ) 53 | .post( 54 | "/api/question/:surveyId", 55 | authMiddleware, 56 | questionController.create.bind(questionController), 57 | ) 58 | .put( 59 | "/api/question/:id", 60 | authMiddleware, 61 | questionController.update.bind(questionController), 62 | ) 63 | .delete( 64 | "/api/question/:id", 65 | authMiddleware, 66 | questionController.delete.bind(questionController), 67 | ); 68 | 69 | export default router; 70 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import { Application, send } from "./deps.ts"; 2 | import router from "./router.ts"; 3 | import { staticFileMiddleware } from "./middleware/staticFileMiddleware.ts"; 4 | 5 | const app = new Application(); 6 | 7 | app.addEventListener("listen", ({ hostname, port, secure }) => { 8 | console.log( 9 | `Listening on: ${secure ? "https://" : "http://"}${hostname ?? 10 | "localhost"}:${port}`, 11 | ); 12 | }); 13 | 14 | app.addEventListener("error", (evt) => { 15 | console.log(evt.error); 16 | }); 17 | 18 | // register some middleware 19 | app.use(staticFileMiddleware); 20 | 21 | app.use(router.routes()); 22 | app.use(router.allowedMethods()); 23 | 24 | await app.listen({ port: 8000 }); 25 | -------------------------------------------------------------------------------- /views/notfound.ejs: -------------------------------------------------------------------------------- 1 | <%- await include(`views/partials/header.ejs`) %> 2 | 3 |
4 |

404 - Page does not exist

5 | 6 |
7 | Go to home page 8 |
9 |
10 | 11 | <%- await include(`views/partials/footer.ejs`) %> -------------------------------------------------------------------------------- /views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /views/partials/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | DenoSurvey 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /views/survey.ejs: -------------------------------------------------------------------------------- 1 | <%- await include(`views/partials/header.ejs`) %> 2 | 3 |
4 |

5 | Go back to all surveys 6 |

7 |
8 |
9 |

<%= survey.name %>

10 |
11 | <%= survey.description %> 12 |

* Required

13 |
14 |
15 |
16 | 17 |
18 | <% for (let question of questions) { %> 19 |
20 |
21 |
22 | <%= question.text %> 23 | <% if (question.required) { %> 24 | * 25 | <% } %> 26 |
27 | <% if (question.isText()) { %> 28 |
29 | 31 | <% if (errors[question.id]) { %> 32 |
33 | <%= errors[question.id] %> 34 |
35 | <% } %> 36 |
37 | <% } else if (question.isChoice() && !question.data.multiple) { %> 38 |
39 | <% for (const [index, answer] of question.data.answers.entries()) { %> 40 |
41 | > 44 | 45 | <% if (index === question.data.answers.length - 1 && errors[question.id]) { %> 46 |
47 | <%= errors[question.id] %> 48 |
49 | <% } %> 50 |
51 | <% } %> 52 |
53 | <% } else if (question.isChoice() && question.data.multiple) { %> 54 |
55 | <% for (const [index, answer] of question.data.answers.entries()) { %> 56 |
57 | > 60 | 61 | <% if (index === question.data.answers.length - 1 && errors[question.id]) { %> 62 |
63 | <%= errors[question.id] %> 64 |
65 | <% } %> 66 |
67 | <% } %> 68 |
69 | <% } %> 70 |
71 |
72 | <% } %> 73 |
74 | 75 |
76 |
77 |
78 | 79 | <%- await include(`views/partials/footer.ejs`) %> -------------------------------------------------------------------------------- /views/surveyFinish.ejs: -------------------------------------------------------------------------------- 1 | <%- await include(`views/partials/header.ejs`) %> 2 | 3 |
4 |
5 |

Thank you for participating in survey

6 |

Here is your answer ID for future reference: <%= answerId %>

7 |

8 | Submit another response 9 |

10 |
11 | 12 |
13 | Go to home page 14 |
15 |
16 | 17 | <%- await include(`views/partials/footer.ejs`) %> -------------------------------------------------------------------------------- /views/surveys.ejs: -------------------------------------------------------------------------------- 1 | <%- await include(`views/partials/header.ejs`) %> 2 | 3 |
4 |

Active Surveys

5 | 6 | <% for (let survey of surveys) { %> 7 | 15 | <% } %> 16 |
17 | 18 | <%- await include(`views/partials/footer.ejs`) %> --------------------------------------------------------------------------------