feat: bootstarp gists

AUTHENTICATION

- Sign Up
- Sign IN

ACCOUNT
- Username Exists
- Email Exists
- Account delete
- Password update
- Email update
- Username update
- Get account secret
- Update secret

All routes are implemented with proper error handling and testing

CONFIGURATION
See ./config/default.toml for full list
master
Aravinth Manivannan 2022-02-12 23:48:35 +05:30
parent 3447a3a45c
commit 34a67a5535
29 changed files with 3338 additions and 6 deletions

201
Cargo.lock generated
View File

@ -2,6 +2,18 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "actix-auth-middleware"
version = "0.2.0"
source = "git+https://github.com/realaravinth/actix-auth-middleware?branch=v4#29737aeb5594487e4192b5f2fb1a76a15c36c8f2"
dependencies = [
"actix-http",
"actix-identity",
"actix-service",
"actix-web",
"futures",
]
[[package]]
name = "actix-codec"
version = "0.4.2"
@ -56,6 +68,21 @@ dependencies = [
"zstd",
]
[[package]]
name = "actix-identity"
version = "0.4.0-beta.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f084963856cf7990b1f21d6298626de4ae6178385cadece312e12c9f7a9f432"
dependencies = [
"actix-service",
"actix-utils",
"actix-web",
"futures-util",
"serde 1.0.136",
"serde_json",
"time 0.3.7",
]
[[package]]
name = "actix-macros"
version = "0.2.3"
@ -198,6 +225,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
dependencies = [
"generic-array",
]
[[package]]
name = "aes"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"opaque-debug",
]
[[package]]
name = "aes-gcm"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "ahash"
version = "0.7.6"
@ -429,6 +491,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cipher"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
dependencies = [
"generic-array",
]
[[package]]
name = "config"
version = "0.11.0"
@ -469,7 +540,14 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
dependencies = [
"aes-gcm",
"base64",
"hkdf",
"hmac 0.12.0",
"percent-encoding",
"rand 0.8.4",
"sha2 0.10.1",
"subtle",
"time 0.3.7",
"version_check",
]
@ -556,6 +634,15 @@ dependencies = [
"subtle",
]
[[package]]
name = "ctr"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
dependencies = [
"cipher",
]
[[package]]
name = "darling"
version = "0.12.4"
@ -680,6 +767,7 @@ checksum = "8cb780dce4f9a8f5c087362b3a4595936b2019e7c8b30f2c3e9a7e94e6ae9837"
dependencies = [
"block-buffer 0.10.2",
"crypto-common",
"subtle",
]
[[package]]
@ -801,6 +889,21 @@ dependencies = [
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.21"
@ -839,6 +942,23 @@ dependencies = [
"parking_lot",
]
[[package]]
name = "futures-io"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
[[package]]
name = "futures-macro"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.21"
@ -857,9 +977,13 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
@ -897,11 +1021,23 @@ dependencies = [
"wasi 0.10.2+wasi-snapshot-preview1",
]
[[package]]
name = "ghash"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "gists"
version = "0.1.0"
dependencies = [
"actix-auth-middleware",
"actix-http",
"actix-identity",
"actix-rt",
"actix-web",
"actix-web-codegen 0.5.0-rc.2 (git+https://github.com/realaravinth/actix-web)",
@ -911,6 +1047,7 @@ dependencies = [
"db-sqlx-postgres",
"db-sqlx-sqlite",
"derive_more",
"futures",
"git2",
"lazy_static",
"log",
@ -922,6 +1059,7 @@ dependencies = [
"sqlx",
"tokio",
"url",
"urlencoding",
"validator",
]
@ -1001,6 +1139,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "158bc31e00a68e380286904cc598715f861f2b0ccf7aa6fe20c6d0c49ca5d0f6"
dependencies = [
"hmac 0.12.0",
]
[[package]]
name = "hmac"
version = "0.11.0"
@ -1011,6 +1158,15 @@ dependencies = [
"digest 0.9.0",
]
[[package]]
name = "hmac"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddca131f3e7f2ce2df364b57949a9d47915cfbd35e46cfee355ccebbf794d6a2"
dependencies = [
"digest 0.10.2",
]
[[package]]
name = "html5ever"
version = "0.25.1"
@ -1601,6 +1757,18 @@ version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
[[package]]
name = "polyval"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "ppv-lite86"
version = "0.2.16"
@ -2025,6 +2193,17 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "sha2"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.2",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
@ -2124,7 +2303,7 @@ dependencies = [
"futures-util",
"hashlink",
"hex",
"hmac",
"hmac 0.11.0",
"indexmap",
"itoa",
"libc",
@ -2140,7 +2319,7 @@ dependencies = [
"serde 1.0.136",
"serde_json",
"sha-1 0.9.8",
"sha2",
"sha2 0.9.9",
"smallvec",
"sqlformat",
"sqlx-rt",
@ -2170,7 +2349,7 @@ dependencies = [
"quote",
"serde 1.0.136",
"serde_json",
"sha2",
"sha2 0.9.9",
"sqlx-core",
"sqlx-rt",
"syn",
@ -2555,6 +2734,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "universal-hash"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.7.1"
@ -2573,6 +2762,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "urlencoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821"
[[package]]
name = "utf-8"
version = "0.7.6"

View File

@ -21,13 +21,18 @@ members = [
]
[dependencies]
actix-auth-middleware = { branch="v4", version = "0.2", git = "https://github.com/realaravinth/actix-auth-middleware", features = ["actix_identity_backend"] }
actix-web = "4.0.0-rc.3"
actix-http = "3.0.0-rc.2"
actix-identity = "0.4.0-beta.8"
actix-rt = "2.6.0"
argon2-creds = { branch = "master", git = "https://github.com/realaravinth/argon2-creds"}
config = "0.11"
db-core = {path = "./database/db-core"}
db-sqlx-postgres = {path = "./database/db-sqlx-postgres"}
db-sqlx-sqlite = {path = "./database/db-sqlx-sqlite"}
derive_more = "0.99"
futures = "0.3.21"
git2 = "0.13.25"
lazy_static = "1.4"
log = "0.4"
@ -37,12 +42,13 @@ pretty_env_logger = "0.4"
rand = "0.8.4"
serde = { version = "1", features = ["derive"]}
serde_json = "1"
sqlx = { version = "0.5.10", features = [ "runtime-actix-rustls", "uuid", "postgres", "time", "offline", "sqlite" ] }
tokio = "1.16.1"
url = "2.2"
urlencoding = "2.1.0"
validator = { version = "0.14.0", features = ["derive"] }
[dev-dependencies]
db-sqlx-postgres = {path = "./database/db-sqlx-postgres"}
db-sqlx-sqlite = {path = "./database/db-sqlx-sqlite"}
actix-rt = "2"
sqlx = { version = "0.5.10", features = [ "runtime-actix-rustls", "uuid", "postgres", "time", "offline", "sqlite" ] }

36
config/default.toml Normal file
View File

@ -0,0 +1,36 @@
log = "info" # possible values: "info", "warn", "trace", "error", "debug"
source_code = "https://github.com/realaravinth/gists"
allow_registration = true # allow registration on server
allow_demo = true # allow demo on server
[server]
# 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"
proxy_has_tls = false
cookie_secret = "k&y8G#J&2gesW&N6hNauy63vgRzq9ZLPb39"
#workers = 2
[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 = "postgres"
pool = 4
database_type = "postgres"
[repository]
root = "/tmp/gists.batsense.net"

17
src/api/mod.rs Normal file
View File

@ -0,0 +1,17 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
pub mod v1;

173
src/api/v1/account/mod.rs Normal file
View File

@ -0,0 +1,173 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use crate::data::api::v1::account::*;
use crate::data::api::v1::auth::Password;
use crate::errors::*;
use crate::AppData;
#[cfg(test)]
pub mod test;
pub use super::auth;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AccountCheckPayload {
pub val: String,
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(username_exists);
cfg.service(set_username);
cfg.service(email_exists);
cfg.service(set_email);
cfg.service(delete_account);
cfg.service(update_user_password);
cfg.service(get_secret);
cfg.service(update_user_secret);
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Email {
pub email: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Username {
pub username: String,
}
/// update username
#[my_codegen::post(
path = "crate::V1_API_ROUTES.account.update_username",
wrap = "super::get_auth_middleware()"
)]
async fn set_username(
id: Identity,
payload: web::Json<Username>,
data: AppData,
db: crate::DB,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let new_name = data
.update_username(&(**db), &username, &payload.username)
.await?;
id.forget();
id.remember(new_name);
Ok(HttpResponse::Ok())
}
#[my_codegen::post(path = "crate::V1_API_ROUTES.account.username_exists")]
async fn username_exists(
payload: web::Json<AccountCheckPayload>,
data: AppData,
db: crate::DB,
) -> ServiceResult<impl Responder> {
Ok(HttpResponse::Ok().json(data.username_exists(&(**db), &payload.val).await?))
}
#[my_codegen::post(path = "crate::V1_API_ROUTES.account.email_exists")]
pub async fn email_exists(
payload: web::Json<AccountCheckPayload>,
data: AppData,
db: crate::DB,
) -> ServiceResult<impl Responder> {
Ok(HttpResponse::Ok().json(data.email_exists(&(**db), &payload.val).await?))
}
/// update email
#[my_codegen::post(
path = "crate::V1_API_ROUTES.account.update_email",
wrap = "super::get_auth_middleware()"
)]
async fn set_email(
id: Identity,
payload: web::Json<Email>,
data: AppData,
db: crate::DB,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
data.set_email(&(**db), &username, &payload.email).await?;
Ok(HttpResponse::Ok())
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.account.delete",
wrap = "super::get_auth_middleware()"
)]
async fn delete_account(
id: Identity,
payload: web::Json<Password>,
data: AppData,
db: crate::DB,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
data.delete_user(&(**db), &username, &payload.password)
.await?;
id.forget();
Ok(HttpResponse::Ok())
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.account.update_password",
wrap = "super::get_auth_middleware()"
)]
async fn update_user_password(
id: Identity,
data: AppData,
db: crate::DB,
payload: web::Json<ChangePasswordReqest>,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let payload = payload.into_inner();
data.change_password(&(**db), &username, &payload).await?;
Ok(HttpResponse::Ok())
}
#[my_codegen::get(
path = "crate::V1_API_ROUTES.account.get_secret",
wrap = "super::get_auth_middleware()"
)]
async fn get_secret(id: Identity, data: AppData, db: crate::DB) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let secret = data.get_secret(&(**db), &username).await?;
Ok(HttpResponse::Ok().json(secret))
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.account.update_secret",
wrap = "super::get_auth_middleware()"
)]
async fn update_user_secret(
id: Identity,
data: AppData,
db: crate::DB,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let _secret = data.update_user_secret(&(**db), &username).await?;
Ok(HttpResponse::Ok())
}

