commit c9f31e5c8a3ad3d2bcf860593761b84e28e17d95 Author: AndrewTrieu Date: Fri Apr 21 13:17:50 2023 +0300 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a192735 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/settings.json +project.zip diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/authserver/Dockerfile b/authserver/Dockerfile new file mode 100644 index 0000000..8cebf7b --- /dev/null +++ b/authserver/Dockerfile @@ -0,0 +1,13 @@ +FROM denoland/deno:alpine-1.29.2 + +EXPOSE 7777 + +WORKDIR /app + +COPY deps.js . + +RUN deno cache deps.js + +COPY . . + +CMD [ "run", "--unstable", "--watch", "--allow-all", "--no-check", "app-launch.js" ] \ No newline at end of file diff --git a/authserver/app-launch.js b/authserver/app-launch.js new file mode 100644 index 0000000..00528b3 --- /dev/null +++ b/authserver/app-launch.js @@ -0,0 +1,7 @@ +import { app } from "./app.js"; + +const options = { + port: 7777, +}; + +app.listen(options); diff --git a/authserver/app.js b/authserver/app.js new file mode 100644 index 0000000..1a91067 --- /dev/null +++ b/authserver/app.js @@ -0,0 +1,19 @@ +import { Application, Session } from "./deps.js"; +import { authMiddleware } from "./middlewares/authMiddleware.js"; +import { errorMiddleware } from "./middlewares/errorMiddleware.js"; +import { renderMiddleware } from "./middlewares/renderMiddleware.js"; +import { userMiddleware } from "./middlewares/userMiddleware.js"; +import { serveStaticMiddleware } from "./middlewares/serveStaticMiddleware.js"; +import { router } from "./routes/routes.js"; + +const app = new Application(); +app.use(Session.initMiddleware()); +app.use(errorMiddleware); +app.use(authMiddleware); +app.use(userMiddleware); +app.use(serveStaticMiddleware); +app.use(renderMiddleware); +app.use(router.routes()); +app.use(router.allowedMethods()); + +export { app }; diff --git a/authserver/database/database.js b/authserver/database/database.js new file mode 100644 index 0000000..a2a2600 --- /dev/null +++ b/authserver/database/database.js @@ -0,0 +1,10 @@ +import { postgres } from "../deps.js"; + +let sql; +if (Deno.env.get("DATABASE_URL")) { + sql = postgres(Deno.env.get("DATABASE_URL")); +} else { + sql = postgres({}); +} + +export { sql }; diff --git a/authserver/deps.js b/authserver/deps.js new file mode 100644 index 0000000..99341c8 --- /dev/null +++ b/authserver/deps.js @@ -0,0 +1,13 @@ +export { configure, renderFile } from "https://deno.land/x/eta@v2.0.0/mod.ts"; +export { + Application, + Router, + send, +} from "https://deno.land/x/oak@v11.1.0/mod.ts"; +import { readFileSync } from "https://deno.land/std@0.152.0/node/fs.ts"; +export { readFileSync }; +import postgres from "https://deno.land/x/postgresjs@v3.3.3/mod.js"; +export { postgres }; +export { Session } from "https://deno.land/x/oak_sessions@v4.0.5/mod.ts"; +export * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"; +export * as validasaur from "https://deno.land/x/validasaur@v0.15.0/mod.ts"; diff --git a/authserver/fly.toml b/authserver/fly.toml new file mode 100644 index 0000000..856e9cd --- /dev/null +++ b/authserver/fly.toml @@ -0,0 +1,38 @@ +# fly.toml file generated for authapp on 2023-04-20T11:14:23+03:00 + +app = "authapp" +kill_signal = "SIGINT" +kill_timeout = 5 +primary_region = "arn" +processes = [] + +[env] + +[experimental] + auto_rollback = true + +[[services]] + http_checks = [] + internal_port = 7777 + processes = ["app"] + protocol = "tcp" + script_checks = [] + [services.concurrency] + hard_limit = 25 + soft_limit = 20 + type = "connections" + + [[services.ports]] + force_https = true + handlers = ["http"] + port = 80 + + [[services.ports]] + handlers = ["tls", "http"] + port = 443 + + [[services.tcp_checks]] + grace_period = "1s" + interval = "15s" + restart_limit = 0 + timeout = "2s" diff --git a/authserver/middlewares/authMiddleware.js b/authserver/middlewares/authMiddleware.js new file mode 100644 index 0000000..0ad4c3f --- /dev/null +++ b/authserver/middlewares/authMiddleware.js @@ -0,0 +1,17 @@ +const restrictedPaths = ["/index"]; + +const authMiddleware = async (context, next) => { + const user = await context.state.session.get("user"); + if ( + !user && + restrictedPaths.some((path) => + context.request.url.pathname.startsWith(path) + ) + ) { + context.response.redirect("/auth/login"); + } else { + await next(); + } +}; + +export { authMiddleware }; diff --git a/authserver/middlewares/errorMiddleware.js b/authserver/middlewares/errorMiddleware.js new file mode 100644 index 0000000..f0e2373 --- /dev/null +++ b/authserver/middlewares/errorMiddleware.js @@ -0,0 +1,9 @@ +const errorMiddleware = async (context, next) => { + try { + await next(); + } catch (e) { + console.log(e); + } +}; + +export { errorMiddleware }; diff --git a/authserver/middlewares/renderMiddleware.js b/authserver/middlewares/renderMiddleware.js new file mode 100644 index 0000000..c40b167 --- /dev/null +++ b/authserver/middlewares/renderMiddleware.js @@ -0,0 +1,24 @@ +import { configure, renderFile } from "../deps.js"; + +const renderMiddleware = async (context, next) => { + configure({ + views: `${Deno.cwd()}/views/`, + }); + + context.render = async (file, data) => { + if (!data) { + data = {}; + } + + if (context.user) { + data.user = context.user; + } + + context.response.headers.set("Content-Type", "text/html; charset=utf-8"); + context.response.body = await renderFile(file, data); + }; + + await next(); +}; + +export { renderMiddleware }; diff --git a/authserver/middlewares/serveStaticMiddleware.js b/authserver/middlewares/serveStaticMiddleware.js new file mode 100644 index 0000000..041e9a4 --- /dev/null +++ b/authserver/middlewares/serveStaticMiddleware.js @@ -0,0 +1,15 @@ +import { send } from "../deps.js"; + +const serveStaticMiddleware = async (context, next) => { + if (context.request.url.pathname.startsWith("/static")) { + const path = context.request.url.pathname.substring(7); + + await send(context, path, { + root: `${Deno.cwd()}/static`, + }); + } else { + await next(); + } +}; + +export { serveStaticMiddleware }; diff --git a/authserver/middlewares/userMiddleware.js b/authserver/middlewares/userMiddleware.js new file mode 100644 index 0000000..9749401 --- /dev/null +++ b/authserver/middlewares/userMiddleware.js @@ -0,0 +1,14 @@ +import * as authService from "../services/authService.js"; + +const userMiddleware = async (context, next) => { + const user = await context.state.session.get("user"); + + if (user) { + const userFromDatabase = await authService.findUser(user.username); + context.user = userFromDatabase[0]; + } + + await next(); +}; + +export { userMiddleware }; diff --git a/authserver/routes/controllers/authController.js b/authserver/routes/controllers/authController.js new file mode 100644 index 0000000..6ac1ced --- /dev/null +++ b/authserver/routes/controllers/authController.js @@ -0,0 +1,85 @@ +import * as authService from "../../services/authService.js"; +import { bcrypt, validasaur } from "../../deps.js"; + +const validationRules = { + username: [validasaur.required], + password: [validasaur.minLength(8), validasaur.required], +}; + +const showLogin = ({ render }) => { + render("login.eta"); +}; + +const showRegister = ({ render }) => { + render("register.eta"); +}; + +const register = async ({ request, response, render }) => { + const body = request.body({ type: "form" }); + const params = await body.value; + const userData = { + username: params.get("username"), + password: params.get("password"), + }; + + const userDatabase = await authService.findUser(userData.username); + if (userDatabase.length > 0) { + response.status = 422; + render("register.eta", { + errors: { error: { message: "Username already registered!" } }, + }); + return; + } + + const [passes, errors] = await validasaur.validate(userData, validationRules); + + if (!passes) { + response.status = 422; + userData.errors = errors; + render("register.eta", userData); + } else { + const hashedPassword = await bcrypt.hash(userData.password); + await authService.createUser(userData.username, hashedPassword); + render("register.eta", { + errors: { error: { message: "Registration successful!" } }, + }); + } +}; + +const login = async ({ request, response, state, render }) => { + const body = request.body({ type: "form" }); + const params = await body.value; + const username = params.get("username"); + const userDatabase = await authService.findUser(username); + if (userDatabase.length < 1) { + response.status = 422; + render("login.eta", { + errors: { error: { message: "Please enter correct username!" } }, + }); + return; + } + + const user = userDatabase[0]; + const passwordCorrect = await bcrypt.compare( + params.get("password"), + user.password + ); + + if (!passwordCorrect) { + response.status = 422; + render("login.eta", { + errors: { error: { message: "Please enter correct password!" } }, + }); + return; + } + + await state.session.set("user", user); + response.redirect("/index"); +}; + +const logout = async ({ response, state }) => { + await state.session.set("user", null); + response.redirect("/"); +}; + +export { showLogin, showRegister, register, login, logout }; diff --git a/authserver/routes/controllers/homeController.js b/authserver/routes/controllers/homeController.js new file mode 100644 index 0000000..9be35d3 --- /dev/null +++ b/authserver/routes/controllers/homeController.js @@ -0,0 +1,6 @@ +const showHome = async ({ state, render }) => { + const user = await state.session.get("user"); + render("index.eta", user); +}; + +export { showHome }; diff --git a/authserver/routes/controllers/mainController.js b/authserver/routes/controllers/mainController.js new file mode 100644 index 0000000..1a8c06e --- /dev/null +++ b/authserver/routes/controllers/mainController.js @@ -0,0 +1,5 @@ +const showMain = async ({ render }) => { + render("main.eta"); +}; + +export { showMain }; diff --git a/authserver/routes/routes.js b/authserver/routes/routes.js new file mode 100644 index 0000000..4036e3a --- /dev/null +++ b/authserver/routes/routes.js @@ -0,0 +1,20 @@ +import { Router } from "../deps.js"; +import * as mainController from "./controllers/mainController.js"; +import * as authController from "./controllers/authController.js"; +import * as homeController from "./controllers/homeController.js"; + +const router = new Router(); + +// mainController routes (home page) +router.get("/", mainController.showMain); + +// authController routes (login and register) +router.get("/auth/login", authController.showLogin); +router.get("/auth/register", authController.showRegister); +router.post("/auth/login", authController.login); +router.post("/auth/register", authController.register); +router.get("/logout", authController.logout); + +router.get("/index", homeController.showHome); + +export { router }; diff --git a/authserver/services/authService.js b/authserver/services/authService.js new file mode 100644 index 0000000..0fc321e --- /dev/null +++ b/authserver/services/authService.js @@ -0,0 +1,11 @@ +import { sql } from "../database/database.js"; + +const findUser = async (username) => { + return await sql`SELECT * FROM users WHERE username = ${username}`; +}; + +const createUser = async (username, password) => { + return await sql`INSERT INTO users (username, password) VALUES (${username}, ${password})`; +}; + +export { findUser, createUser }; diff --git a/authserver/views/index.eta b/authserver/views/index.eta new file mode 100644 index 0000000..24d3951 --- /dev/null +++ b/authserver/views/index.eta @@ -0,0 +1,9 @@ +<% layout("./layouts/layout.eta") %> +

