From 2c54518e12617da35b91154238a4060e8ca583b1 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Tue, 9 Mar 2021 15:58:54 +0530 Subject: [PATCH] moved from mono repo --- .github/workflows/linux.yml | 80 ++++++ .gitignore | 3 + Cargo.toml | 32 +++ README.md | 83 ++++++ config/default.toml | 30 +++ migrations/20210309085146_mcaptcha_users.sql | 4 + migrations/20210309085201_mcaptcha_config.sql | 5 + migrations/20210309085205_mcaptcha_levels.sql | 5 + src/data.rs | 49 ++++ src/errors.rs | 139 ++++++++++ src/main.rs | 82 ++++++ src/routes.rs | 248 ++++++++++++++++++ src/settings.rs | 153 +++++++++++ 13 files changed, 913 insertions(+) create mode 100644 .github/workflows/linux.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 config/default.toml create mode 100644 migrations/20210309085146_mcaptcha_users.sql create mode 100644 migrations/20210309085201_mcaptcha_config.sql create mode 100644 migrations/20210309085205_mcaptcha_levels.sql create mode 100644 src/data.rs create mode 100644 src/errors.rs create mode 100644 src/main.rs create mode 100644 src/routes.rs create mode 100644 src/settings.rs diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 00000000..dad80013 --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,80 @@ +name: CI (Linux) + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master + + +jobs: + build_and_test: + strategy: + fail-fast: false + matrix: + version: + - stable + - nightly + + name: ${{ matrix.version }} - x86_64-unknown-linux-gnu + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: ⚡ Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install ${{ matrix.version }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: check build + uses: actions-rs/cargo@v1 + with: + command: check + args: --all --bins --examples --tests + + - name: tests + uses: actions-rs/cargo@v1 + timeout-minutes: 40 + with: + command: test + args: --all --all-features --no-fail-fast + + - name: Generate coverage file + if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') + uses: actions-rs/tarpaulin@v0.1 + with: + version: '0.15.0' + args: '-t 1200' + + - name: Upload to Codecov + if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') + uses: codecov/codecov-action@v1 + with: + file: cobertura.xml + + - name: generate documentation + if: matrix.version == 'stable' && (github.repository == 'mCaptcha/guard') + uses: actions-rs/cargo@v1 + with: + command: doc + args: --no-deps --workspace --all-features + + - name: Deploy to GitHub Pages + if: matrix.version == 'stable' && (github.repository == 'mCaptcha/guard') + uses: JamesIves/github-pages-deploy-action@3.7.1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: target/doc diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8bfa10ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +tarpaulin-report.html +.env diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..cd439e64 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "guard" +version = "0.1.0" +authors = ["realaravinth "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "3" + +sqlx = { version = "0.4.0", features = [ "runtime-actix-rustls", "postgres" ] } +argon2-creds = { version = "0.2", git = "https://github.com/realaravinth/argon2-creds" } + +config = "0.10" +validator = "0.12" + +derive_builder = "0.9" +derive_more = "0.99" + +serde = "1" +serde_json = "1" + +url = "2.2" + +pretty_env_logger = "0.3" +log = "0.4" + +lazy_static = "1.4" + +actix-identity = "0.3" +actix-http = "2.2" diff --git a/README.md b/README.md new file mode 100644 index 00000000..4d4d8d5f --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +
+

mCaptcha Guard

+

+ Back-end component of mCaptcha +

+ +[![Documentation](https://img.shields.io/badge/docs-master-blue)](https://mcaptcha.github.io/mCaptcha/guard/index.html) +![CI (Linux)]() +[![dependency status](https://deps.rs/repo/github/mCaptcha/guard/status.svg)](https://deps.rs/repo/github/mCaptcha/guard) +[![codecov](https://codecov.io/gh/mCaptcha/guard/branch/master/graph/badge.svg)](https://codecov.io/gh/mCaptcha/guard) +
+[![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg)](http://www.gnu.org/licenses/agpl-3.0) +
+ + + +**placeholder-repo** is an placeholder-repo and access management platform built for the +[IndieWeb](indieweb.org) + +### How to build + +- Install Cargo using [rustup](https://rustup.rs/) with: + +``` +$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +- Clone the repository with: + +``` +$ git clone https://github.com/mCaptcha/guard +``` + +- Build with Cargo: + +``` +$ cd guard && cargo build +``` + +### Configuration: + +placeholder-repo is highly configurable. +Configuration is applied/merged in the following order: + +1. `config/default.toml` +2. environment variables. + +To make installation process seamless, placeholder-repo ships with a CLI tool to +assist in database migrations. + +#### Setup + +##### Environment variables: + +Setting environment variables are optional. The configuration files have +all the necessary parameters listed. By setting environment variables, +you will be overriding the values set in the configuration files. + +###### Database: + +| Name | Value | +| ------------------------------- | -------------------------------------- | +| `PLACEHOLDER_DATEBASE_PASSWORD` | Postgres password | +| `PLACEHOLDER_DATEBASE_NAME` | Postgres database name | +| `PLACEHOLDER_DATEBASE_PORT` | Postgres port | +| `PLACEHOLDER_DATEBASE_HOSTNAME` | Postgres hostmane | +| `PLACEHOLDER_DATEBASE_USERNAME` | Postgres username | +| `PLACEHOLDER_DATEBASE_POOL` | Postgres database connection pool size | + +###### Redis cache: + +| Name | Value | +| ---------------------------- | -------------- | +| `PLACEHOLDER_REDIS_PORT` | Redis port | +| `PLACEHOLDER_REDIS_HOSTNAME` | Redis hostmane | + +###### Server: + +| Name | Value | +| ----------------------------------------- | --------------------------------------------------- | +| `PLACEHOLDER_SERVER_PORT` (or) `PORT`\*\* | The port on which you want wagon to listen to | +| `PLACEHOLDER_SERVER_IP` | The IP address on which you want wagon to listen to | +| `PLACEHOLDER_SERVER_STATIC_FILES_DIR` | Path to directory containing static files | diff --git a/config/default.toml b/config/default.toml new file mode 100644 index 00000000..11107e85 --- /dev/null +++ b/config/default.toml @@ -0,0 +1,30 @@ +debug = true + +[database] +# This section deals with the database location and how to access it +# Please note that at the moment, we have support for only postgresqa. +# Example, if you are Batman, your config would be: +# hostname = "batcave.org" +# port = "5432" +# username = "batman" +# password = "somereallycomplicatedBatmanpassword" +hostname = "localhost" +port = "5432" +username = "postgres" +password = "password" +name = "webhunt-postgress" +pool = 4 + +# This section deals with the configuration of the actual server +[server] +cookie_secret = "Zae0OOxf^bOJ#zN^&k7VozgW&QAx%n02TQFXpRMG4cCU0xMzgu3dna@tQ9dvc&TlE6p*n#kXUdLZJCQsuODIV%r$@o4%770ePQB7m#dpV!optk01NpY0@615w5e2Br4d" +# The port at which you want authentication to listen to +# takes a number, choose from 1000-10000 if you dont know what you are doing +port = 7000 +#IP address. Enter 0.0.0.0 to listen on all availale addresses +ip= "0.0.0.0" +# enter your hostname, eg: example.com +domain = "localhost" +allow_registration = true +# directory containing static files +static_files_dir = "./frontend/dist" diff --git a/migrations/20210309085146_mcaptcha_users.sql b/migrations/20210309085146_mcaptcha_users.sql new file mode 100644 index 00000000..6c93f34a --- /dev/null +++ b/migrations/20210309085146_mcaptcha_users.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS mcaptcha_users ( + name VARCHAR(100) NOT NULL UNIQUE, + ID SERIAL PRIMARY KEY NOT NULL +); diff --git a/migrations/20210309085201_mcaptcha_config.sql b/migrations/20210309085201_mcaptcha_config.sql new file mode 100644 index 00000000..d1ba1d0d --- /dev/null +++ b/migrations/20210309085201_mcaptcha_config.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS mcaptcha_config ( + name VARCHAR(100) references mcaptcha_users(name), + id VARCHAR(32) PRIMARY KEY NOT NULL UNIQUE, + duration INTEGER NOT NULL +); diff --git a/migrations/20210309085205_mcaptcha_levels.sql b/migrations/20210309085205_mcaptcha_levels.sql new file mode 100644 index 00000000..afa8a660 --- /dev/null +++ b/migrations/20210309085205_mcaptcha_levels.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS mcaptcha_levels ( + id VARCHAR(32) references mcaptcha_config(id), + difficulty_factor INTEGER NOT NULL, + visitor_threshold INTEGER NOT NULL +); diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 00000000..d83f5fc8 --- /dev/null +++ b/src/data.rs @@ -0,0 +1,49 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + +use argon2_creds::{Config, ConfigBuilder, PasswordPolicy}; +use sqlx::postgres::PgPoolOptions; +use sqlx::PgPool; + +use crate::SETTINGS; + +#[derive(Clone)] +pub struct Data { + pub db: PgPool, + pub creds: Config, +} + +impl Data { + #[cfg(not(tarpaulin_include))] + pub async fn new() -> Self { + let db = PgPoolOptions::new() + .max_connections(SETTINGS.database.pool) + .connect(&SETTINGS.database.url) + .await + .expect("Unable to form database pool"); + + let creds = ConfigBuilder::default() + .username_case_mapped(false) + .profanity(true) + .blacklist(false) + .password_policy(PasswordPolicy::default()) + .build() + .unwrap(); + + Data { creds, db } + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 00000000..cadf1067 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,139 @@ +use std::io::{Error as IOError, ErrorKind as IOErrorKind}; + +use actix_web::{ + dev::HttpResponseBuilder, + error::ResponseError, + http::{header, StatusCode}, + HttpResponse, +}; + +use argon2_creds::errors::CredsError; + +use derive_more::{Display, Error}; +use log::debug; +use serde::Serialize; +// use validator::ValidationErrors; + +use std::convert::From; + +#[derive(Debug, Display, Clone, PartialEq, Error)] +#[cfg(not(tarpaulin_include))] +pub enum ServiceError { + #[display(fmt = "internal server error")] + InternalServerError, + #[display(fmt = "The value you entered for email is not an email")] //405j + NotAnEmail, + #[display(fmt = "File not found")] + FileNotFound, + #[display(fmt = "File exists")] + FileExists, + #[display(fmt = "Permission denied")] + PermissionDenied, + #[display(fmt = "Invalid credentials")] + InvalidCredentials, + #[display(fmt = "Authorization required")] + AuthorizationRequired, + + /// when the value passed contains profainity + #[display(fmt = "Can't allow profanity in usernames")] + ProfainityError, + /// when the value passed contains blacklisted words + /// see [blacklist](https://github.com/shuttlecraft/The-Big-Username-Blacklist) + #[display(fmt = "Username contains blacklisted words")] + BlacklistError, + + /// when the value passed contains characters not present + /// in [UsernameCaseMapped](https://tools.ietf.org/html/rfc8265#page-7) + /// profile + #[display(fmt = "username_case_mapped violation")] + UsernameCaseMappedError, + + /// when the value passed contains profainity + #[display(fmt = "Username not available")] + UsernameTaken, + /// when a question is already answered + #[display(fmt = "Already answered")] + AlreadyAnswered, +} + +#[derive(Serialize)] +#[cfg(not(tarpaulin_include))] +struct ErrorToResponse { + error: String, +} + +impl ResponseError for ServiceError { + fn error_response(&self) -> HttpResponse { + HttpResponseBuilder::new(self.status_code()) + .set_header(header::CONTENT_TYPE, "application/json; charset=UTF-8") + .json(ErrorToResponse { + error: self.to_string(), + }) + } + + fn status_code(&self) -> StatusCode { + match *self { + ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, + ServiceError::NotAnEmail => StatusCode::BAD_REQUEST, + ServiceError::FileNotFound => StatusCode::NOT_FOUND, + ServiceError::FileExists => StatusCode::METHOD_NOT_ALLOWED, + ServiceError::PermissionDenied => StatusCode::UNAUTHORIZED, + ServiceError::InvalidCredentials => StatusCode::UNAUTHORIZED, + ServiceError::AuthorizationRequired => StatusCode::UNAUTHORIZED, + ServiceError::ProfainityError => StatusCode::BAD_REQUEST, + ServiceError::BlacklistError => StatusCode::BAD_REQUEST, + ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST, + ServiceError::UsernameTaken => StatusCode::BAD_REQUEST, + ServiceError::AlreadyAnswered => StatusCode::BAD_REQUEST, + } + } +} + +impl From for ServiceError { + fn from(e: IOError) -> ServiceError { + debug!("{:?}", &e); + match e.kind() { + IOErrorKind::NotFound => ServiceError::FileNotFound, + IOErrorKind::PermissionDenied => ServiceError::PermissionDenied, + IOErrorKind::AlreadyExists => ServiceError::FileExists, + _ => ServiceError::InternalServerError, + } + } +} + +impl From for ServiceError { + fn from(e: CredsError) -> ServiceError { + debug!("{:?}", &e); + match e { + CredsError::UsernameCaseMappedError => ServiceError::UsernameCaseMappedError, + CredsError::ProfainityError => ServiceError::ProfainityError, + CredsError::BlacklistError => ServiceError::BlacklistError, + CredsError::NotAnEmail => ServiceError::NotAnEmail, + CredsError::Argon2Error(_) => ServiceError::InternalServerError, + _ => ServiceError::InternalServerError, + } + } +} + +// impl From for ServiceError { +// fn from(_: ValidationErrors) -> ServiceError { +// ServiceError::NotAnEmail +// } +// } +// +impl From for ServiceError { + fn from(e: sqlx::Error) -> Self { + use sqlx::error::Error; + use std::borrow::Cow; + debug!("{:?}", &e); + if let Error::Database(err) = e { + if err.code() == Some(Cow::from("23505")) { + return ServiceError::UsernameTaken; + } + } + + ServiceError::InternalServerError + } +} + +pub type ServiceResult = std::result::Result; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..8a24c0ce --- /dev/null +++ b/src/main.rs @@ -0,0 +1,82 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + +use actix_identity::{CookieIdentityPolicy, IdentityService}; +use actix_web::{ + error::InternalError, http::StatusCode, middleware, web::JsonConfig, App, HttpServer, +}; +use lazy_static::lazy_static; + +mod data; +mod errors; +//mod routes; +mod settings; + +pub use data::Data; +pub use settings::Settings; + +lazy_static! { + pub static ref SETTINGS: Settings = Settings::new().unwrap(); +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + // use routes::services; + + // let data = Data::new().await; + pretty_env_logger::init(); + + // sqlx::migrate!("./migrations/").run(&data.db).await.unwrap(); + + HttpServer::new(move || { + App::new() + .wrap(middleware::Logger::default()) + .wrap(get_identity_service()) + .wrap(middleware::Compress::default()) + // .data(data.clone()) + .wrap(middleware::NormalizePath::new( + middleware::normalize::TrailingSlash::Trim, + )) + .app_data(get_json_err()) + //.configure(services) + }) + .bind(SETTINGS.server.get_ip()) + .unwrap() + .run() + .await +} + +#[cfg(not(tarpaulin_include))] +fn get_json_err() -> JsonConfig { + JsonConfig::default().error_handler(|err, _| { + //debug!("JSON deserialization error: {:?}", &err); + InternalError::new(err, StatusCode::BAD_REQUEST).into() + }) +} + +#[cfg(not(tarpaulin_include))] +fn get_identity_service() -> IdentityService { + let cookie_secret = &SETTINGS.server.cookie_secret; + IdentityService::new( + CookieIdentityPolicy::new(cookie_secret.as_bytes()) + .name("Authorization") + //TODO change cookie age + .max_age(216000) + .domain(&SETTINGS.server.domain) + .secure(false), + ) +} diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 00000000..7d1be4d2 --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,248 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + +use actix_identity::Identity; +use actix_web::{ + get, post, + web::{self, Path as WebPath, ServiceConfig}, + HttpResponse, Responder, +}; +use log::debug; +use serde::{Deserialize, Serialize}; + +use crate::errors::*; +use crate::Data; + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct SomeData { + pub a: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct Creds { + pub username: String, + pub password: String, +} + +#[post("/api/signup")] +async fn signup(payload: web::Json, data: web::Data) -> ServiceResult { + let username = data.creds.username(&payload.username)?; + let hash = data.creds.password(&payload.password)?; + sqlx::query!( + "INSERT INTO users (name , password) VALUES ($1, $2)", + username, + hash + ) + .execute(&data.db) + .await?; + Ok(HttpResponse::Ok()) +} + +struct Password { + password: String, +} + +#[post("/api/signin")] +async fn signin( + id: Identity, + payload: web::Json, + data: web::Data, +) -> ServiceResult { + use argon2_creds::Config; + + let rec = sqlx::query_as!( + Password, + "SELECT password FROM users WHERE name = ($1)", + &payload.username + ) + .fetch_one(&data.db) + .await?; + + if Config::verify(&rec.password, &payload.password)? { + debug!("remembered {}", payload.username); + id.remember(payload.into_inner().username); + return Ok(HttpResponse::Ok()); + } else { + return Err(ServiceError::InvalidCredentials); + } +} + +#[get("/api/signout")] +async fn signout(id: Identity) -> impl Responder { + if let Some(_) = id.identity() { + id.forget(); + } + HttpResponse::Ok() +} + +#[get("/questions/{id}")] +async fn get_question( + //session: Session, + id: Identity, + path: WebPath<(u32,)>, +) -> ServiceResult { + is_authenticated(&id)?; + Ok(HttpResponse::Ok().body(format!("User detail: {}", path.into_inner().0))) +} + +struct LevelScore { + level: i32, + points: i32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct Answer { + answer: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct AnswerDatabaseFetch { + answer: String, + points: i32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct AnswerVerifyResp { + correct: bool, + points: i32, +} + +#[post("/api/answer/verify/{id}")] +async fn verify_answer( + //session: Session, + payload: web::Json, + data: web::Data, + id: Identity, + path: WebPath<(u32,)>, +) -> ServiceResult { + is_authenticated(&id)?; + let name = id.identity().unwrap(); + let rec = sqlx::query_as!( + LevelScore, + "SELECT level, points FROM users WHERE name = ($1)", + &name + ) + .fetch_one(&data.db) + .await?; + + let current = path.into_inner().0 as i32; + if rec.level == current { + // TODO + // check answer + let answer = sqlx::query_as!( + AnswerDatabaseFetch, + "SELECT answer, points FROM answers WHERE question_num = ($1)", + ¤t + ) + .fetch_one(&data.db) + .await?; + + let resp; + + // TODO all answers lowercase? + if payload.answer.trim().to_lowercase() == answer.answer { + let points = rec.points + answer.points; + resp = AnswerVerifyResp { + correct: true, + points, + }; + + sqlx::query!( + "UPDATE users SET points = $1, level = $2 WHERE name = $3", + points, + rec.level + 1, + name + ) + .execute(&data.db) + .await?; + } else { + resp = AnswerVerifyResp { + correct: false, + points: rec.points, + }; + } + + return Ok(HttpResponse::Ok().json(resp)); + } else if rec.level > current { + return Err(ServiceError::AlreadyAnswered); + } else { + return Err(ServiceError::AuthorizationRequired); + } +} + +#[get("/api/score")] +async fn score( + //session: Session, + // payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + debug!("{:?}", id.identity()); + is_authenticated(&id)?; + let recs = sqlx::query_as!( + Leader, + "SELECT name, points FROM users ORDER BY points DESC" + ) + .fetch_all(&data.db) + .await?; + Ok(HttpResponse::Ok().json(recs)) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct Leader { + name: String, + points: i32, +} + +#[get("/api/leaderboard")] +async fn leaderboard( + //session: Session, + // payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + let recs = sqlx::query_as!( + Leader, + "SELECT name, points FROM users ORDER BY points DESC" + ) + .fetch_all(&data.db) + .await?; + debug!("{:?}", &recs); + + Ok(HttpResponse::Ok().json(recs)) +} + +pub fn services(cfg: &mut ServiceConfig) { + cfg.service(get_question); + cfg.service(verify_answer); + cfg.service(score); + cfg.service(leaderboard); + cfg.service(signout); + cfg.service(signin); + cfg.service(signup); +} + +fn is_authenticated(id: &Identity) -> ServiceResult { + debug!("{:?}", id.identity()); + // access request identity + if let Some(_) = id.identity() { + Ok(true) + } else { + Err(ServiceError::AuthorizationRequired) + } +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 00000000..0409faeb --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,153 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ +use std::env; + +use config::{Config, ConfigError, Environment, File}; +use log::debug; +use serde::Deserialize; +use url::Url; + +#[derive(Debug, Clone, Deserialize)] +pub struct Server { + // TODO yet to be configured + pub allow_registration: bool, + pub port: u32, + pub domain: String, + pub cookie_secret: String, + pub ip: String, +} + +impl Server { + pub fn get_ip(&self) -> String { + format!("{}:{}", self.ip, self.port) + } +} + +#[derive(Debug, Clone, Deserialize)] +struct DatabaseBuilder { + pub port: u32, + pub hostname: String, + pub username: String, + pub password: String, + pub name: String, + pub url: String, +} + +impl DatabaseBuilder { + fn extract_database_url(url: &Url) -> Self { + // if url.scheme() != "postgres" || url.scheme() != "postgresql" { + // panic!("URL must be postgres://url, url found: {}", url.scheme()); + // } else { + + debug!("Databse name: {}", url.path()); + let mut path = url.path().split("/"); + path.next(); + let name = path.next().expect("no database name").to_string(); + DatabaseBuilder { + port: url.port().expect("Enter database port").into(), + hostname: url.host().expect("Enter database host").to_string(), + username: url.username().into(), + url: url.to_string(), + password: url.password().expect("Enter database password").into(), + name, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Database { + pub url: String, + pub pool: u32, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Settings { + pub debug: bool, + pub database: Database, + pub server: Server, +} + +#[cfg(not(tarpaulin_include))] +impl Settings { + pub fn new() -> Result { + let mut s = Config::new(); + + // setting default values + #[cfg(test)] + s.set_default("database.pool", 2.to_string()) + .expect("Couldn't get the number of CPUs"); + + // merging default config from file + s.merge(File::with_name("./config/default.toml"))?; + + // TODO change PLACEHOLDER to app name + s.merge(Environment::with_prefix("WEBHUNT"))?; + + match env::var("PORT") { + Ok(val) => { + s.set("server.port", val).unwrap(); + } + Err(e) => println!("couldn't interpret PORT: {}", e), + } + + match env::var("DATABASE_URL") { + Ok(val) => { + let url = Url::parse(&val).expect("couldn't parse Database URL"); + let database_conf = DatabaseBuilder::extract_database_url(&url); + set_from_database_url(&mut s, &database_conf); + } + Err(e) => println!("couldn't interpret DATABASE_URL: {}", e), + } + + set_database_url(&mut s); + + s.try_into() + } +} + +fn set_from_database_url(s: &mut Config, database_conf: &DatabaseBuilder) { + s.set("database.username", database_conf.username.clone()) + .expect("Couldn't set database username"); + s.set("database.password", database_conf.password.clone()) + .expect("Couldn't access database password"); + s.set("database.hostname", database_conf.hostname.clone()) + .expect("Couldn't access database hostname"); + s.set("database.port", database_conf.port as i64) + .expect("Couldn't access database port"); + s.set("database.name", database_conf.name.clone()) + .expect("Couldn't access database name"); +} + +fn set_database_url(s: &mut Config) { + s.set( + "database.url", + format!( + r"postgres://{}:{}@{}:{}/{}", + s.get::("database.username") + .expect("Couldn't access database username"), + s.get::("database.password") + .expect("Couldn't access database password"), + s.get::("database.hostname") + .expect("Couldn't access database hostname"), + s.get::("database.port") + .expect("Couldn't access database port"), + s.get::("database.name") + .expect("Couldn't access database name") + ), + ) + .expect("Couldn't set databse url"); +}