344
src/api/v1/account/test.rs Normal file
View File

@ -0,0 +1,344 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
use std::sync::Arc;
use actix_web::http::StatusCode;
use actix_web::test;
use super::*;
use crate::api::v1::ROUTES;
use crate::data::api::v1::account::*;
use crate::data::api::v1::auth::Password;
use crate::data::Data;
use crate::errors::*;
use crate::*;
use crate::tests::*;
#[actix_rt::test]
async fn postgrest_account_works() {
let (db, data) = sqlx_postgres::get_data().await;
uname_email_exists_works(data.clone(), db.clone()).await;
email_udpate_password_validation_del_userworks(data.clone(), db.clone()).await;
username_update_works(data.clone(), db.clone()).await;
update_password_works(data.clone(), db.clone()).await;
}
#[actix_rt::test]
async fn sqlite_account_works() {
let (db, data) = sqlx_sqlite::get_data().await;
uname_email_exists_works(data.clone(), db.clone()).await;
email_udpate_password_validation_del_userworks(data.clone(), db.clone()).await;
username_update_works(data.clone(), db.clone()).await;
update_password_works(data.clone(), db.clone()).await;
}
async fn uname_email_exists_works(data: Arc<Data>, db: BoxDB) {
const NAME: &str = "testuserexists";
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuserexists@a.com2";
let db = &db;
let _ = data.delete_user(db, NAME, PASSWORD).await;
let (_, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data, db).await;
// chech if get user secret works
let resp = test::call_service(
&app,
test::TestRequest::get()
.cookie(cookies.clone())
.uri(ROUTES.account.get_secret)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
// chech if get user secret works
let resp = test::call_service(
&app,
test::TestRequest::post()
.cookie(cookies.clone())
.uri(ROUTES.account.update_secret)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let mut payload = AccountCheckPayload { val: NAME.into() };
let user_exists_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.account.username_exists)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(user_exists_resp.status(), StatusCode::OK);
let mut resp: AccountCheckResp = test::read_body_json(user_exists_resp).await;
assert!(resp.exists);
payload.val = PASSWORD.into();
let user_doesnt_exist = test::call_service(
&app,
post_request!(&payload, ROUTES.account.username_exists)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(user_doesnt_exist.status(), StatusCode::OK);
resp = test::read_body_json(user_doesnt_exist).await;
assert!(!resp.exists);
let email_doesnt_exist = test::call_service(
&app,
post_request!(&payload, ROUTES.account.email_exists)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(email_doesnt_exist.status(), StatusCode::OK);
resp = test::read_body_json(email_doesnt_exist).await;
assert!(!resp.exists);
payload.val = EMAIL.into();
let email_exist = test::call_service(
&app,
post_request!(&payload, ROUTES.account.email_exists)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(email_exist.status(), StatusCode::OK);
resp = test::read_body_json(email_exist).await;
assert!(resp.exists);
}
async fn email_udpate_password_validation_del_userworks(data: Arc<Data>, db: BoxDB) {
const NAME: &str = "testuser2";
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuser1@a.com2";
const NAME2: &str = "eupdauser";
const EMAIL2: &str = "eupdauser@a.com";
let _ = data.delete_user(&db, NAME, PASSWORD).await;
let _ = data.delete_user(&db, NAME2, PASSWORD).await;
let _ = data.register_and_signin(&db, NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = data.register_and_signin(&db, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data, db).await;
// update email
let mut email_payload = Email {
email: EMAIL.into(),
};
let email_update_resp = test::call_service(
&app,
post_request!(&email_payload, ROUTES.account.update_email)
//post_request!(&email_payload, EMAIL_UPDATE)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(email_update_resp.status(), StatusCode::OK);
// check duplicate email while duplicate email
email_payload.email = EMAIL2.into();
data.bad_post_req_test(
&db,
NAME,
PASSWORD,
ROUTES.account.update_email,
&email_payload,
ServiceError::EmailTaken,
)
.await;
// wrong password while deleteing account
let mut payload = Password {
password: NAME.into(),
};
data.bad_post_req_test(
&db,
NAME,
PASSWORD,
ROUTES.account.delete,
&payload,
ServiceError::WrongPassword,
)
.await;
// delete account
payload.password = PASSWORD.into();
let delete_user_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.account.delete)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(delete_user_resp.status(), StatusCode::OK);
// try to delete an account that doesn't exist
let account_not_found_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.account.delete)
.cookie(cookies)
.to_request(),
)
.await;
assert_eq!(account_not_found_resp.status(), StatusCode::NOT_FOUND);
let txt: ErrorToResponse = test::read_body_json(account_not_found_resp).await;
assert_eq!(txt.error, format!("{}", ServiceError::AccountNotFound));
}
async fn username_update_works(data: Arc<Data>, db: BoxDB) {
const NAME: &str = "testuserupda";
const EMAIL: &str = "testuserupda@sss.com";
const EMAIL2: &str = "testuserupda2@sss.com";
const PASSWORD: &str = "longpassword2";
const NAME2: &str = "terstusrtds";
const NAME_CHANGE: &str = "terstusrtdsxx";
let db = &db;
let _ = futures::join!(
data.delete_user(db, NAME, PASSWORD),
data.delete_user(db, NAME2, PASSWORD),
data.delete_user(db, NAME_CHANGE, PASSWORD)
);
let _ = data.register_and_signin(db, NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data, db).await;
// update username
let mut username_udpate = Username {
username: NAME_CHANGE.into(),
};
let username_update_resp = test::call_service(
&app,
post_request!(&username_udpate, ROUTES.account.update_username)
.cookie(cookies)
.to_request(),
)
.await;
assert_eq!(username_update_resp.status(), StatusCode::OK);
// check duplicate username with duplicate username
username_udpate.username = NAME2.into();
data.bad_post_req_test(
db,
NAME_CHANGE,
PASSWORD,
ROUTES.account.update_username,
&username_udpate,
ServiceError::UsernameTaken,
)
.await;
}
async fn update_password_works(data: Arc<Data>, db: BoxDB) {
const NAME: &str = "updatepassuser";
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "updatepassuser@a.com";
let db = &db;
let _ = data.delete_user(db, NAME, PASSWORD).await;
let (_, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data, db).await;
let new_password = "newpassword";
let update_password = ChangePasswordReqest {
password: PASSWORD.into(),
new_password: new_password.into(),
confirm_new_password: PASSWORD.into(),
};
let res = data.change_password(db, NAME, &update_password).await;
assert!(res.is_err());
assert_eq!(res, Err(ServiceError::PasswordsDontMatch));
let update_password = ChangePasswordReqest {
password: PASSWORD.into(),
new_password: new_password.into(),
confirm_new_password: new_password.into(),
};
assert!(data
.change_password(db, NAME, &update_password)
.await
.is_ok());
let update_password = ChangePasswordReqest {
password: new_password.into(),
new_password: new_password.into(),
confirm_new_password: PASSWORD.into(),
};
data.bad_post_req_test(
db,
NAME,
new_password,
ROUTES.account.update_password,
&update_password,
ServiceError::PasswordsDontMatch,
)
.await;
let update_password = ChangePasswordReqest {
password: PASSWORD.into(),
new_password: PASSWORD.into(),
confirm_new_password: PASSWORD.into(),
};
data.bad_post_req_test(
db,
NAME,
new_password,
ROUTES.account.update_password,
&update_password,
ServiceError::WrongPassword,
)
.await;
let update_password = ChangePasswordReqest {
password: new_password.into(),
new_password: PASSWORD.into(),
confirm_new_password: PASSWORD.into(),
};
let update_password_resp = test::call_service(
&app,
post_request!(&update_password, ROUTES.account.update_password)
.cookie(cookies)
.to_request(),
)
.await;
assert_eq!(update_password_resp.status(), StatusCode::OK);
}

76
src/api/v1/auth.rs Normal file
View File

@ -0,0 +1,76 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
use crate::data::api::v1::auth::{Login, Register};
use actix_identity::Identity;
use actix_web::http::header;
use actix_web::{web, HttpResponse, Responder};
use super::RedirectQuery;
use crate::errors::*;
use crate::AppData;
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(register);
cfg.service(login);
cfg.service(signout);
}
#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.register")]
async fn register(
payload: web::Json<Register>,
data: AppData,
db: crate::DB,
) -> ServiceResult<impl Responder> {
data.register(&(**db), &payload).await?;
Ok(HttpResponse::Ok())
}
#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.login")]
async fn login(
id: Identity,
payload: web::Json<Login>,
query: web::Query<RedirectQuery>,
data: AppData,
db: crate::DB,
) -> ServiceResult<impl Responder> {
let payload = payload.into_inner();
let username = data.login(&(**db), &payload).await?;
id.remember(username);
let query = query.into_inner();
if let Some(redirect_to) = query.redirect_to {
Ok(HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to))
.finish())
} else {
Ok(HttpResponse::Ok().into())
}
}
#[my_codegen::get(
path = "crate::V1_API_ROUTES.auth.logout",
wrap = "super::get_auth_middleware()"
)]
async fn signout(id: Identity) -> impl Responder {
use actix_auth_middleware::GetLoginRoute;
if id.identity().is_some() {
id.forget();
}
HttpResponse::Found()
.append_header((header::LOCATION, crate::V1_API_ROUTES.get_login_route(None)))
.finish()
}