Online Forum

+ +

Welcome!

+ +

You are authenticated, welcome <%= it.username %>!

+ + + diff --git a/authserver/views/layouts/layout.eta b/authserver/views/layouts/layout.eta new file mode 100644 index 0000000..25fd0e8 --- /dev/null +++ b/authserver/views/layouts/layout.eta @@ -0,0 +1,35 @@ + + + + + + + + Online Forum + + + <% if (it.user) { %> + + <% } %> + +
+ <%~ it.body %> +
+ + \ No newline at end of file diff --git a/authserver/views/login.eta b/authserver/views/login.eta new file mode 100644 index 0000000..d011609 --- /dev/null +++ b/authserver/views/login.eta @@ -0,0 +1,24 @@ +<% layout("./layouts/layout.eta") %> + +

Login

+ +
+ Username:
+
+ Password:
+
+ +
+ +<% if (it.errors) { %> + +<% } %> +
+ +Are you a new user? diff --git a/authserver/views/main.eta b/authserver/views/main.eta new file mode 100644 index 0000000..1955262 --- /dev/null +++ b/authserver/views/main.eta @@ -0,0 +1,9 @@ +<% layout("./layouts/layout.eta") %> + +

Online Forum

+ +

This is an online forum that allows the participants to join, post questions, answer the posts etc.