94
src/api/v1/meta.rs Normal file
View File

@ -0,0 +1,94 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use crate::{DB, GIT_COMMIT_HASH, VERSION};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BuildDetails {
pub version: &'static str,
pub git_commit_hash: &'static str,
}
pub mod routes {
pub struct Meta {
pub build_details: &'static str,
pub health: &'static str,
}
impl Meta {
pub const fn new() -> Self {
Self {
build_details: "/api/v1/meta/build",
health: "/api/v1/meta/health",
}
}
}
}
/// emmits build details of the bninary
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.build_details")]
async fn build_details() -> impl Responder {
let build = BuildDetails {
version: VERSION,
git_commit_hash: GIT_COMMIT_HASH,
};
HttpResponse::Ok().json(build)
}
#[derive(Clone, Debug, Deserialize, Serialize)]
/// Health check return datatype
pub struct Health {
db: bool,
}
/// emmits build details of the bninary
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.health")]
async fn health(db: DB) -> impl Responder {
let health = Health {
db: db.ping().await,
};
HttpResponse::Ok().json(health)
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(build_details);
cfg.service(health);
}
#[cfg(test)]
mod tests {
use actix_web::{http::StatusCode, test, App};
use crate::routes::services;
use crate::*;
#[actix_rt::test]
async fn build_details_works() {
let app = test::init_service(App::new().configure(services)).await;
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri(V1_API_ROUTES.meta.build_details)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
}
}

44
src/api/v1/mod.rs Normal file
View File

@ -0,0 +1,44 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
use actix_auth_middleware::Authentication;
use actix_web::web::ServiceConfig;
use serde::Deserialize;
pub mod account;
pub mod auth;
pub mod meta;
pub mod routes;
pub use routes::ROUTES;
pub fn services(cfg: &mut ServiceConfig) {
auth::services(cfg);
account::services(cfg);
meta::services(cfg);
}
#[derive(Deserialize)]
pub struct RedirectQuery {
pub redirect_to: Option<String>,
}
pub fn get_auth_middleware() -> Authentication<routes::Routes> {
Authentication::with_identity(ROUTES)
}
#[cfg(test)]
mod tests;

130
src/api/v1/routes.rs Normal file
View File

@ -0,0 +1,130 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
//! V1 API Routes
use actix_auth_middleware::{Authentication, GetLoginRoute};
use super::meta::routes::Meta;
/// constant [Routes](Routes) instance
pub const ROUTES: Routes = Routes::new();
/// Authentication routes
pub struct Auth {
/// logout route
pub logout: &'static str,
/// login route
pub login: &'static str,
/// registration route
pub register: &'static str,
}
impl Auth {
/// create new instance of Authentication route
pub const fn new() -> Auth {
let login = "/api/v1/signin";
let logout = "/logout";
let register = "/api/v1/signup";
Auth {
logout,
login,
register,
}
}
}
/// Account management routes
pub struct Account {
/// delete account route
pub delete: &'static str,
/// route to check if an email exists
pub email_exists: &'static str,
/// route to fetch account secret
pub get_secret: &'static str,
/// route to update a user's email
pub update_email: &'static str,
/// route to update password
pub update_password: &'static str,
/// route to update secret
pub update_secret: &'static str,
/// route to check if a username is already registered
pub username_exists: &'static str,
/// route to change username
pub update_username: &'static str,
}
impl Account {
/// create a new instance of [Account][Account] routes
pub const fn new() -> Account {
let get_secret = "/api/v1/account/secret/get";
let update_secret = "/api/v1/account/secret/update";
let delete = "/api/v1/account/delete";
let email_exists = "/api/v1/account/email/exists";
let username_exists = "/api/v1/account/username/exists";
let update_username = "/api/v1/account/username/update";
let update_email = "/api/v1/account/email/update";
let update_password = "/api/v1/account/password/update";
Account {
delete,
email_exists,
get_secret,
update_email,
update_password,
update_secret,
username_exists,
update_username,
}
}
}
/// Top-level routes data structure for V1 AP1
pub struct Routes {
/// Authentication routes
pub auth: Auth,
/// Account routes
pub account: Account,
/// Meta routes
pub meta: Meta,
}
impl Routes {
/// create new instance of Routes
const fn new() -> Routes {
Routes {
auth: Auth::new(),
account: Account::new(),
meta: Meta::new(),
}
}
}
pub fn get_auth_middleware() -> Authentication<Routes> {
Authentication::with_identity(ROUTES)
}
impl GetLoginRoute for Routes {
fn get_login_route(&self, src: Option<&str>) -> String {
if let Some(redirect_to) = src {
format!(
"{}?redirect_to={}",
self.auth.login,
urlencoding::encode(redirect_to)
)
} else {
self.auth.register.to_string()
}
}
}