+ +

Are you already registered? Login

+ +

Not registered yet? Register

diff --git a/authserver/views/register.eta b/authserver/views/register.eta new file mode 100644 index 0000000..d6f7e24 --- /dev/null +++ b/authserver/views/register.eta @@ -0,0 +1,24 @@ +<% layout("./layouts/layout.eta") %> + +

New user registration

+ +
+ Username:
+
+ Password:
+
+ +
+ +<% if (it.errors) { %> + +<% } %> +
+ +Are you already registered? diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..10142cf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +version: "3.4" + +services: + drill-and-practice: + build: authserver + image: authserver + restart: "no" + volumes: + - ./authserver/:/app + ports: + - 7777:7777 + depends_on: + - database + - flyway + env_file: + - project.env + + database: + container_name: database-p2-e8774b8e-c7a9-4cce-a779-3b59be02206d + image: postgres:14.1 + restart: "no" + env_file: + - project.env + + flyway: + image: flyway/flyway:9.11.0-alpine + depends_on: + - database + volumes: + - .:/flyway/sql + command: -connectRetries=60 -baselineOnMigrate=true migrate + env_file: + - project.env + diff --git a/flyway/sql/V1___initial_schema.sql b/flyway/sql/V1___initial_schema.sql new file mode 100644 index 0000000..c4b7c50 --- /dev/null +++ b/flyway/sql/V1___initial_schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE, + password CHAR(60) +); + diff --git a/project.env b/project.env new file mode 100644 index 0000000..d85243e --- /dev/null +++ b/project.env @@ -0,0 +1,16 @@ +# Database configuration for PostgreSQL (running in container called "database-p2-e8774b8e-c7a9-4cce-a779-3b59be02206d") +POSTGRES_USER=username +POSTGRES_PASSWORD=password +POSTGRES_DB=database + +# Database configuration for Flyway (used for database migrations) +FLYWAY_USER=username +FLYWAY_PASSWORD=password +FLYWAY_URL=jdbc:postgresql://database-p2-e8774b8e-c7a9-4cce-a779-3b59be02206d:5432/database + +# Database configuration for Deno's PostgreSQL driver +PGUSER=username +PGPASSWORD=password +PGHOST=database-p2-e8774b8e-c7a9-4cce-a779-3b59be02206d +PGPORT=5432 +PGDATABASE=database