203
src/api/v1/tests/auth.rs Normal file
View File

@ -0,0 +1,203 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
use std::sync::Arc;
use actix_auth_middleware::GetLoginRoute;
use actix_web::http::{header, StatusCode};
use actix_web::test;
use crate::api::v1::ROUTES;
use crate::data::api::v1::auth::{Login, Register};
use crate::data::Data;
use crate::db::BoxDB;
use crate::errors::*;
use crate::*;
use crate::tests::*;
#[actix_rt::test]
async fn postgrest_auth_works() {
let (db, data) = sqlx_postgres::get_data().await;
auth_works(data.clone(), db.clone()).await;
serverside_password_validation_works(data, db).await;
}
#[actix_rt::test]
async fn sqlite_auth_works() {
let (db, data) = sqlx_sqlite::get_data().await;
auth_works(data.clone(), db.clone()).await;
serverside_password_validation_works(data, db).await;
}
async fn auth_works(data: Arc<Data>, db: BoxDB) {
const NAME: &str = "testuserfoo";
const PASSWORD: &str = "longpassword";
const EMAIL: &str = "testuser1foo@a.com";
let _ = data.delete_user(&db, NAME, PASSWORD).await;
let app = get_app!(data, db).await;
// 1. Register with email == None
let msg = Register {
username: NAME.into(),
password: PASSWORD.into(),
confirm_password: PASSWORD.into(),
email: None,
};
let resp =
test::call_service(&app, post_request!(&msg, ROUTES.auth.register).to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
// delete user TODO
let _ = data.delete_user(&db, NAME, PASSWORD).await;
// 1. Register and signin
let (_, signin_resp) = data.register_and_signin(&db, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
// Sign in with email
data.signin_test(&db, EMAIL, PASSWORD).await;
// 2. check if duplicate username is allowed
let mut msg = Register {
username: NAME.into(),
password: PASSWORD.into(),
confirm_password: PASSWORD.into(),
email: Some(EMAIL.into()),
};
msg.username = format!("asdfasd{}", msg.username);
data.bad_post_req_test(
&db,
NAME,
PASSWORD,
ROUTES.auth.register,
&msg,
ServiceError::EmailTaken,
)
.await;
msg.email = Some(format!