switch to rust, implement discord login and switch to svelte
This commit is contained in:
parent
4d32a3e146
commit
a297a965aa
73 changed files with 4525 additions and 2038 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -42,6 +42,11 @@ build/Release
|
|||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
/public/build/
|
||||
|
||||
.DS_Store
|
||||
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
|
@ -108,6 +113,7 @@ dist
|
|||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
.vscode/
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
|
@ -127,3 +133,6 @@ pnpm-lock.yaml
|
|||
backend/config.json
|
||||
|
||||
*.pem
|
||||
.env
|
||||
test_coverage/
|
||||
target/
|
||||
|
|
3
backend/.env.example
Normal file
3
backend/.env.example
Normal file
|
@ -0,0 +1,3 @@
|
|||
DATABASE_URL=postgres://postgres@localhost/todo_dev
|
||||
LOG_LEVEL=DEBUG
|
||||
PORT=8080
|
|
@ -1,16 +0,0 @@
|
|||
arrowParens: 'always'
|
||||
bracketSpacing: true
|
||||
endOfLine: 'lf'
|
||||
htmlWhitespaceSensitivity: 'css'
|
||||
insertPragma: false
|
||||
jsxBracketSameLine: true
|
||||
jsxSingleQuote: true
|
||||
printWidth: 120
|
||||
proseWrap: 'preserve'
|
||||
quoteProps: 'consistent'
|
||||
requirePragma: false
|
||||
semi: true
|
||||
singleQuote: true
|
||||
tabWidth: 2
|
||||
trailingComma: 'none'
|
||||
useTabs: false
|
2418
backend/Cargo.lock
generated
Normal file
2418
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
33
backend/Cargo.toml
Normal file
33
backend/Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
edition = "2018"
|
||||
name = "backend"
|
||||
version = "0.1.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-lock = "2.4.0"
|
||||
async-redis-session = {git = "https://github.com/jbr/async-redis-session", version = "0.2.2"}
|
||||
async-session = "3.0.0"
|
||||
async-std = {version = "1.9.0", features = ["attributes"]}
|
||||
axum = {version = "0.1.1", features = ["headers"]}
|
||||
chrono = {version = "0.4.0", features = ["serde"]}
|
||||
diesel = {version = "1.4.7", features = ["postgres", "chrono", "serde_json", "r2d2", "uuidv07"]}
|
||||
diesel_migrations = {version = "1.4.0"}
|
||||
dotenv = "0.15.0"
|
||||
fern = "0.6.0"
|
||||
headers = "0.3.4"
|
||||
http = "0.2.4"
|
||||
hyper = {version = "0.14.11", features = ["full"]}
|
||||
log = "0.4.14"
|
||||
oauth2 = "4.1.0"
|
||||
redis = {version = "0.21.0", features = ["aio", "async-std-comp"]}
|
||||
reqwest = {version = "0.11.4", features = ["json"]}
|
||||
serde = {version = "1.0.127", features = ["derive"]}
|
||||
serde-redis = "0.10.0"
|
||||
serde_json = "1.0.66"
|
||||
tokio = {version = "1.9.0", features = ["full"]}
|
||||
tokio-diesel = {git = "https://github.com/mehcode/tokio-diesel", version = "0.3.0"}
|
||||
tower = {version = "0.4.6", features = ["full"]}
|
||||
url = "2.2.2"
|
||||
uuid = {version = "0.8.2", features = ["serde", "v4"]}
|
|
@ -1,2 +0,0 @@
|
|||
# todo
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"secret": "TEST_SECRET",
|
||||
"https": true,
|
||||
"alter_db": true,
|
||||
"port": 8080,
|
||||
"frontend_url": "localhost:3000",
|
||||
"db_url": "postgres://postgres:@127.0.0.1/todo",
|
||||
"cert": "",
|
||||
"cert_key": "",
|
||||
"mail_host": "",
|
||||
"mail_port": 465,
|
||||
"mail_username": "",
|
||||
"mail_password": "",
|
||||
"discord_id": "",
|
||||
"discord_secret": ""
|
||||
}
|
0
backend/default.profraw
Normal file
0
backend/default.profraw
Normal file
5
backend/diesel.toml
Normal file
5
backend/diesel.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
26
backend/env.sh
Executable file
26
backend/env.sh
Executable file
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
|
||||
args=("$@")
|
||||
|
||||
if [[ ${1} == "prod" ]]; then
|
||||
echo "prod build"
|
||||
export RUSTFLAGS=""
|
||||
ARGS="--release"
|
||||
if [[ ${2} ]]; then
|
||||
cargo ${2} $ARGS ${args[@]:2}
|
||||
else
|
||||
echo "defaulting to build"
|
||||
cargo build $ARGS
|
||||
fi
|
||||
else
|
||||
echo "dev build"
|
||||
export RUSTFLAGS="-Zinstrument-coverage -Zmacro-backtrace"
|
||||
ARGS=""
|
||||
if [[ ${1} ]]; then
|
||||
cargo ${1} $ARGS ${args[@]:1}
|
||||
else
|
||||
echo "defaulting to build+tests"
|
||||
cargo test
|
||||
grcov . --binary-path ./target/debug -s . -t html --branch --ignore-not-existing -o ./test_coverage/
|
||||
fi
|
||||
fi
|
0
backend/migrations/.gitkeep
Normal file
0
backend/migrations/.gitkeep
Normal file
|
@ -0,0 +1,6 @@
|
|||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
|
@ -0,0 +1,36 @@
|
|||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
|
||||
|
||||
|
||||
-- Sets up a trigger for the given table to automatically set a column called
|
||||
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||
-- in the modified columns)
|
||||
--
|
||||
-- # Example
|
||||
--
|
||||
-- ```sql
|
||||
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
|
||||
--
|
||||
-- SELECT diesel_manage_updated_at('users');
|
||||
-- ```
|
||||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF (
|
||||
NEW IS DISTINCT FROM OLD AND
|
||||
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||
) THEN
|
||||
NEW.updated_at := current_timestamp;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
|
@ -0,0 +1,2 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE IF EXISTS users;
|
9
backend/migrations/2021-08-05-011028_create_user/up.sql
Normal file
9
backend/migrations/2021-08-05-011028_create_user/up.sql
Normal file
|
@ -0,0 +1,9 @@
|
|||
-- Your SQL goes here
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
discord_id VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
SELECT diesel_manage_updated_at('users');
|
|
@ -0,0 +1,2 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE IF EXISTS blocks;
|
12
backend/migrations/2021-08-06-173217_create_block/up.sql
Normal file
12
backend/migrations/2021-08-06-173217_create_block/up.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
-- Your SQL goes here
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
user_id UUID NOT NULL,
|
||||
block_type VARCHAR(255) NOT NULL,
|
||||
props JSON NOT NULL,
|
||||
children TEXT[],
|
||||
created_at TIMESTAMP DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
SELECT diesel_manage_updated_at('blocks');
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"name": "todo",
|
||||
"version": "1.0.0",
|
||||
"description": "todo list app (because it hasnt been done before)",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"who": "pwd",
|
||||
"start": "node src/index.js",
|
||||
"test": "mocha"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@ssh.gitdab.com:jane/todo.git"
|
||||
},
|
||||
"author": "jane <jane@j4.pm>",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"express-paginate": "^1.0.2",
|
||||
"http-proxy": "^1.18.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nodemailer": "^6.6.2",
|
||||
"pg": "^8.6.0",
|
||||
"sequelize": "^6.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.4",
|
||||
"mocha": "^9.0.2",
|
||||
"proxyrequire": "^1.0.21",
|
||||
"sequelize-cli": "^6.2.0",
|
||||
"sequelize-test-helpers": "^1.3.3",
|
||||
"sinon": "^11.1.1"
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
const fs = require('fs');
|
||||
|
||||
if (!global.config) {
|
||||
global.config = {}
|
||||
const cfg = JSON.parse(fs.readFileSync('./config.json'));
|
||||
if (cfg) {
|
||||
global.config = cfg;
|
||||
}
|
||||
}
|
||||
|
||||
class Config {
|
||||
get config() {
|
||||
return global.config;
|
||||
}
|
||||
|
||||
set config(dat) {
|
||||
global.config = dat;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Config();
|
|
@ -1,20 +0,0 @@
|
|||
const Sequelize = require('sequelize');
|
||||
const Config = require('./config.js');
|
||||
const Models = require('./models');
|
||||
|
||||
if (!Config.config.db_url) {
|
||||
console.error('No database url found. please set `db_url` in config.json');
|
||||
process.exit();
|
||||
}
|
||||
|
||||
const db = new Sequelize(Config.config.db_url);
|
||||
|
||||
module.exports = {
|
||||
db: db,
|
||||
constructors: {
|
||||
user: () => {
|
||||
return User.build();
|
||||
}
|
||||
},
|
||||
schemas: Models(db, Sequelize)
|
||||
};
|
30
backend/src/endpoints.rs
Normal file
30
backend/src/endpoints.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use crate::diesel::PgConnection;
|
||||
use crate::logging::LogService;
|
||||
use async_redis_session::RedisSessionStore;
|
||||
use axum::{prelude::*, routing::BoxRoute, AddExtensionLayer};
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use std::env;
|
||||
|
||||
pub mod block;
|
||||
pub mod discord;
|
||||
pub mod user;
|
||||
|
||||
// this should never get called, because the reverse
|
||||
// proxy on caddy should only direct calls from /api
|
||||
async fn root() -> &'static str {
|
||||
"Hi"
|
||||
}
|
||||
|
||||
pub fn get_routes(pool: Pool<ConnectionManager<PgConnection>>) -> BoxRoute<Body> {
|
||||
let redis_url = env::var("REDIS_URL").unwrap_or(String::from("redis://localhost"));
|
||||
|
||||
let client = redis::Client::open(redis_url.as_str()).expect("Could not create redis client.");
|
||||
route("/", get(root))
|
||||
.nest("/discord", discord::get_routes())
|
||||
.layer(tower::layer::layer_fn(|service| LogService { service }))
|
||||
.layer(AddExtensionLayer::new(RedisSessionStore::from_client(
|
||||
client,
|
||||
)))
|
||||
.layer(AddExtensionLayer::new(pool))
|
||||
.boxed()
|
||||
}
|
0
backend/src/endpoints/block.rs
Normal file
0
backend/src/endpoints/block.rs
Normal file
214
backend/src/endpoints/discord.rs
Normal file
214
backend/src/endpoints/discord.rs
Normal file
|
@ -0,0 +1,214 @@
|
|||
use async_redis_session::RedisSessionStore;
|
||||
use async_session::{Session, SessionStore};
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{Extension, FromRequest, Query, RequestParts, TypedHeader},
|
||||
prelude::*,
|
||||
response::IntoResponse,
|
||||
routing::BoxRoute,
|
||||
AddExtensionLayer,
|
||||
};
|
||||
use http::{header::SET_COOKIE, StatusCode};
|
||||
use hyper::Body;
|
||||
use oauth2::{
|
||||
basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId,
|
||||
ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
|
||||
static COOKIE_NAME: &str = "SESSION";
|
||||
|
||||
fn oauth_client() -> BasicClient {
|
||||
// Environment variables (* = required):
|
||||
// *"CLIENT_ID" "123456789123456789";
|
||||
// *"CLIENT_SECRET" "rAn60Mch4ra-CTErsSf-r04utHcLienT";
|
||||
// "REDIRECT_URL" "http://127.0.0.1:3000/auth/authorized";
|
||||
// "AUTH_URL" "https://discord.com/api/oauth2/authorize?response_type=code";
|
||||
// "TOKEN_URL" "https://discord.com/api/oauth2/token";
|
||||
|
||||
let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID!");
|
||||
let client_secret = env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET!");
|
||||
let redirect_url = env::var("REDIRECT_URL")
|
||||
.unwrap_or_else(|_| "http://127.0.0.1:3000/auth/authorized".to_string());
|
||||
|
||||
let auth_url = env::var("AUTH_URL").unwrap_or_else(|_| {
|
||||
"https://discord.com/api/oauth2/authorize?response_type=code".to_string()
|
||||
});
|
||||
|
||||
let token_url = env::var("TOKEN_URL")
|
||||
.unwrap_or_else(|_| "https://discord.com/api/oauth2/token".to_string());
|
||||
|
||||
BasicClient::new(
|
||||
ClientId::new(client_id),
|
||||
Some(ClientSecret::new(client_secret)),
|
||||
AuthUrl::new(auth_url).unwrap(),
|
||||
Some(TokenUrl::new(token_url).unwrap()),
|
||||
)
|
||||
.set_redirect_uri(RedirectUrl::new(redirect_url).unwrap())
|
||||
}
|
||||
|
||||
// The user data we'll get back from Discord.
|
||||
// https://discord.com/developers/docs/resources/user#user-object-user-structure
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct DiscordUser {
|
||||
id: String,
|
||||
avatar: Option<String>,
|
||||
username: String,
|
||||
discriminator: String,
|
||||
}
|
||||
|
||||
// Session is optional
|
||||
async fn index(user: Option<DiscordUser>) -> impl IntoResponse {
|
||||
match user {
|
||||
Some(u) => format!(
|
||||
"Hey {}! You're logged in!\nYou may now access `/protected`.\nLog out with `/logout`.",
|
||||
u.username
|
||||
),
|
||||
None => "You're not logged in.\nVisit `/auth/discord` to do so.".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn discord_auth(Extension(client): Extension<BasicClient>) -> impl IntoResponse {
|
||||
let (auth_url, _csrf_token) = client
|
||||
.authorize_url(CsrfToken::new_random)
|
||||
.add_scope(Scope::new("identify".to_string()))
|
||||
.url();
|
||||
|
||||
// Redirect to Discord's oauth service
|
||||
Redirect(auth_url.into())
|
||||
}
|
||||
|
||||
// Valid user session required. If there is none, redirect to the auth page
|
||||
async fn protected(user: DiscordUser) -> impl IntoResponse {
|
||||
format!(
|
||||
"Welcome to the protected area :)\nHere's your info:\n{:?}",
|
||||
user
|
||||
)
|
||||
}
|
||||
|
||||
async fn logout(
|
||||
Extension(store): Extension<RedisSessionStore>,
|
||||
TypedHeader(cookies): TypedHeader<headers::Cookie>,
|
||||
) -> impl IntoResponse {
|
||||
let cookie = cookies.get(COOKIE_NAME).unwrap();
|
||||
let session = match store.load_session(cookie.to_string()).await.unwrap() {
|
||||
Some(s) => s,
|
||||
// No session active, just redirect
|
||||
None => return Redirect("/".to_string()),
|
||||
};
|
||||
|
||||
store.destroy_session(session).await.unwrap();
|
||||
|
||||
Redirect("/".to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthRequest {
|
||||
code: String,
|
||||
state: String,
|
||||
}
|
||||
|
||||
async fn login_authorized(
|
||||
Query(query): Query<AuthRequest>,
|
||||
Extension(store): Extension<RedisSessionStore>,
|
||||
Extension(oauth_client): Extension<BasicClient>,
|
||||
) -> impl IntoResponse {
|
||||
// Get an auth token
|
||||
let token = oauth_client
|
||||
.exchange_code(AuthorizationCode::new(query.code.clone()))
|
||||
.request_async(async_http_client)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Fetch user data from discord
|
||||
let client = reqwest::Client::new();
|
||||
let user_data: DiscordUser = client
|
||||
// https://discord.com/developers/docs/resources/user#get-current-user
|
||||
.get("https://discordapp.com/api/users/@me")
|
||||
.bearer_auth(token.access_token().secret())
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<DiscordUser>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a new session filled with user data
|
||||
let mut session = Session::new();
|
||||
session.insert("user", &user_data).unwrap();
|
||||
|
||||
// Store session and get corresponding cookie
|
||||
let cookie = store.store_session(session).await.unwrap().unwrap();
|
||||
|
||||
// Build the cookie
|
||||
let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie);
|
||||
|
||||
// Set cookie
|
||||
let r = http::Response::builder()
|
||||
.header("Location", "/")
|
||||
.header(SET_COOKIE, cookie)
|
||||
.status(302);
|
||||
|
||||
r.body(Body::empty()).unwrap()
|
||||
}
|
||||
|
||||
// Utility to save some lines of code
|
||||
struct Redirect(String);
|
||||
impl IntoResponse for Redirect {
|
||||
fn into_response(self) -> http::Response<Body> {
|
||||
let builder = http::Response::builder()
|
||||
.header("Location", self.0)
|
||||
.status(StatusCode::FOUND);
|
||||
builder.body(Body::empty()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthRedirect;
|
||||
impl IntoResponse for AuthRedirect {
|
||||
fn into_response(self) -> http::Response<Body> {
|
||||
Redirect("/auth/discord".to_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B> FromRequest<B> for DiscordUser
|
||||
where
|
||||
B: Send,
|
||||
{
|
||||
// If anything goes wrong or no session is found, redirect to the auth page
|
||||
type Rejection = AuthRedirect;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
|
||||
let extract::Extension(store) = extract::Extension::<RedisSessionStore>::from_request(req)
|
||||
.await
|
||||
.expect("`RedisSessionStore` extension is missing");
|
||||
|
||||
let cookies: TypedHeader<headers::Cookie> =
|
||||
TypedHeader::<headers::Cookie>::from_request(req)
|
||||
.await
|
||||
.expect("could not get cookies");
|
||||
|
||||
let session_cookie = cookies.0.get(COOKIE_NAME).ok_or(AuthRedirect)?;
|
||||
|
||||
let session = store
|
||||
.load_session(session_cookie.to_string())
|
||||
.await
|
||||
.unwrap()
|
||||
.ok_or(AuthRedirect)?;
|
||||
|
||||
let user = session.get::<DiscordUser>("user").ok_or(AuthRedirect)?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_routes() -> BoxRoute<Body> {
|
||||
route("/", get(index))
|
||||
.route("/login/discord", get(discord_auth))
|
||||
.route("/authorized", get(login_authorized))
|
||||
.route("/protected", get(protected))
|
||||
.route("/logout", get(logout))
|
||||
.layer(AddExtensionLayer::new(oauth_client))
|
||||
.boxed()
|
||||
}
|
1
backend/src/endpoints/user.rs
Normal file
1
backend/src/endpoints/user.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
2
backend/src/helpers.rs
Normal file
2
backend/src/helpers.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod block;
|
||||
pub mod user;
|
56
backend/src/helpers/block.rs
Normal file
56
backend/src/helpers/block.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use crate::diesel::{prelude::*, PgConnection, QueryDsl};
|
||||
use crate::models::*;
|
||||
use crate::schema::*;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use std::error::Error;
|
||||
use tokio_diesel::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn create_block(
|
||||
pool: &Pool<ConnectionManager<PgConnection>>,
|
||||
block: InsertableBlock,
|
||||
) -> Result<Block, Box<dyn Error>> {
|
||||
let inserted: Block = diesel::insert_into(blocks::table)
|
||||
.values(block)
|
||||
.get_result_async(pool)
|
||||
.await?;
|
||||
Ok(inserted)
|
||||
}
|
||||
|
||||
pub async fn update_block(
|
||||
pool: &Pool<ConnectionManager<PgConnection>>,
|
||||
block: Block,
|
||||
) -> Result<Block, Box<dyn Error>> {
|
||||
use crate::schema::blocks::dsl::*;
|
||||
let result = diesel::update(blocks.filter(id.eq(block.id)))
|
||||
.set((
|
||||
block_type.eq(block.block_type),
|
||||
children.eq(block.children),
|
||||
props.eq(block.props),
|
||||
user_id.eq(block.user_id),
|
||||
))
|
||||
.get_result_async(pool)
|
||||
.await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn find_block_by_id(
|
||||
pool: &Pool<ConnectionManager<PgConnection>>,
|
||||
block_id: Uuid,
|
||||
) -> Result<Block, Box<dyn Error>> {
|
||||
use crate::schema::blocks::dsl::*;
|
||||
let result: Block = blocks.find(block_id).get_result_async(pool).await?;
|
||||
log::error!("{:?}", result);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn delete_block_by_id(
|
||||
pool: &Pool<ConnectionManager<PgConnection>>,
|
||||
block_id: Uuid,
|
||||
) -> Result<Block, Box<dyn Error>> {
|
||||
use crate::schema::blocks::dsl::*;
|
||||
let result: Block = diesel::delete(blocks.filter(id.eq(block_id)))
|
||||
.get_result_async(pool)
|
||||
.await?;
|
||||
Ok(result)
|
||||
}
|
51
backend/src/helpers/user.rs
Normal file
51
backend/src/helpers/user.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use crate::diesel::{prelude::*, PgConnection, QueryDsl};
|
||||
use crate::models::*;
|
||||
use crate::schema::*;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use std::error::Error;
|
||||
use tokio_diesel::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn create_user(
|
||||
pool: &Pool<ConnectionManager<PgConnection>>,
|
||||
user: InsertableUser,
|
||||
) -> Result<User, Box<dyn Error>> {
|
||||
let inserted: User = diesel::insert_into(users::table)
|
||||
.values(user)
|
||||
.get_result_async(pool)
|
||||
.await?;
|
||||
Ok(inserted)
|
||||
}
|
||||
|
||||
pub async fn update_user(
|
||||
pool: &Pool<ConnectionManager<PgConnection>>,
|
||||
user: User,
|
||||
) -> Result<User, Box<dyn Error>> {
|
||||
use crate::schema::users::dsl::*;
|
||||
let result = diesel::update(users.filter(id.eq(user.id)))
|
||||
.set((discord_id.eq(user.discord_id),))
|
||||
.get_result_async(pool)
|
||||
.await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn find_user_by_id(
|
||||
pool: &Pool<ConnectionManager<PgConnection>>,
|
||||
user_id: Uuid,
|
||||
) -> Result<User, Box<dyn Error>> {
|
||||
use crate::schema::users::dsl::*;
|
||||
let result = users.find(user_id).get_result_async::<User>(pool).await?;
|
||||
log::error!("{:?}", result);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn delete_user_by_id(
|
||||
pool: &Pool<ConnectionManager<PgConnection>>,
|
||||
user_id: Uuid,
|
||||
) -> Result<User, Box<dyn Error>> {
|
||||
use crate::schema::users::dsl::*;
|
||||
let result = diesel::delete(users.filter(id.eq(user_id)))
|
||||
.get_result_async(pool)
|
||||
.await?;
|
||||
Ok(result)
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
const http = require('http');
|
||||
const https = require('https');
|
||||
const httpProxy = require('http-proxy');
|
||||
const cors = require('cors');
|
||||
const express = require('express');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const fs = require('fs');
|
||||
const Config = require('./config.js');
|
||||
|
||||
const UserInterface = require('./user.js');
|
||||
const TodoInterface = require('./todo.js');
|
||||
|
||||
let credentials = {};
|
||||
|
||||
if (Config.config.https) {
|
||||
if (fs.existsSync(Config.config.cert) && fs.existsSync(Config.config.cert_key)) {
|
||||
credentials.key = fs.readFileSync(Config.config.cert_key);
|
||||
credentials.cert = fs.readFileSync(Config.config.cert);
|
||||
}
|
||||
else {
|
||||
console.error('could not load certs')
|
||||
process.exit()
|
||||
}
|
||||
}
|
||||
|
||||
let app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(cookieParser());
|
||||
|
||||
// force https
|
||||
app.use((req, res, next) => {
|
||||
if (Config.config.https) {
|
||||
if (req.headers['x-forwarded-proto'] !== 'https') {
|
||||
return res.redirect(`https://${req.headers.host}${req.url}`);
|
||||
}
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
if (!Config.config.secret) {
|
||||
console.error('No password secret found. please set `secret` in config.json');
|
||||
process.exit();
|
||||
} else if (Config.config.https && Config.config.secret == 'TEST_SECRET') {
|
||||
console.error('please do not use the testing secret in production.');
|
||||
process.exit();
|
||||
}
|
||||
|
||||
app.use('/api/user', UserInterface.router);
|
||||
app.use('/api/todo', TodoInterface.router);
|
||||
|
||||
if (Config.config.frontend_url) {
|
||||
const proxy = httpProxy.createProxyServer({})
|
||||
app.use('/', (req, res) => {
|
||||
return proxy.web(req, res, {
|
||||
target: Config.config.frontend_url
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (Config.config.https) {
|
||||
var server = https.createServer(credentials, app);
|
||||
server.listen(Config.config.port || 8080);
|
||||
} else {
|
||||
var server = http.createServer(app);
|
||||
server.listen(Config.config.port || 8080);
|
||||
}
|
||||
console.log(
|
||||
`listening on port ${Config.config.port || 8080}` + ` with https ${Config.config.https ? 'enabled' : 'disabled'}`
|
||||
);
|
26
backend/src/logging.rs
Normal file
26
backend/src/logging.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use std::fmt;
|
||||
use std::task::{Context, Poll};
|
||||
use tower::Service;
|
||||
|
||||
pub struct LogService<S> {
|
||||
pub service: S,
|
||||
}
|
||||
|
||||
impl<S, Request> Service<Request> for LogService<S>
|
||||
where
|
||||
S: Service<Request>,
|
||||
Request: fmt::Debug,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = S::Future;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.service.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
log::debug!("request = {:?}", request);
|
||||
self.service.call(request)
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
const Config = require('./config.js');
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
class Mailer {
|
||||
sender;
|
||||
started = false;
|
||||
mailer;
|
||||
constructor(host, port, email, password) {
|
||||
this.mailer = nodemailer.createTransport({
|
||||
host: host,
|
||||
port: port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: email,
|
||||
pass: password
|
||||
}
|
||||
});
|
||||
this.sender = email;
|
||||
this.started = true;
|
||||
}
|
||||
async sendMail(recipients, subject, content, contentStripped) {
|
||||
console.log(`sending mail to ${recipients}`);
|
||||
let info = await this.mailer.sendMail({
|
||||
from: `"Todo App" <${this.sender}>`,
|
||||
to: Array.isArray(recipients) ? recipients.join(', ') : recipients,
|
||||
subject: subject,
|
||||
text: contentStripped,
|
||||
html: content
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!global.mailer || !global.mailer.started) {
|
||||
if (
|
||||
!Config.config['mail_host'] ||
|
||||
!Config.config['mail_port'] ||
|
||||
!Config.config['mail_username'] ||
|
||||
!Config.config['mail_password']
|
||||
) {
|
||||
console.error(`could not create email account as
|
||||
mail_host, mail_port, mail_username or mail_password is not set.`);
|
||||
process.exit();
|
||||
}
|
||||
global.mailer = new Mailer(
|
||||
Config.config['mail_host'],
|
||||
Config.config['mail_port'],
|
||||
Config.config['mail_username'],
|
||||
Config.config['mail_password']
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = global.mailer;
|
66
backend/src/main.rs
Normal file
66
backend/src/main.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use axum::prelude::*;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use diesel::{
|
||||
prelude::*,
|
||||
r2d2::{ConnectionManager, Pool},
|
||||
};
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
extern crate redis;
|
||||
|
||||
mod endpoints;
|
||||
pub mod helpers;
|
||||
pub mod logging;
|
||||
pub mod migration;
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
pub mod tests;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
let _ = setup_logger();
|
||||
|
||||
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set");
|
||||
|
||||
migration::run_migrations(&db_url);
|
||||
let manager = ConnectionManager::<PgConnection>::new(&db_url);
|
||||
let pool = Pool::builder()
|
||||
.build(manager)
|
||||
.expect("Could not build connection pool");
|
||||
|
||||
let root = route("/api", endpoints::get_routes(pool));
|
||||
|
||||
let port = env::var("PORT").unwrap_or(String::from("8000"));
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port.parse().unwrap_or(8000)));
|
||||
|
||||
log::info!("started listening on {:?}", addr);
|
||||
hyper::Server::bind(&addr)
|
||||
.serve(root.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn setup_logger() -> Result<(), fern::InitError> {
|
||||
let log_level = env::var("LOG_LEVEL").unwrap_or(String::from("INFO"));
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{} <{}> [{}] {}",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
record.file().unwrap_or(record.target()),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(log::LevelFilter::from_str(log_level.as_str()).unwrap_or(log::LevelFilter::Info))
|
||||
.chain(std::io::stdout())
|
||||
.chain(fern::log_file("latest.log")?)
|
||||
.apply()?;
|
||||
Ok(())
|
||||
}
|
22
backend/src/migration.rs
Normal file
22
backend/src/migration.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use diesel::{prelude::*, sql_query};
|
||||
use diesel_migrations::*;
|
||||
|
||||
pub fn reset_database(url: &String) {
|
||||
let conn = PgConnection::establish(&url).expect(&format!("Error connecting to {}", url));
|
||||
println!("dropping all tables");
|
||||
let _ = sql_query("drop table users;").execute(&conn);
|
||||
let _ = sql_query("drop table unverified_users;").execute(&conn);
|
||||
let _ = sql_query("drop table blocks;").execute(&conn);
|
||||
let _ = sql_query("drop table __diesel_schema_migrations;").execute(&conn);
|
||||
println!("finished resetting db");
|
||||
}
|
||||
|
||||
pub fn run_migrations(url: &String) {
|
||||
println!("running migrations");
|
||||
let conn = PgConnection::establish(&url).expect(&format!("Error connecting to {}", url));
|
||||
let result = run_pending_migrations(&conn);
|
||||
if result.is_err() {
|
||||
panic!("could not run migrations: {}", result.err().unwrap());
|
||||
}
|
||||
println!("finished migrations");
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
const Sequelize = require('sequelize');
|
||||
const Config = require('./config.js');
|
||||
|
||||
const models = (db) => {
|
||||
const UnverifiedUser = db.define('UnverifiedUser', {
|
||||
id: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
defaultValue: Sequelize.UUIDV4,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
unique: true
|
||||
},
|
||||
verificationToken: {
|
||||
type: Sequelize.DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
discord_only_account: {
|
||||
type: Sequelize.DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
discord_id: {
|
||||
type: Sequelize.DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
password_hash: {
|
||||
type: Sequelize.DataTypes.STRING,
|
||||
allowNull: true
|
||||
}
|
||||
});
|
||||
|
||||
const User = db.define('User', {
|
||||
id: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
defaultValue: Sequelize.UUIDV4,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
unique: true
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
discord_only_account: {
|
||||
type: Sequelize.DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
discord_id: {
|
||||
type: Sequelize.DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
password_hash: {
|
||||
type: Sequelize.DataTypes.STRING,
|
||||
allowNull: true
|
||||
}
|
||||
});
|
||||
|
||||
const Todo = db.define('Todo', {
|
||||
id: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
defaultValue: Sequelize.UUIDV4,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
unique: true
|
||||
},
|
||||
user: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: false
|
||||
},
|
||||
content: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
tags: {
|
||||
type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING),
|
||||
allowNull: true
|
||||
},
|
||||
complete: {
|
||||
type: Sequelize.DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
deadline: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: true
|
||||
}
|
||||
});
|
||||
|
||||
const Grouping = db.define('Grouping', {
|
||||
id: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
defaultValue: Sequelize.UUIDV4,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
unique: true
|
||||
},
|
||||
complete: {
|
||||
type: Sequelize.DataTypes.BOOLEAN,
|
||||
allowNull: true
|
||||
},
|
||||
manually_added: {
|
||||
type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.UUID),
|
||||
allowNull: true
|
||||
},
|
||||
required: {
|
||||
type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING),
|
||||
allowNull: true
|
||||
},
|
||||
exclusions: {
|
||||
type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING),
|
||||
allowNull: true
|
||||
}
|
||||
});
|
||||
|
||||
let options = {
|
||||
alter: false
|
||||
};
|
||||
if (Config.config.alter_db) {
|
||||
options.alter = true;
|
||||
}
|
||||
UnverifiedUser.sync(options);
|
||||
User.sync(options);
|
||||
Todo.sync(options);
|
||||
Grouping.sync(options);
|
||||
|
||||
return {
|
||||
user: User,
|
||||
unverifiedUser: UnverifiedUser,
|
||||
todo: Todo,
|
||||
grouping: Grouping
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = models;
|
38
backend/src/models.rs
Normal file
38
backend/src/models.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use super::schema::*;
|
||||
use chrono::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Queryable, Deserialize, Serialize, Debug, Clone, PartialEq)]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub discord_id: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Insertable, Debug, Clone, PartialEq)]
|
||||
#[table_name = "users"]
|
||||
pub struct InsertableUser {
|
||||
pub discord_id: String,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct Block {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub block_type: String,
|
||||
pub props: serde_json::Value,
|
||||
pub children: Option<Vec<String>>,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Insertable, Debug, Clone, PartialEq)]
|
||||
#[table_name = "blocks"]
|
||||
pub struct InsertableBlock {
|
||||
pub user_id: Uuid,
|
||||
pub block_type: String,
|
||||
pub props: serde_json::Value,
|
||||
pub children: Option<Vec<String>>,
|
||||
}
|
47
backend/src/schema.rs
Normal file
47
backend/src/schema.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
table! {
|
||||
blocks (id) {
|
||||
id -> Uuid,
|
||||
user_id -> Uuid,
|
||||
block_type -> Varchar,
|
||||
props -> Json,
|
||||
children -> Nullable<Array<Text>>,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
migration_versions (id) {
|
||||
id -> Int4,
|
||||
version -> Varchar,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
unverifiedusers (id) {
|
||||
id -> Varchar,
|
||||
email -> Nullable<Varchar>,
|
||||
discord_only_account -> Nullable<Bool>,
|
||||
discord_id -> Nullable<Varchar>,
|
||||
password_hash -> Nullable<Varchar>,
|
||||
verification_token -> Nullable<Varchar>,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
users (id) {
|
||||
id -> Uuid,
|
||||
discord_id -> Varchar,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
allow_tables_to_appear_in_same_query!(
|
||||
blocks,
|
||||
migration_versions,
|
||||
unverifiedusers,
|
||||
users,
|
||||
);
|
18
backend/src/tests.rs
Normal file
18
backend/src/tests.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
pub mod db;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// a basic test to ensure that tests are executing.
|
||||
#[test]
|
||||
fn works() {
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn db_tests() {
|
||||
let url = String::from("postgres://postgres@localhost/todo_test");
|
||||
crate::migration::reset_database(&url);
|
||||
crate::migration::run_migrations(&url);
|
||||
super::db::run_tests(&url).await;
|
||||
}
|
||||
}
|
132
backend/src/tests/db.rs
Normal file
132
backend/src/tests/db.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
use crate::helpers::*;
|
||||
use crate::models::*;
|
||||
use diesel::{
|
||||
r2d2::{ConnectionManager, Pool},
|
||||
PgConnection,
|
||||
};
|
||||
use std::{error::Error, vec::Vec};
|
||||
use uuid::Uuid;
|
||||
|
||||
async fn get_pool(url: &String) -> Pool<ConnectionManager<PgConnection>> {
|
||||
let manager = ConnectionManager::<PgConnection>::new(url);
|
||||
Pool::builder()
|
||||
.build(manager)
|
||||
.expect("Could not build connection pool")
|
||||
}
|
||||
|
||||
async fn user_tests(pool: &Pool<ConnectionManager<PgConnection>>) {
|
||||
let user = InsertableUser {
|
||||
discord_id: String::from("test"),
|
||||
};
|
||||
let created_result = user::create_user(pool, user).await;
|
||||
if created_result.is_err() {
|
||||
panic!("could not create user: {:?}", created_result.err());
|
||||
} else {
|
||||
assert!(created_result.is_ok());
|
||||
}
|
||||
|
||||
let created_user: User = created_result.unwrap();
|
||||
|
||||
let mut new_user = created_user.clone();
|
||||
new_user.discord_id = String::from("test2");
|
||||
let user_update = new_user.clone();
|
||||
|
||||
let updated_result: Result<User, Box<dyn Error>> = user::update_user(pool, user_update).await;
|
||||
|
||||
if updated_result.is_err() {
|
||||
panic!(
|
||||
"cound not update user {}: {:?}",
|
||||
created_user.id,
|
||||
updated_result.err()
|
||||
);
|
||||
}
|
||||
|
||||
let updated_user = updated_result.unwrap();
|
||||
assert_eq!(new_user.id, updated_user.id);
|
||||
assert_eq!(new_user.discord_id, updated_user.discord_id);
|
||||
|
||||
let get_result = user::find_user_by_id(pool, created_user.id).await;
|
||||
if get_result.is_err() {
|
||||
panic!(
|
||||
"could not find previously created user {}: {:?}",
|
||||
created_user.id,
|
||||
get_result.err()
|
||||
);
|
||||
} else {
|
||||
assert_eq!(updated_user, get_result.unwrap());
|
||||
}
|
||||
|
||||
let delete_result = user::delete_user_by_id(pool, created_user.id).await;
|
||||
if delete_result.is_err() {
|
||||
panic!(
|
||||
"could not delete user {}: {:?}",
|
||||
created_user.id,
|
||||
delete_result.err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn block_tests(pool: &Pool<ConnectionManager<PgConnection>>) {
|
||||
let json = serde_json::from_str("[]");
|
||||
let block = InsertableBlock {
|
||||
user_id: Uuid::new_v4(),
|
||||
block_type: String::from("test"),
|
||||
children: Some(Vec::new()),
|
||||
props: json.unwrap(),
|
||||
};
|
||||
let created_result = block::create_block(pool, block).await;
|
||||
if created_result.is_err() {
|
||||
panic!("could not create block: {:?}", created_result.err());
|
||||
} else {
|
||||
assert!(created_result.is_ok());
|
||||
}
|
||||
|
||||
let created_block: Block = created_result.unwrap();
|
||||
|
||||
let mut new_block = created_block.clone();
|
||||
new_block.block_type = String::from("test2");
|
||||
let block_update = new_block.clone();
|
||||
|
||||
let updated_result: Result<Block, Box<dyn Error>> =
|
||||
block::update_block(pool, block_update).await;
|
||||
|
||||
if updated_result.is_err() {
|
||||
panic!(
|
||||
"cound not update block {}: {:?}",
|
||||
created_block.id,
|
||||
updated_result.err()
|
||||
);
|
||||
}
|
||||
let updated_block = updated_result.unwrap();
|
||||
assert_eq!(new_block.id, updated_block.id);
|
||||
assert_eq!(new_block.block_type, updated_block.block_type);
|
||||
assert_eq!(new_block.user_id, updated_block.user_id);
|
||||
assert_eq!(new_block.children, updated_block.children);
|
||||
assert_eq!(new_block.props, updated_block.props);
|
||||
|
||||
let get_result = block::find_block_by_id(pool, created_block.id).await;
|
||||
if get_result.is_err() {
|
||||
panic!(
|
||||
"could not find previously created block {}: {:?}",
|
||||
created_block.id,
|
||||
get_result.err()
|
||||
);
|
||||
} else {
|
||||
assert_eq!(updated_block, get_result.unwrap());
|
||||
}
|
||||
|
||||
let delete_result = block::delete_block_by_id(pool, created_block.id).await;
|
||||
if delete_result.is_err() {
|
||||
panic!(
|
||||
"could not delete block {}: {:?}",
|
||||
created_block.id,
|
||||
delete_result.err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_tests(url: &String) {
|
||||
let pool = get_pool(url).await;
|
||||
user_tests(&pool).await;
|
||||
block_tests(&pool).await;
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
const express = require('express');
|
||||
const paginate = require('express-paginate');
|
||||
const Database = require('./db_interface.js');
|
||||
const User = require('./user.js');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
let router = express.Router();
|
||||
|
||||
router.use(express.json());
|
||||
|
||||
function map_todo(result) {
|
||||
return {
|
||||
id: result.id,
|
||||
content: result.content,
|
||||
tags: result.tags
|
||||
};
|
||||
}
|
||||
|
||||
function parse_tags(tags) {
|
||||
result = {
|
||||
complete: undefined,
|
||||
required: [],
|
||||
excluded: []
|
||||
};
|
||||
tags.map((tag) => {
|
||||
if (tag === 'complete') {
|
||||
complete = true;
|
||||
} else if (tag === '~complete') {
|
||||
complete = false;
|
||||
} else if (tag.startsWith('~')) {
|
||||
excluded.push(tag);
|
||||
} else {
|
||||
required.push(tag);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
const todo_fields = ['currentPage', 'limit'];
|
||||
|
||||
router.use(paginate.middleware(10, 50));
|
||||
router.use('/todos', User.enforce_session_login);
|
||||
router.get('/todos', async (req, res) => {
|
||||
if (!req.query) {
|
||||
return res.status(400).json({
|
||||
error: `query must include the fields: ${todo_fields.join(', ')}}`
|
||||
});
|
||||
} else {
|
||||
let error = [];
|
||||
for (let field of todo_fields) {
|
||||
if (!req.query[field]) {
|
||||
error.push(field);
|
||||
}
|
||||
}
|
||||
if (error.length > 0) {
|
||||
return res.status(400).json({
|
||||
error: `query must include the fields: ${error.join(', ')}}`
|
||||
});
|
||||
}
|
||||
}
|
||||
let tag_options = {};
|
||||
if (req.query.tags) {
|
||||
let parsed = parse_tags(req.query.tags.split(','));
|
||||
tag_options['tags'] = {
|
||||
[Op.and]: parsed.required,
|
||||
[Op.not]: parsed.excluded
|
||||
};
|
||||
if (parsed.complete !== undefined) {
|
||||
tag_options['complete'] = {
|
||||
[Op.is]: parsed.complete
|
||||
};
|
||||
}
|
||||
}
|
||||
console.log(tag_options);
|
||||
let all_todos = await Database.schemas.todo.findAndCountAll({
|
||||
where: {
|
||||
user: req.get('id'),
|
||||
...tag_options
|
||||
},
|
||||
limit: req.query.limit,
|
||||
offset: req.skip
|
||||
});
|
||||
const item_count = all_todos.count;
|
||||
const page_count = Math.ceil(item_count / req.query.limit);
|
||||
res.json({
|
||||
result: all_todos.map(map_todo),
|
||||
currentPage: req.query.currentPage,
|
||||
pageCount: page_count,
|
||||
itemCount: item_count,
|
||||
pages: paginate.getArrayPages(req)(5, page_count, req.query.currentPage)
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/todo', User.enforce_session_login);
|
||||
router.get('/todo/:id([a-f0-9-]+)', async (req, res) => {
|
||||
let userid = req.get('id');
|
||||
let id = req.params?.id;
|
||||
|
||||
let match = await Database.schemas.todo.findOne({
|
||||
where: {
|
||||
user: userid,
|
||||
id: id
|
||||
}
|
||||
});
|
||||
if (!match) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
result: map_todo(match),
|
||||
tags: get_tags(match.id)
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/todo', User.enforce_session_login);
|
||||
router.post('/todo/:id([a-f0-9-]+)', async (req, res) => {
|
||||
let userid = req.get('id');
|
||||
let id = req.params?.id;
|
||||
|
||||
let body = req.body;
|
||||
|
||||
if (!body) {
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
let match = await Database.schemas.todo.findOne({
|
||||
where: {
|
||||
user: userid,
|
||||
id: id
|
||||
}
|
||||
});
|
||||
if (!match) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
return res.json({
|
||||
result: map_todo(match),
|
||||
tags: get_tags(match.id)
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
router: router
|
||||
};
|
|
@ -1,424 +0,0 @@
|
|||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const fetch = require('node-fetch');
|
||||
const Config = require('./config.js');
|
||||
const Database = require('./db_interface.js');
|
||||
const Mail = require('./mail.js');
|
||||
|
||||
let router = express.Router();
|
||||
|
||||
router.use(express.json());
|
||||
|
||||
let session_entropy = {};
|
||||
|
||||
user_cache = {};
|
||||
email_cache = {};
|
||||
discord_cache = {};
|
||||
discord_user_cache = {};
|
||||
|
||||
async function fetch_user(where) {
|
||||
let user = await Database.schemas.user.findOne({ where: where });
|
||||
if (user === null) {
|
||||
return undefined;
|
||||
}
|
||||
user_cache[user.id] = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
discord_id: user.discord_id,
|
||||
password_hash: user.password_hash
|
||||
};
|
||||
email_cache[user.email] = user.id;
|
||||
if (user.discord_id) {
|
||||
discord_cache[user.discord_id] = user.id
|
||||
}
|
||||
return user_cache[user.id]
|
||||
}
|
||||
|
||||
async function fetch_discord_user(auth) {
|
||||
const result = await fetch(`https://discord.com/api/v8/users/@me`, {
|
||||
headers: {
|
||||
'Authorization': auth.token_type + ' ' + auth.access_token
|
||||
}
|
||||
});
|
||||
const json = result.json();
|
||||
discord_user_cache[json.id] = {
|
||||
user: json,
|
||||
auth: auth,
|
||||
expires: (new Date().getTime()) + (json.expires_in * 1000)
|
||||
}
|
||||
return discord_user_cache[id];
|
||||
}
|
||||
|
||||
async function acquire_discord_token(code, redirect) {
|
||||
let data = {
|
||||
client_id: Config.config.discord_id,
|
||||
client_secret: Config.config.discord_secret,
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
redirect_uri: redirect
|
||||
}
|
||||
const result = await fetch(`https://discord.com/api/oauth2/token`, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}).catch(err => console.error(err));
|
||||
if (!result.ok) {
|
||||
return res.status(500).json({error: "could not fetch user details"})
|
||||
}
|
||||
const json = result.json();
|
||||
return fetch_discord_user(json);
|
||||
}
|
||||
|
||||
async function refresh_discord_token(id) {
|
||||
let data = {
|
||||
client_id: Config.config.discord_id,
|
||||
client_secret: Config.config.discord_secret,
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: discord_user_cache[id].auth.refresh_token
|
||||
}
|
||||
const result = await fetch(`https://discord.com/api/oauth2/token`, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}).catch(err => console.error(err));
|
||||
if (!result.ok) {
|
||||
return false;
|
||||
}
|
||||
const json = result.json();
|
||||
discord_user_cache[id].auth.access_token = json.access_token;
|
||||
discord_user_cache[id].expires = (new Date().getTime()) + (json.expires_in * 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function get_user_details(id) {
|
||||
if (!id || id === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
console.log(`search for user with id ${id}`);
|
||||
if (!user_cache[id]) {
|
||||
return await fetch_user({ id: id })
|
||||
}
|
||||
// console.log(`returning ${JSON.stringify(user_cache[id])}`);
|
||||
return user_cache[id];
|
||||
}
|
||||
|
||||
async function get_user_details_by_email(email) {
|
||||
if (!email || email === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
console.log(`search for user with email ${email}}`);
|
||||
if (!email_cache[email] || !user_cache[email_cache[email]]) {
|
||||
return await fetch_user({ email: email })
|
||||
}
|
||||
// console.log(`returning ${JSON.stringify(user_cache[email_cache[email]])}`);
|
||||
return user_cache[email_cache[email]];
|
||||
}
|
||||
|
||||
async function get_user_details_by_discord_id(id) {
|
||||
if (!id || id === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
if (!discord_cache[id] || !user_cache[discord_cache[id]]) {
|
||||
return await fetch_user({ discord_id: id })
|
||||
}
|
||||
return user_cache[discord_cache[id]];
|
||||
}
|
||||
|
||||
function hash(secret, password, base64 = true) {
|
||||
let pw_hash = crypto.pbkdf2Sync(
|
||||
password,
|
||||
secret,
|
||||
Config.config.key?.iterations || 1000,
|
||||
Config.config.key?.length || 64,
|
||||
'sha512'
|
||||
);
|
||||
|
||||
return pw_hash.toString(base64 ? 'base64' : 'hex');
|
||||
}
|
||||
|
||||
function verify(secret, password, hash) {
|
||||
let pw_hash = crypto.pbkdf2Sync(
|
||||
password,
|
||||
secret,
|
||||
Config.config.key?.iterations || 1000,
|
||||
Config.config.key?.length || 64,
|
||||
'sha512'
|
||||
);
|
||||
|
||||
return hash === pw_hash.toString('base64');
|
||||
}
|
||||
|
||||
function hash_password(password) {
|
||||
return hash(Config.config.secret, password);
|
||||
}
|
||||
|
||||
function verify_password(password, hash) {
|
||||
return verify(Config.config.secret, password, hash);
|
||||
}
|
||||
|
||||
function get_session_token(id, password_hash, base64 = true) {
|
||||
session_entropy[id] = crypto.randomBytes(Config.config.session_entropy || 32);
|
||||
return hash(session_entropy[id], password_hash, base64);
|
||||
}
|
||||
|
||||
function verify_session_token(id, hash, token) {
|
||||
if (session_entropy[id]) {
|
||||
return verify(session_entropy[id], hash, token);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function enforce_session_login(req, res, next) {
|
||||
let userid = req.get('id');
|
||||
let session_token = req.get('authorization');
|
||||
console.log('a', userid, session_token);
|
||||
if (!userid || !session_token) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
let user = await get_user_details(userid);
|
||||
if (!user) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
let verified_session = verify_session_token(userid, user.password_hash, session_token);
|
||||
if (!verified_session) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
router.post('/signup', async (req, res) => {
|
||||
if (!req.body?.email || !req.body?.password) {
|
||||
return res.status(400).json({
|
||||
error: 'must have email and password fields'
|
||||
});
|
||||
}
|
||||
let user = await get_user_details_by_email(req.body?.email);
|
||||
|
||||
if (user !== undefined && user !== {}) {
|
||||
console.warn(`user already found: ${JSON.stringify(user)}`);
|
||||
return res.status(403).json({
|
||||
error: `email ${req.body.email} is already in use.`
|
||||
});
|
||||
} else {
|
||||
let match = await Database.schemas.unverifiedUser.findOne({ where: { email: req.body.email } });
|
||||
if (!!match) {
|
||||
await Database.schemas.unverifiedUser.destroy({ where: { email: match.email } });
|
||||
}
|
||||
let randomString = 'Signup';
|
||||
for (let i = 0; i < 16; i++) {
|
||||
randomString += Math.floor(Math.random() * 10);
|
||||
}
|
||||
let password_hash = hash_password(req.body.password);
|
||||
let user = await Database.schemas.unverifiedUser.create({
|
||||
email: String(req.body.email),
|
||||
password_hash: password_hash,
|
||||
verificationToken: get_session_token(randomString, password_hash, false)
|
||||
});
|
||||
const link = `${Config.config.https ? 'https://' : 'http://'}${req.headers.host}/api/user/verify?verification=${user.verificationToken
|
||||
}`;
|
||||
const content = `Click here to verify your sign-up:
|
||||
${link}`;
|
||||
const contentHtml = `<h1>Click here to verify your sign-up:</h1>
|
||||
<p><a href=${link}>${link}</a></p>`;
|
||||
await Mail.sendMail([String(req.body.email)], 'Verify Your Account', contentHtml, content);
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/verify', async (req, res) => {
|
||||
if (!req.query?.verification) {
|
||||
return res.status(400).send(
|
||||
`<html>
|
||||
<body>
|
||||
<h1>No Verification Link</h1>
|
||||
</body>
|
||||
</html>`
|
||||
);
|
||||
}
|
||||
let verification = req.query?.verification;
|
||||
let user = await Database.schemas.unverifiedUser.findOne({ where: { verificationToken: verification } });
|
||||
|
||||
if (user !== undefined && user !== {}) {
|
||||
if (user.verificationToken != verification) {
|
||||
return res.status(404).send(
|
||||
`<html>
|
||||
<body>
|
||||
<h1>Unknown Verification Link</h1>
|
||||
</body>
|
||||
</html>`
|
||||
);
|
||||
}
|
||||
let newUser = await Database.schemas.user.create({
|
||||
email: user.email,
|
||||
password_hash: user.password_hash
|
||||
});
|
||||
|
||||
return res.send(`<html>
|
||||
<body>
|
||||
<h1>Sign up complete.</h1>
|
||||
</body>
|
||||
</html>`);
|
||||
} else {
|
||||
return res.status(404).send(`<html>
|
||||
<body>
|
||||
<h1>Unknown Verification Link</h1>
|
||||
</body>
|
||||
</html>`);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/login/discord', async (req, res) => {
|
||||
if (!Config.config.discord_id || !Config.config.discord_secret) {
|
||||
return res.status(403).send("discord login is not enabled.");
|
||||
}
|
||||
const url = encodeURIComponent(`${req.headers.host}discord`);
|
||||
return res.send(`https://discord.com/api/oauth2/authorize?client_id=${Config.config.discord_id}&redirect_uri=${url}&response_type=code&scope=identify%20email%20guilds`);
|
||||
});
|
||||
|
||||
router.post('/login/discord', async (req, res) => {
|
||||
if (!Config.config.discord_id || !Config.config.discord_secret) {
|
||||
return res.status(403).json({ error: "discord login is not enabled." });
|
||||
}
|
||||
if (!req.params.code || !req.headers.host) {
|
||||
return res.status(400).json({error: "invalid oauth request"});
|
||||
}
|
||||
const result = await acquire_discord_token(req.params.code, req.headers.host);
|
||||
const matching_account = await get_user_details_by_discord_id(result.user.id);
|
||||
if (!matching_account) {
|
||||
let user = await Database.schemas.unverifiedUser.create({
|
||||
email: String(result.user.email),
|
||||
discord_id: user.id,
|
||||
verificationToken: get_session_token(randomString, result.auth.access_token, false)
|
||||
});
|
||||
return res.json({
|
||||
type: 'unverified',
|
||||
verificationToken: user.verificationToken
|
||||
})
|
||||
}
|
||||
return res.json({
|
||||
type: 'verified',
|
||||
userid: matching_account.id,
|
||||
session_token: get_session_token(matching_account.id, result.auth.access_token)
|
||||
});
|
||||
});
|
||||
|
||||
//TODO
|
||||
router.post('/discord/create', async (req, res) =>{});
|
||||
router.post('/discord/link', async (req, res) =>{});
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
if (!req.body?.email || !req.body?.password) {
|
||||
return res.status(400).json({
|
||||
error: 'must have email and password fields'
|
||||
});
|
||||
}
|
||||
let user = await get_user_details_by_email(req.body.email);
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
error: 'incorrect email or password'
|
||||
});
|
||||
}
|
||||
let verified = verify_password(req.body.password, user.password_hash);
|
||||
|
||||
if (!verified) {
|
||||
return res.status(401).json({
|
||||
error: 'incorrect email or password'
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
userid: user.id,
|
||||
session_token: get_session_token(user.id, user.password_hash)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.use('/logout', enforce_session_login)
|
||||
router.post('/logout', async (req, res) => {
|
||||
let userid = req.get('id');
|
||||
let session_token = req.get('authorization');
|
||||
|
||||
let user = await get_user_details(userid);
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
error: 'invalid user data'
|
||||
});
|
||||
}
|
||||
let verified = verify_session_token(user.id, user.password_hash, session_token);
|
||||
|
||||
if (!verified) {
|
||||
return res.status(401).json({
|
||||
error: 'invalid user data'
|
||||
});
|
||||
}
|
||||
|
||||
delete session_entropy[user.id];
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.use('/byEmail', enforce_session_login)
|
||||
router.get('/byEmail/:email', async (req, res) => {
|
||||
if (!req.params?.email) {
|
||||
res.status(400).json({
|
||||
error: 'email is a required parameter'
|
||||
});
|
||||
}
|
||||
let user = get_user_details_by_email(req.params.email);
|
||||
console.log(user);
|
||||
if (user !== undefined && user !== {}) {
|
||||
res.json({
|
||||
id: user.id,
|
||||
email: user.email
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.use('/', enforce_session_login)
|
||||
router.get('/:id([a-f0-9-]+)', async (req, res) => {
|
||||
console.log(req.params);
|
||||
if (!req.params?.id) {
|
||||
return res.status(400).json({
|
||||
error: 'must have id parameter'
|
||||
});
|
||||
}
|
||||
let id = req.params?.id;
|
||||
console.log(id);
|
||||
let user = await get_user_details(id);
|
||||
console.log(user);
|
||||
if (user !== undefined && user !== {}) {
|
||||
return res.json({
|
||||
id: user.id,
|
||||
email: user.email
|
||||
});
|
||||
} else {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
router.use('/authorized', enforce_session_login);
|
||||
router.get('/authorized', async (req, res) => {
|
||||
let userid = req.get('id');
|
||||
let user = await get_user_details(userid);
|
||||
return res.json({
|
||||
authorized: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
router: router,
|
||||
enforce_session_login: enforce_session_login,
|
||||
get_user_details: get_user_details,
|
||||
get_user_details_by_email: get_user_details_by_email
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
const { expect } = require('chai');
|
||||
const Models = require('../src/models');
|
||||
|
||||
const { sequelize, checkModelName, checkUniqueIndex, checkPropertyExists } = require('sequelize-test-helpers');
|
||||
|
||||
describe('Sequelize model tests', function () {
|
||||
const models = Models(sequelize);
|
||||
|
||||
checkModelName(models.user)('User');
|
||||
checkModelName(models.unverifiedUser)('UnverifiedUser');
|
||||
checkModelName(models.grouping)('Grouping');
|
||||
checkModelName(models.todo)('Todo');
|
||||
|
||||
context('user props', function () {
|
||||
['id', 'email', 'discord_only_account'].forEach(checkPropertyExists(new models.user()));
|
||||
});
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
const { expect } = require('chai');
|
||||
const proxyrequire = require('proxyrequire');
|
||||
const { match, stub, resetHistory } = require('sinon');
|
||||
const { sequelize, Sequelize, makeMockModels } = require('sequelize-test-helpers');
|
||||
|
||||
describe('User Router Tests', function () {
|
||||
const Database = proxyrequire('../src/db_interface', {
|
||||
sequelize: Sequelize
|
||||
});
|
||||
});
|
23
frontend/.gitignore
vendored
23
frontend/.gitignore
vendored
|
@ -1,23 +0,0 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
|
@ -1,70 +1,105 @@
|
|||
# Getting Started with Create React App
|
||||
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
---
|
||||
|
||||
## Available Scripts
|
||||
# svelte app
|
||||
|
||||
In the project directory, you can run:
|
||||
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
|
||||
|
||||
### `yarn start`
|
||||
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
```bash
|
||||
npx degit sveltejs/template svelte-app
|
||||
cd svelte-app
|
||||
```
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
## Get started
|
||||
|
||||
### `yarn build`
|
||||
Install the dependencies...
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
```bash
|
||||
cd svelte-app
|
||||
npm install
|
||||
```
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
...then start [Rollup](https://rollupjs.org):
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### `yarn eject`
|
||||
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
## Building and running in production mode
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
To create an optimised version of the app:
|
||||
|
||||
## Learn More
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
## Single-page app mode
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
```js
|
||||
"start": "sirv public --single"
|
||||
```
|
||||
|
||||
### Making a Progressive Web App
|
||||
## Using TypeScript
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
|
||||
|
||||
### Advanced Configuration
|
||||
```bash
|
||||
node scripts/setupTypeScript.js
|
||||
```
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
Or remove the script via:
|
||||
|
||||
### Deployment
|
||||
```bash
|
||||
rm scripts/setupTypeScript.js
|
||||
```
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
## Deploying to the web
|
||||
|
||||
### `yarn build` fails to minify
|
||||
### With [Vercel](https://vercel.com)
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
Install `vercel` if you haven't already:
|
||||
|
||||
```bash
|
||||
npm install -g vercel
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
cd public
|
||||
vercel deploy --name my-project
|
||||
```
|
||||
|
||||
### With [surge](https://surge.sh/)
|
||||
|
||||
Install `surge` if you haven't already:
|
||||
|
||||
```bash
|
||||
npm install -g surge
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
surge public my-project.surge.sh
|
||||
```
|
||||
|
|
|
@ -1,50 +1,23 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"name": "svelte-app",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.4.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@material-ui/core": "^5.0.0-beta.0",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/styles": "^4.11.4",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"axios": "^0.21.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"start": "sirv public --no-clear --host --port 3000"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||
"rollup": "^2.3.4",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^7.0.0",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"svelte": "^3.0.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"dependencies": {
|
||||
"sirv-cli": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
1
frontend/public/build/bundle.css
Normal file
1
frontend/public/build/bundle.css
Normal file
|
@ -0,0 +1 @@
|
|||
.svelte-lxvsk7{font-family:inherit;font-size:inherit}input.svelte-lxvsk7{display:block;margin:0 0 0.5em 0}select.svelte-lxvsk7{float:left;margin:0 1em 1em 0;width:14em}.buttons.svelte-lxvsk7{clear:both}
|
801
frontend/public/build/bundle.js
Normal file
801
frontend/public/build/bundle.js
Normal file
|
@ -0,0 +1,801 @@
|
|||
|
||||
(function(l, r) { if (!l || l.getElementById('livereloadscript')) return; r = l.createElement('script'); r.async = 1; r.src = '//' + (self.location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1'; r.id = 'livereloadscript'; l.getElementsByTagName('head')[0].appendChild(r) })(self.document);
|
||||
var app = (function () {
|
||||
'use strict';
|
||||
|
||||
function noop() { }
|
||||
function add_location(element, file, line, column, char) {
|
||||
element.__svelte_meta = {
|
||||
loc: { file, line, column, char }
|
||||
};
|
||||
}
|
||||
function run(fn) {
|
||||
return fn();
|
||||
}
|
||||
function blank_object() {
|
||||
return Object.create(null);
|
||||
}
|
||||
function run_all(fns) {
|
||||
fns.forEach(run);
|
||||
}
|
||||
function is_function(thing) {
|
||||
return typeof thing === 'function';
|
||||
}
|
||||
function safe_not_equal(a, b) {
|
||||
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
|
||||
}
|
||||
function is_empty(obj) {
|
||||
return Object.keys(obj).length === 0;
|
||||
}
|
||||
function append(target, node) {
|
||||
target.appendChild(node);
|
||||
}
|
||||
function insert(target, node, anchor) {
|
||||
target.insertBefore(node, anchor || null);
|
||||
}
|
||||
function detach(node) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
function destroy_each(iterations, detaching) {
|
||||
for (let i = 0; i < iterations.length; i += 1) {
|
||||
if (iterations[i])
|
||||
iterations[i].d(detaching);
|
||||
}
|
||||
}
|
||||
function element(name) {
|
||||
return document.createElement(name);
|
||||
}
|
||||
function text(data) {
|
||||
return document.createTextNode(data);
|
||||
}
|
||||
function space() {
|
||||
return text(' ');
|
||||
}
|
||||
function listen(node, event, handler, options) {
|
||||
node.addEventListener(event, handler, options);
|
||||
return () => node.removeEventListener(event, handler, options);
|
||||
}
|
||||
function attr(node, attribute, value) {
|
||||
if (value == null)
|
||||
node.removeAttribute(attribute);
|
||||
else if (node.getAttribute(attribute) !== value)
|
||||
node.setAttribute(attribute, value);
|
||||
}
|
||||
function children(element) {
|
||||
return Array.from(element.childNodes);
|
||||
}
|
||||
function set_input_value(input, value) {
|
||||
input.value = value == null ? '' : value;
|
||||
}
|
||||
function select_option(select, value) {
|
||||
for (let i = 0; i < select.options.length; i += 1) {
|
||||
const option = select.options[i];
|
||||
if (option.__value === value) {
|
||||
option.selected = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
function select_value(select) {
|
||||
const selected_option = select.querySelector(':checked') || select.options[0];
|
||||
return selected_option && selected_option.__value;
|
||||
}
|
||||
function custom_event(type, detail, bubbles = false) {
|
||||
const e = document.createEvent('CustomEvent');
|
||||
e.initCustomEvent(type, bubbles, false, detail);
|
||||
return e;
|
||||
}
|
||||
|
||||
let current_component;
|
||||
function set_current_component(component) {
|
||||
current_component = component;
|
||||
}
|
||||
|
||||
const dirty_components = [];
|
||||
const binding_callbacks = [];
|
||||
const render_callbacks = [];
|
||||
const flush_callbacks = [];
|
||||
const resolved_promise = Promise.resolve();
|
||||
let update_scheduled = false;
|
||||
function schedule_update() {
|
||||
if (!update_scheduled) {
|
||||
update_scheduled = true;
|
||||
resolved_promise.then(flush);
|
||||
}
|
||||
}
|
||||
function add_render_callback(fn) {
|
||||
render_callbacks.push(fn);
|
||||
}
|
||||
let flushing = false;
|
||||
const seen_callbacks = new Set();
|
||||
function flush() {
|
||||
if (flushing)
|
||||
return;
|
||||
flushing = true;
|
||||
do {
|
||||
// first, call beforeUpdate functions
|
||||
// and update components
|
||||
for (let i = 0; i < dirty_components.length; i += 1) {
|
||||
const component = dirty_components[i];
|
||||
set_current_component(component);
|
||||
update(component.$$);
|
||||
}
|
||||
set_current_component(null);
|
||||
dirty_components.length = 0;
|
||||
while (binding_callbacks.length)
|
||||
binding_callbacks.pop()();
|
||||
// then, once components are updated, call
|
||||
// afterUpdate functions. This may cause
|
||||
// subsequent updates...
|
||||
for (let i = 0; i < render_callbacks.length; i += 1) {
|
||||
const callback = render_callbacks[i];
|
||||
if (!seen_callbacks.has(callback)) {
|
||||
// ...so guard against infinite loops
|
||||
seen_callbacks.add(callback);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
render_callbacks.length = 0;
|
||||
} while (dirty_components.length);
|
||||
while (flush_callbacks.length) {
|
||||
flush_callbacks.pop()();
|
||||
}
|
||||
update_scheduled = false;
|
||||
flushing = false;
|
||||
seen_callbacks.clear();
|
||||
}
|
||||
function update($$) {
|
||||
if ($$.fragment !== null) {
|
||||
$$.update();
|
||||
run_all($$.before_update);
|
||||
const dirty = $$.dirty;
|
||||
$$.dirty = [-1];
|
||||
$$.fragment && $$.fragment.p($$.ctx, dirty);
|
||||
$$.after_update.forEach(add_render_callback);
|
||||
}
|
||||
}
|
||||
const outroing = new Set();
|
||||
function transition_in(block, local) {
|
||||
if (block && block.i) {
|
||||
outroing.delete(block);
|
||||
block.i(local);
|
||||
}
|
||||
}
|
||||
function mount_component(component, target, anchor, customElement) {
|
||||
const { fragment, on_mount, on_destroy, after_update } = component.$$;
|
||||
fragment && fragment.m(target, anchor);
|
||||
if (!customElement) {
|
||||
// onMount happens before the initial afterUpdate
|
||||
add_render_callback(() => {
|
||||
const new_on_destroy = on_mount.map(run).filter(is_function);
|
||||
if (on_destroy) {
|
||||
on_destroy.push(...new_on_destroy);
|
||||
}
|
||||
else {
|
||||
// Edge case - component was destroyed immediately,
|
||||
// most likely as a result of a binding initialising
|
||||
run_all(new_on_destroy);
|
||||
}
|
||||
component.$$.on_mount = [];
|
||||
});
|
||||
}
|
||||
after_update.forEach(add_render_callback);
|
||||
}
|
||||
function destroy_component(component, detaching) {
|
||||
const $$ = component.$$;
|
||||
if ($$.fragment !== null) {
|
||||
run_all($$.on_destroy);
|
||||
$$.fragment && $$.fragment.d(detaching);
|
||||
// TODO null out other refs, including component.$$ (but need to
|
||||
// preserve final state?)
|
||||
$$.on_destroy = $$.fragment = null;
|
||||
$$.ctx = [];
|
||||
}
|
||||
}
|
||||
function make_dirty(component, i) {
|
||||
if (component.$$.dirty[0] === -1) {
|
||||
dirty_components.push(component);
|
||||
schedule_update();
|
||||
component.$$.dirty.fill(0);
|
||||
}
|
||||
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
|
||||
}
|
||||
function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) {
|
||||
const parent_component = current_component;
|
||||
set_current_component(component);
|
||||
const $$ = component.$$ = {
|
||||
fragment: null,
|
||||
ctx: null,
|
||||
// state
|
||||
props,
|
||||
update: noop,
|
||||
not_equal,
|
||||
bound: blank_object(),
|
||||
// lifecycle
|
||||
on_mount: [],
|
||||
on_destroy: [],
|
||||
on_disconnect: [],
|
||||
before_update: [],
|
||||
after_update: [],
|
||||
context: new Map(parent_component ? parent_component.$$.context : options.context || []),
|
||||
// everything else
|
||||
callbacks: blank_object(),
|
||||
dirty,
|
||||
skip_bound: false,
|
||||
root: options.target || parent_component.$$.root
|
||||
};
|
||||
append_styles && append_styles($$.root);
|
||||
let ready = false;
|
||||
$$.ctx = instance
|
||||
? instance(component, options.props || {}, (i, ret, ...rest) => {
|
||||
const value = rest.length ? rest[0] : ret;
|
||||
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
|
||||
if (!$$.skip_bound && $$.bound[i])
|
||||
$$.bound[i](value);
|
||||
if (ready)
|
||||
make_dirty(component, i);
|
||||
}
|
||||
return ret;
|
||||
})
|
||||
: [];
|
||||
$$.update();
|
||||
ready = true;
|
||||
run_all($$.before_update);
|
||||
// `false` as a special case of no DOM component
|
||||
$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
|
||||
if (options.target) {
|
||||
if (options.hydrate) {
|
||||
const nodes = children(options.target);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
$$.fragment && $$.fragment.l(nodes);
|
||||
nodes.forEach(detach);
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
$$.fragment && $$.fragment.c();
|
||||
}
|
||||
if (options.intro)
|
||||
transition_in(component.$$.fragment);
|
||||
mount_component(component, options.target, options.anchor, options.customElement);
|
||||
flush();
|
||||
}
|
||||
set_current_component(parent_component);
|
||||
}
|
||||
/**
|
||||
* Base class for Svelte components. Used when dev=false.
|
||||
*/
|
||||
class SvelteComponent {
|
||||
$destroy() {
|
||||
destroy_component(this, 1);
|
||||
this.$destroy = noop;
|
||||
}
|
||||
$on(type, callback) {
|
||||
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
|
||||
callbacks.push(callback);
|
||||
return () => {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index !== -1)
|
||||
callbacks.splice(index, 1);
|
||||
};
|
||||
}
|
||||
$set($$props) {
|
||||
if (this.$$set && !is_empty($$props)) {
|
||||
this.$$.skip_bound = true;
|
||||
this.$$set($$props);
|
||||
this.$$.skip_bound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dispatch_dev(type, detail) {
|
||||
document.dispatchEvent(custom_event(type, Object.assign({ version: '3.42.1' }, detail), true));
|
||||
}
|
||||
function append_dev(target, node) {
|
||||
dispatch_dev('SvelteDOMInsert', { target, node });
|
||||
append(target, node);
|
||||
}
|
||||
function insert_dev(target, node, anchor) {
|
||||
dispatch_dev('SvelteDOMInsert', { target, node, anchor });
|
||||
insert(target, node, anchor);
|
||||
}
|
||||
function detach_dev(node) {
|
||||
dispatch_dev('SvelteDOMRemove', { node });
|
||||
detach(node);
|
||||
}
|
||||
function listen_dev(node, event, handler, options, has_prevent_default, has_stop_propagation) {
|
||||
const modifiers = options === true ? ['capture'] : options ? Array.from(Object.keys(options)) : [];
|
||||
if (has_prevent_default)
|
||||
modifiers.push('preventDefault');
|
||||
if (has_stop_propagation)
|
||||
modifiers.push('stopPropagation');
|
||||
dispatch_dev('SvelteDOMAddEventListener', { node, event, handler, modifiers });
|
||||
const dispose = listen(node, event, handler, options);
|
||||
return () => {
|
||||
dispatch_dev('SvelteDOMRemoveEventListener', { node, event, handler, modifiers });
|
||||
dispose();
|
||||
};
|
||||
}
|
||||
function attr_dev(node, attribute, value) {
|
||||
attr(node, attribute, value);
|
||||
if (value == null)
|
||||
dispatch_dev('SvelteDOMRemoveAttribute', { node, attribute });
|
||||
else
|
||||
dispatch_dev('SvelteDOMSetAttribute', { node, attribute, value });
|
||||
}
|
||||
function prop_dev(node, property, value) {
|
||||
node[property] = value;
|
||||
dispatch_dev('SvelteDOMSetProperty', { node, property, value });
|
||||
}
|
||||
function set_data_dev(text, data) {
|
||||
data = '' + data;
|
||||
if (text.wholeText === data)
|
||||
return;
|
||||
dispatch_dev('SvelteDOMSetData', { node: text, data });
|
||||
text.data = data;
|
||||
}
|
||||
function validate_each_argument(arg) {
|
||||
if (typeof arg !== 'string' && !(arg && typeof arg === 'object' && 'length' in arg)) {
|
||||
let msg = '{#each} only iterates over array-like objects.';
|
||||
if (typeof Symbol === 'function' && arg && Symbol.iterator in arg) {
|
||||
msg += ' You can use a spread to convert this iterable into an array.';
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
function validate_slots(name, slot, keys) {
|
||||
for (const slot_key of Object.keys(slot)) {
|
||||
if (!~keys.indexOf(slot_key)) {
|
||||
console.warn(`<${name}> received an unexpected slot "${slot_key}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Base class for Svelte components with some minor dev-enhancements. Used when dev=true.
|
||||
*/
|
||||
class SvelteComponentDev extends SvelteComponent {
|
||||
constructor(options) {
|
||||
if (!options || (!options.target && !options.$$inline)) {
|
||||
throw new Error("'target' is a required option");
|
||||
}
|
||||
super();
|
||||
}
|
||||
$destroy() {
|
||||
super.$destroy();
|
||||
this.$destroy = () => {
|
||||
console.warn('Component was already destroyed'); // eslint-disable-line no-console
|
||||
};
|
||||
}
|
||||
$capture_state() { }
|
||||
$inject_state() { }
|
||||
}
|
||||
|
||||
/* src/App.svelte generated by Svelte v3.42.1 */
|
||||
|
||||
const file = "src/App.svelte";
|
||||
|
||||
function get_each_context(ctx, list, i) {
|
||||
const child_ctx = ctx.slice();
|
||||
child_ctx[15] = list[i];
|
||||
child_ctx[3] = i;
|
||||
return child_ctx;
|
||||
}
|
||||
|
||||
// (56:1) {#each filteredPeople as person, i}
|
||||
function create_each_block(ctx) {
|
||||
let option;
|
||||
let t0_value = /*person*/ ctx[15].last + "";
|
||||
let t0;
|
||||
let t1;
|
||||
let t2_value = /*person*/ ctx[15].first + "";
|
||||
let t2;
|
||||
|
||||
const block = {
|
||||
c: function create() {
|
||||
option = element("option");
|
||||
t0 = text(t0_value);
|
||||
t1 = text(", ");
|
||||
t2 = text(t2_value);
|
||||
option.__value = /*i*/ ctx[3];
|
||||
option.value = option.__value;
|
||||
attr_dev(option, "class", "svelte-lxvsk7");
|
||||
add_location(option, file, 56, 2, 1258);
|
||||
},
|
||||
m: function mount(target, anchor) {
|
||||
insert_dev(target, option, anchor);
|
||||
append_dev(option, t0);
|
||||
append_dev(option, t1);
|
||||
append_dev(option, t2);
|
||||
},
|
||||
p: function update(ctx, dirty) {
|
||||
if (dirty & /*filteredPeople*/ 2 && t0_value !== (t0_value = /*person*/ ctx[15].last + "")) set_data_dev(t0, t0_value);
|
||||
if (dirty & /*filteredPeople*/ 2 && t2_value !== (t2_value = /*person*/ ctx[15].first + "")) set_data_dev(t2, t2_value);
|
||||
},
|
||||
d: function destroy(detaching) {
|
||||
if (detaching) detach_dev(option);
|
||||
}
|
||||
};
|
||||
|
||||
dispatch_dev("SvelteRegisterBlock", {
|
||||
block,
|
||||
id: create_each_block.name,
|
||||
type: "each",
|
||||
source: "(56:1) {#each filteredPeople as person, i}",
|
||||
ctx
|
||||
});
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
function create_fragment(ctx) {
|
||||
let input0;
|
||||
let t0;
|
||||
let select;
|
||||
let t1;
|
||||
let label0;
|
||||
let input1;
|
||||
let t2;
|
||||
let label1;
|
||||
let input2;
|
||||
let t3;
|
||||
let div;
|
||||
let button0;
|
||||
let t4;
|
||||
let button0_disabled_value;
|
||||
let t5;
|
||||
let button1;
|
||||
let t6;
|
||||
let button1_disabled_value;
|
||||
let t7;
|
||||
let button2;
|
||||
let t8;
|
||||
let button2_disabled_value;
|
||||
let mounted;
|
||||
let dispose;
|
||||
let each_value = /*filteredPeople*/ ctx[1];
|
||||
validate_each_argument(each_value);
|
||||
let each_blocks = [];
|
||||
|
||||
for (let i = 0; i < each_value.length; i += 1) {
|
||||
each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i));
|
||||
}
|
||||
|
||||
const block = {
|
||||
c: function create() {
|
||||
input0 = element("input");
|
||||
t0 = space();
|
||||
select = element("select");
|
||||
|
||||
for (let i = 0; i < each_blocks.length; i += 1) {
|
||||
each_blocks[i].c();
|
||||
}
|
||||
|
||||
t1 = space();
|
||||
label0 = element("label");
|
||||
input1 = element("input");
|
||||
t2 = space();
|
||||
label1 = element("label");
|
||||
input2 = element("input");
|
||||
t3 = space();
|
||||
div = element("div");
|
||||
button0 = element("button");
|
||||
t4 = text("create");
|
||||
t5 = space();
|
||||
button1 = element("button");
|
||||
t6 = text("update");
|
||||
t7 = space();
|
||||
button2 = element("button");
|
||||
t8 = text("delete");
|
||||
attr_dev(input0, "placeholder", "filter prefix");
|
||||
attr_dev(input0, "class", "svelte-lxvsk7");
|
||||
add_location(input0, file, 52, 0, 1129);
|
||||
attr_dev(select, "size", 5);
|
||||
attr_dev(select, "class", "svelte-lxvsk7");
|
||||
if (/*i*/ ctx[3] === void 0) add_render_callback(() => /*select_change_handler*/ ctx[11].call(select));
|
||||
add_location(select, file, 54, 0, 1186);
|
||||
attr_dev(input1, "placeholder", "first");
|
||||
attr_dev(input1, "class", "svelte-lxvsk7");
|
||||
add_location(input1, file, 60, 7, 1342);
|
||||
attr_dev(label0, "class", "svelte-lxvsk7");
|
||||
add_location(label0, file, 60, 0, 1335);
|
||||
attr_dev(input2, "placeholder", "last");
|
||||
attr_dev(input2, "class", "svelte-lxvsk7");
|
||||
add_location(input2, file, 61, 7, 1404);
|
||||
attr_dev(label1, "class", "svelte-lxvsk7");
|
||||
add_location(label1, file, 61, 0, 1397);
|
||||
button0.disabled = button0_disabled_value = !/*first*/ ctx[4] || !/*last*/ ctx[5];
|
||||
attr_dev(button0, "class", "svelte-lxvsk7");
|
||||
add_location(button0, file, 64, 1, 1481);
|
||||
button1.disabled = button1_disabled_value = !/*first*/ ctx[4] || !/*last*/ ctx[5] || !/*selected*/ ctx[2];
|
||||
attr_dev(button1, "class", "svelte-lxvsk7");
|
||||
add_location(button1, file, 65, 1, 1553);
|
||||
button2.disabled = button2_disabled_value = !/*selected*/ ctx[2];
|
||||
attr_dev(button2, "class", "svelte-lxvsk7");
|
||||
add_location(button2, file, 66, 1, 1638);
|
||||
attr_dev(div, "class", "buttons svelte-lxvsk7");
|
||||
add_location(div, file, 63, 0, 1458);
|
||||
},
|
||||
l: function claim(nodes) {
|
||||
throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");
|
||||
},
|
||||
m: function mount(target, anchor) {
|
||||
insert_dev(target, input0, anchor);
|
||||
set_input_value(input0, /*prefix*/ ctx[0]);
|
||||
insert_dev(target, t0, anchor);
|
||||
insert_dev(target, select, anchor);
|
||||
|
||||
for (let i = 0; i < each_blocks.length; i += 1) {
|
||||
each_blocks[i].m(select, null);
|
||||
}
|
||||
|
||||
select_option(select, /*i*/ ctx[3]);
|
||||
insert_dev(target, t1, anchor);
|
||||
insert_dev(target, label0, anchor);
|
||||
append_dev(label0, input1);
|
||||
set_input_value(input1, /*first*/ ctx[4]);
|
||||
insert_dev(target, t2, anchor);
|
||||
insert_dev(target, label1, anchor);
|
||||
append_dev(label1, input2);
|
||||
set_input_value(input2, /*last*/ ctx[5]);
|
||||
insert_dev(target, t3, anchor);
|
||||
insert_dev(target, div, anchor);
|
||||
append_dev(div, button0);
|
||||
append_dev(button0, t4);
|
||||
append_dev(div, t5);
|
||||
append_dev(div, button1);
|
||||
append_dev(button1, t6);
|
||||
append_dev(div, t7);
|
||||
append_dev(div, button2);
|
||||
append_dev(button2, t8);
|
||||
|
||||
if (!mounted) {
|
||||
dispose = [
|
||||
listen_dev(input0, "input", /*input0_input_handler*/ ctx[10]),
|
||||
listen_dev(select, "change", /*select_change_handler*/ ctx[11]),
|
||||
listen_dev(input1, "input", /*input1_input_handler*/ ctx[12]),
|
||||
listen_dev(input2, "input", /*input2_input_handler*/ ctx[13]),
|
||||
listen_dev(button0, "click", /*create*/ ctx[6], false, false, false),
|
||||
listen_dev(button1, "click", /*update*/ ctx[7], false, false, false),
|
||||
listen_dev(button2, "click", /*remove*/ ctx[8], false, false, false)
|
||||
];
|
||||
|
||||
mounted = true;
|
||||
}
|
||||
},
|
||||
p: function update(ctx, [dirty]) {
|
||||
if (dirty & /*prefix*/ 1 && input0.value !== /*prefix*/ ctx[0]) {
|
||||
set_input_value(input0, /*prefix*/ ctx[0]);
|
||||
}
|
||||
|
||||
if (dirty & /*filteredPeople*/ 2) {
|
||||
each_value = /*filteredPeople*/ ctx[1];
|
||||
validate_each_argument(each_value);
|
||||
let i;
|
||||
|
||||
for (i = 0; i < each_value.length; i += 1) {
|
||||
const child_ctx = get_each_context(ctx, each_value, i);
|
||||
|
||||
if (each_blocks[i]) {
|
||||
each_blocks[i].p(child_ctx, dirty);
|
||||
} else {
|
||||
each_blocks[i] = create_each_block(child_ctx);
|
||||
each_blocks[i].c();
|
||||
each_blocks[i].m(select, null);
|
||||
}
|
||||
}
|
||||
|
||||
for (; i < each_blocks.length; i += 1) {
|
||||
each_blocks[i].d(1);
|
||||
}
|
||||
|
||||
each_blocks.length = each_value.length;
|
||||
}
|
||||
|
||||
if (dirty & /*i*/ 8) {
|
||||
select_option(select, /*i*/ ctx[3]);
|
||||
}
|
||||
|
||||
if (dirty & /*first*/ 16 && input1.value !== /*first*/ ctx[4]) {
|
||||
set_input_value(input1, /*first*/ ctx[4]);
|
||||
}
|
||||
|
||||
if (dirty & /*last*/ 32 && input2.value !== /*last*/ ctx[5]) {
|
||||
set_input_value(input2, /*last*/ ctx[5]);
|
||||
}
|
||||
|
||||
if (dirty & /*first, last*/ 48 && button0_disabled_value !== (button0_disabled_value = !/*first*/ ctx[4] || !/*last*/ ctx[5])) {
|
||||
prop_dev(button0, "disabled", button0_disabled_value);
|
||||
}
|
||||
|
||||
if (dirty & /*first, last, selected*/ 52 && button1_disabled_value !== (button1_disabled_value = !/*first*/ ctx[4] || !/*last*/ ctx[5] || !/*selected*/ ctx[2])) {
|
||||
prop_dev(button1, "disabled", button1_disabled_value);
|
||||
}
|
||||
|
||||
if (dirty & /*selected*/ 4 && button2_disabled_value !== (button2_disabled_value = !/*selected*/ ctx[2])) {
|
||||
prop_dev(button2, "disabled", button2_disabled_value);
|
||||
}
|
||||
},
|
||||
i: noop,
|
||||
o: noop,
|
||||
d: function destroy(detaching) {
|
||||
if (detaching) detach_dev(input0);
|
||||
if (detaching) detach_dev(t0);
|
||||
if (detaching) detach_dev(select);
|
||||
destroy_each(each_blocks, detaching);
|
||||
if (detaching) detach_dev(t1);
|
||||
if (detaching) detach_dev(label0);
|
||||
if (detaching) detach_dev(t2);
|
||||
if (detaching) detach_dev(label1);
|
||||
if (detaching) detach_dev(t3);
|
||||
if (detaching) detach_dev(div);
|
||||
mounted = false;
|
||||
run_all(dispose);
|
||||
}
|
||||
};
|
||||
|
||||
dispatch_dev("SvelteRegisterBlock", {
|
||||
block,
|
||||
id: create_fragment.name,
|
||||
type: "component",
|
||||
source: "",
|
||||
ctx
|
||||
});
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
function instance($$self, $$props, $$invalidate) {
|
||||
let filteredPeople;
|
||||
let selected;
|
||||
let { $$slots: slots = {}, $$scope } = $$props;
|
||||
validate_slots('App', slots, []);
|
||||
|
||||
let people = [
|
||||
{ first: 'Hans', last: 'Emil' },
|
||||
{ first: 'Max', last: 'Mustermann' },
|
||||
{ first: 'Roman', last: 'Tisch' }
|
||||
];
|
||||
|
||||
let prefix = '';
|
||||
let first = '';
|
||||
let last = '';
|
||||
let i = 0;
|
||||
|
||||
function create() {
|
||||
$$invalidate(9, people = people.concat({ first, last }));
|
||||
$$invalidate(3, i = people.length - 1);
|
||||
$$invalidate(4, first = $$invalidate(5, last = ''));
|
||||
}
|
||||
|
||||
function update() {
|
||||
$$invalidate(2, selected.first = first, selected);
|
||||
$$invalidate(2, selected.last = last, selected);
|
||||
$$invalidate(9, people);
|
||||
}
|
||||
|
||||
function remove() {
|
||||
// Remove selected person from the source array (people), not the filtered array
|
||||
const index = people.indexOf(selected);
|
||||
|
||||
$$invalidate(9, people = [...people.slice(0, index), ...people.slice(index + 1)]);
|
||||
$$invalidate(4, first = $$invalidate(5, last = ''));
|
||||
$$invalidate(3, i = Math.min(i, filteredPeople.length - 2));
|
||||
}
|
||||
|
||||
function reset_inputs(person) {
|
||||
$$invalidate(4, first = person ? person.first : '');
|
||||
$$invalidate(5, last = person ? person.last : '');
|
||||
}
|
||||
|
||||
const writable_props = [];
|
||||
|
||||
Object.keys($$props).forEach(key => {
|
||||
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== '$$' && key !== 'slot') console.warn(`<App> was created with unknown prop '${key}'`);
|
||||
});
|
||||
|
||||
function input0_input_handler() {
|
||||
prefix = this.value;
|
||||
$$invalidate(0, prefix);
|
||||
}
|
||||
|
||||
function select_change_handler() {
|
||||
i = select_value(this);
|
||||
$$invalidate(3, i);
|
||||
}
|
||||
|
||||
function input1_input_handler() {
|
||||
first = this.value;
|
||||
$$invalidate(4, first);
|
||||
}
|
||||
|
||||
function input2_input_handler() {
|
||||
last = this.value;
|
||||
$$invalidate(5, last);
|
||||
}
|
||||
|
||||
$$self.$capture_state = () => ({
|
||||
people,
|
||||
prefix,
|
||||
first,
|
||||
last,
|
||||
i,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
reset_inputs,
|
||||
filteredPeople,
|
||||
selected
|
||||
});
|
||||
|
||||
$$self.$inject_state = $$props => {
|
||||
if ('people' in $$props) $$invalidate(9, people = $$props.people);
|
||||
if ('prefix' in $$props) $$invalidate(0, prefix = $$props.prefix);
|
||||
if ('first' in $$props) $$invalidate(4, first = $$props.first);
|
||||
if ('last' in $$props) $$invalidate(5, last = $$props.last);
|
||||
if ('i' in $$props) $$invalidate(3, i = $$props.i);
|
||||
if ('filteredPeople' in $$props) $$invalidate(1, filteredPeople = $$props.filteredPeople);
|
||||
if ('selected' in $$props) $$invalidate(2, selected = $$props.selected);
|
||||
};
|
||||
|
||||
if ($$props && "$$inject" in $$props) {
|
||||
$$self.$inject_state($$props.$$inject);
|
||||
}
|
||||
|
||||
$$self.$$.update = () => {
|
||||
if ($$self.$$.dirty & /*prefix, people*/ 513) {
|
||||
$$invalidate(1, filteredPeople = prefix
|
||||
? people.filter(person => {
|
||||
const name = `${person.last}, ${person.first}`;
|
||||
return name.toLowerCase().startsWith(prefix.toLowerCase());
|
||||
})
|
||||
: people);
|
||||
}
|
||||
|
||||
if ($$self.$$.dirty & /*filteredPeople, i*/ 10) {
|
||||
$$invalidate(2, selected = filteredPeople[i]);
|
||||
}
|
||||
|
||||
if ($$self.$$.dirty & /*selected*/ 4) {
|
||||
reset_inputs(selected);
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
prefix,
|
||||
filteredPeople,
|
||||
selected,
|
||||
i,
|
||||
first,
|
||||
last,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
people,
|
||||
input0_input_handler,
|
||||
select_change_handler,
|
||||
input1_input_handler,
|
||||
input2_input_handler
|
||||
];
|
||||
}
|
||||
|
||||
class App extends SvelteComponentDev {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
init(this, options, instance, create_fragment, safe_not_equal, {});
|
||||
|
||||
dispatch_dev("SvelteRegisterComponent", {
|
||||
component: this,
|
||||
tagName: "App",
|
||||
options,
|
||||
id: create_fragment.name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var app = new App({
|
||||
target: document.body
|
||||
});
|
||||
|
||||
return app;
|
||||
|
||||
}());
|
||||
//# sourceMappingURL=bundle.js.map
|
1
frontend/public/build/bundle.js.map
Normal file
1
frontend/public/build/bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
frontend/public/favicon.png
Normal file
BIN
frontend/public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
63
frontend/public/global.css
Normal file
63
frontend/public/global.css
Normal file
|
@ -0,0 +1,63 @@
|
|||
html, body {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(0,100,200);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: rgb(0,80,160);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input, button, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
-webkit-padding: 0.4em 0;
|
||||
padding: 0.4em;
|
||||
margin: 0 0 0.5em 0;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
button {
|
||||
color: #333;
|
||||
background-color: #f4f4f4;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
button:not(:disabled):active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
border-color: #666;
|
||||
}
|
|
@ -1,43 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
<title>Todo</title>
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
<link rel='stylesheet' href='/global.css'>
|
||||
<link rel='stylesheet' href='/build/bundle.css'>
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
<script defer src='/build/bundle.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
|
76
frontend/rollup.config.js
Normal file
76
frontend/rollup.config.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
import svelte from 'rollup-plugin-svelte';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import livereload from 'rollup-plugin-livereload';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
function serve() {
|
||||
let server;
|
||||
|
||||
function toExit() {
|
||||
if (server) server.kill(0);
|
||||
}
|
||||
|
||||
return {
|
||||
writeBundle() {
|
||||
if (server) return;
|
||||
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
shell: true
|
||||
});
|
||||
|
||||
process.on('SIGTERM', toExit);
|
||||
process.on('exit', toExit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
input: 'src/main.js',
|
||||
output: {
|
||||
sourcemap: true,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/build/bundle.js'
|
||||
},
|
||||
plugins: [
|
||||
svelte({
|
||||
compilerOptions: {
|
||||
// enable run-time checks when not in production
|
||||
dev: !production
|
||||
}
|
||||
}),
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file - better for performance
|
||||
css({ output: 'bundle.css' }),
|
||||
|
||||
// If you have external dependencies installed from
|
||||
// npm, you'll most likely need these plugins. In
|
||||
// some cases you'll need additional configuration -
|
||||
// consult the documentation for details:
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: ['svelte']
|
||||
}),
|
||||
commonjs(),
|
||||
|
||||
// In dev mode, call `npm run start` once
|
||||
// the bundle has been generated
|
||||
!production && serve(),
|
||||
|
||||
// Watch the `public` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload('public'),
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
production && terser()
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false
|
||||
}
|
||||
};
|
121
frontend/scripts/setupTypeScript.js
Normal file
121
frontend/scripts/setupTypeScript.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
// @ts-check
|
||||
|
||||
/** This script modifies the project to support TS code in .svelte files like:
|
||||
|
||||
<script lang="ts">
|
||||
export let name: string;
|
||||
</script>
|
||||
|
||||
As well as validating the code for CI.
|
||||
*/
|
||||
|
||||
/** To work on this script:
|
||||
rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
|
||||
*/
|
||||
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { argv } = require("process")
|
||||
|
||||
const projectRoot = argv[2] || path.join(__dirname, "..")
|
||||
|
||||
// Add deps to pkg.json
|
||||
const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
|
||||
packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
|
||||
"svelte-check": "^2.0.0",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.0.0",
|
||||
"typescript": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"@tsconfig/svelte": "^2.0.0"
|
||||
})
|
||||
|
||||
// Add script for checking
|
||||
packageJSON.scripts = Object.assign(packageJSON.scripts, {
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
})
|
||||
|
||||
// Write the package JSON
|
||||
fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
|
||||
|
||||
// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
|
||||
const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
|
||||
const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
|
||||
fs.renameSync(beforeMainJSPath, afterMainTSPath)
|
||||
|
||||
// Switch the app.svelte file to use TS
|
||||
const appSveltePath = path.join(projectRoot, "src", "App.svelte")
|
||||
let appFile = fs.readFileSync(appSveltePath, "utf8")
|
||||
appFile = appFile.replace("<script>", '<script lang="ts">')
|
||||
appFile = appFile.replace("export let name;", 'export let name: string;')
|
||||
fs.writeFileSync(appSveltePath, appFile)
|
||||
|
||||
// Edit rollup config
|
||||
const rollupConfigPath = path.join(projectRoot, "rollup.config.js")
|
||||
let rollupConfig = fs.readFileSync(rollupConfigPath, "utf8")
|
||||
|
||||
// Edit imports
|
||||
rollupConfig = rollupConfig.replace(`'rollup-plugin-terser';`, `'rollup-plugin-terser';
|
||||
import sveltePreprocess from 'svelte-preprocess';
|
||||
import typescript from '@rollup/plugin-typescript';`)
|
||||
|
||||
// Replace name of entry point
|
||||
rollupConfig = rollupConfig.replace(`'src/main.js'`, `'src/main.ts'`)
|
||||
|
||||
// Add preprocessor
|
||||
rollupConfig = rollupConfig.replace(
|
||||
'compilerOptions:',
|
||||
'preprocess: sveltePreprocess({ sourceMap: !production }),\n\t\t\tcompilerOptions:'
|
||||
);
|
||||
|
||||
// Add TypeScript
|
||||
rollupConfig = rollupConfig.replace(
|
||||
'commonjs(),',
|
||||
'commonjs(),\n\t\ttypescript({\n\t\t\tsourceMap: !production,\n\t\t\tinlineSources: !production\n\t\t}),'
|
||||
);
|
||||
fs.writeFileSync(rollupConfigPath, rollupConfig)
|
||||
|
||||
// Add TSConfig
|
||||
const tsconfig = `{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
||||
}`
|
||||
const tsconfigPath = path.join(projectRoot, "tsconfig.json")
|
||||
fs.writeFileSync(tsconfigPath, tsconfig)
|
||||
|
||||
// Add global.d.ts
|
||||
const dtsPath = path.join(projectRoot, "src", "global.d.ts")
|
||||
fs.writeFileSync(dtsPath, `/// <reference types="svelte" />`)
|
||||
|
||||
// Delete this script, but not during testing
|
||||
if (!argv[2]) {
|
||||
// Remove the script
|
||||
fs.unlinkSync(path.join(__filename))
|
||||
|
||||
// Check for Mac's DS_store file, and if it's the only one left remove it
|
||||
const remainingFiles = fs.readdirSync(path.join(__dirname))
|
||||
if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') {
|
||||
fs.unlinkSync(path.join(__dirname, '.DS_store'))
|
||||
}
|
||||
|
||||
// Check if the scripts folder is empty
|
||||
if (fs.readdirSync(path.join(__dirname)).length === 0) {
|
||||
// Remove the scripts folder
|
||||
fs.rmdirSync(path.join(__dirname))
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the extension recommendation
|
||||
fs.mkdirSync(path.join(projectRoot, ".vscode"), { recursive: true })
|
||||
fs.writeFileSync(path.join(projectRoot, ".vscode", "extensions.json"), `{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
}
|
||||
`)
|
||||
|
||||
console.log("Converted to TypeScript.")
|
||||
|
||||
if (fs.existsSync(path.join(projectRoot, "node_modules"))) {
|
||||
console.log("\nYou will need to re-run your dependency manager to get started.")
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
|
||||
import RootPage from './modules/Root';
|
||||
import AboutPage from './modules/About';
|
||||
import AccountPage from './modules/Account';
|
||||
import LoginPage from './modules/Login';
|
||||
import OauthPage from './modules/Oauth';
|
||||
import SignupPage from './modules/Signup';
|
||||
import TodoPage from './modules/TodoList';
|
||||
import { connect } from 'react-redux';
|
||||
import ThemeProvider from './ThemeProvider';
|
||||
import Navbar from './Navbar';
|
||||
|
||||
const App = (props) => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<Navbar />
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path='/about'>
|
||||
<AboutPage />
|
||||
</Route>
|
||||
<Route path='/login'>
|
||||
<LoginPage />
|
||||
</Route>
|
||||
<Route path='/signup'>
|
||||
<SignupPage />
|
||||
</Route>
|
||||
<Route path='/discord'>
|
||||
<OauthPage />
|
||||
</Route>
|
||||
<PRoute path='/todos'>
|
||||
<TodoPage />
|
||||
</PRoute>
|
||||
<PRoute path='/account'>
|
||||
<AccountPage />
|
||||
</PRoute>
|
||||
<PRoute exact path='/'>
|
||||
<RootPage />
|
||||
</PRoute>
|
||||
<Route path='/'>
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/'
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const PRoute = connect(
|
||||
(state) => {
|
||||
return {
|
||||
token: state.login.token
|
||||
};
|
||||
},
|
||||
(dispatch, props) => {
|
||||
return {};
|
||||
}
|
||||
)(({ children, ...props }) => {
|
||||
return (
|
||||
<Route
|
||||
{...props}
|
||||
render={({ location }) => {
|
||||
return props.token !== undefined ? (
|
||||
children
|
||||
) : (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/login',
|
||||
state: { from: location }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default App;
|
90
frontend/src/App.svelte
Normal file
90
frontend/src/App.svelte
Normal file
|
@ -0,0 +1,90 @@
|
|||
<!-- https://eugenkiss.github.io/7guis/tasks#crud -->
|
||||
|
||||
<script>
|
||||
let people = [
|
||||
{ first: 'Hans', last: 'Emil' },
|
||||
{ first: 'Max', last: 'Mustermann' },
|
||||
{ first: 'Roman', last: 'Tisch' }
|
||||
];
|
||||
|
||||
let prefix = '';
|
||||
let first = '';
|
||||
let last = '';
|
||||
let i = 0;
|
||||
|
||||
$: filteredPeople = prefix
|
||||
? people.filter(person => {
|
||||
const name = `${person.last}, ${person.first}`;
|
||||
return name.toLowerCase().startsWith(prefix.toLowerCase());
|
||||
})
|
||||
: people;
|
||||
|
||||
$: selected = filteredPeople[i];
|
||||
|
||||
$: reset_inputs(selected);
|
||||
|
||||
function create() {
|
||||
people = people.concat({ first, last });
|
||||
i = people.length - 1;
|
||||
first = last = '';
|
||||
}
|
||||
|
||||
function update() {
|
||||
selected.first = first;
|
||||
selected.last = last;
|
||||
people = people;
|
||||
}
|
||||
|
||||
function remove() {
|
||||
// Remove selected person from the source array (people), not the filtered array
|
||||
const index = people.indexOf(selected);
|
||||
people = [...people.slice(0, index), ...people.slice(index + 1)];
|
||||
|
||||
first = last = '';
|
||||
i = Math.min(i, filteredPeople.length - 2);
|
||||
}
|
||||
|
||||
function reset_inputs(person) {
|
||||
first = person ? person.first : '';
|
||||
last = person ? person.last : '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<input placeholder="filter prefix" bind:value={prefix}>
|
||||
|
||||
<select bind:value={i} size={5}>
|
||||
{#each filteredPeople as person, i}
|
||||
<option value={i}>{person.last}, {person.first}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<label><input bind:value={first} placeholder="first"></label>
|
||||
<label><input bind:value={last} placeholder="last"></label>
|
||||
|
||||
<div class='buttons'>
|
||||
<button on:click={create} disabled="{!first || !last}">create</button>
|
||||
<button on:click={update} disabled="{!first || !last || !selected}">update</button>
|
||||
<button on:click={remove} disabled="{!selected}">delete</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
select {
|
||||
float: left;
|
||||
margin: 0 1em 1em 0;
|
||||
width: 14em;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
clear: both;
|
||||
}
|
||||
</style>
|
|
@ -1,122 +0,0 @@
|
|||
import Grid from '@material-ui/core/Grid';
|
||||
import GroupIcon from '@material-ui/icons/Group';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import AssignmentIndIcon from '@material-ui/icons/AssignmentInd';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import { logout } from './reducers/login';
|
||||
|
||||
const styles = (theme) => {
|
||||
return {
|
||||
container: {
|
||||
width: '100%'
|
||||
},
|
||||
flexbox: {
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
width: 'auto'
|
||||
},
|
||||
buttonWrapper: {
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
button: {
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
typography: {
|
||||
margin: '5px 0px'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const Navbar = (props) => {
|
||||
const { classes } = props;
|
||||
return (
|
||||
<div>
|
||||
<Grid container direction='row' alignItems='flex-end' justify='flex-end' className={classes.container}>
|
||||
<Grid item className={classes.flexbox}>
|
||||
<Box className={classes.flexbox} />
|
||||
</Grid>
|
||||
<Grid item alignItems='flex-end' justify='flex-end'>
|
||||
<LoginLogoutButton className={classes.buttonWrapper} {...props} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LoginLogoutButton = connect(
|
||||
(state) => {
|
||||
return {
|
||||
token: state.login.token
|
||||
};
|
||||
},
|
||||
(dispatch, props) => {
|
||||
return {};
|
||||
}
|
||||
)(({ children, ...props }) => {
|
||||
const { classes } = props;
|
||||
return props.token !== undefined ? (
|
||||
<>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
window.location.pathname = '/account';
|
||||
}}
|
||||
className={classes.button}>
|
||||
<SettingsIcon />
|
||||
<Typography>Settings</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
props.logout();
|
||||
}}
|
||||
className={classes.button}>
|
||||
<ExitToAppIcon />
|
||||
<Typography className={classes.typography}>Log Out</Typography>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
window.location.pathname = '/login';
|
||||
}}
|
||||
className={classes.button}>
|
||||
<GroupIcon />
|
||||
<Typography className={classes.typography}>Log In</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
window.location.pathname = '/signup';
|
||||
}}
|
||||
className={classes.button}>
|
||||
<AssignmentIndIcon />
|
||||
<Typography className={classes.typography}>Sign Up</Typography>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default connect(
|
||||
(state, props) => {
|
||||
return {};
|
||||
},
|
||||
(dispatch, props) => {
|
||||
return {
|
||||
logout: () => {
|
||||
dispatch(logout());
|
||||
}
|
||||
};
|
||||
}
|
||||
)(withStyles(styles)(Navbar));
|
|
@ -1,37 +0,0 @@
|
|||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import { withStyles, withTheme } from '@material-ui/styles';
|
||||
import { createTheme, ThemeProvider } from '@material-ui/core/styles';
|
||||
import theme from './theme';
|
||||
|
||||
const styles = () => {
|
||||
return {
|
||||
root: {
|
||||
height: '100vh',
|
||||
zIndex: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex'
|
||||
},
|
||||
content: {
|
||||
zIndex: 3,
|
||||
flexGrow: 1
|
||||
},
|
||||
spacing: (n) => {
|
||||
return `${n * 2}px`;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ThemeWrapper = (props) => {
|
||||
const { children, classes } = props;
|
||||
return (
|
||||
<ThemeProvider theme={createTheme(theme)}>
|
||||
<CssBaseline />
|
||||
<div id='main' className={classes.content}>
|
||||
{children}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTheme(withStyles(styles)(ThemeWrapper));
|
|
@ -1,79 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import RootReducer from './reducers';
|
||||
import axios from 'axios';
|
||||
import logger from 'redux-logger';
|
||||
|
||||
const defaultConfig = {
|
||||
apiUrl: 'http://localhost:8080/api'
|
||||
};
|
||||
|
||||
const renderApp = ({ config, user }) => {
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
const store = configureStore({
|
||||
devTools: isDev,
|
||||
preloadedState: {
|
||||
config: config,
|
||||
login: user
|
||||
},
|
||||
reducer: RootReducer,
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger)
|
||||
});
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
|
||||
const findConfig = (fullConfig) => {
|
||||
return Object.assign(fullConfig.defaultConfig, fullConfig.configs[fullConfig.hosts[window.location.host]]);
|
||||
};
|
||||
|
||||
axios
|
||||
.get('/config.json')
|
||||
.then(
|
||||
(success) => {
|
||||
return Object.assign(defaultConfig, findConfig(success.data));
|
||||
},
|
||||
() => {
|
||||
return defaultConfig;
|
||||
}
|
||||
)
|
||||
.then((config) => {
|
||||
const details = JSON.parse(localStorage.getItem('userDetails') || '{}');
|
||||
return axios
|
||||
.get(`${config.apiUrl}/user/authorized`, {
|
||||
headers: {
|
||||
id: details.id,
|
||||
Authorization: details.token
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(success) => {
|
||||
return {
|
||||
config,
|
||||
user: details || {}
|
||||
};
|
||||
},
|
||||
() => {
|
||||
return {
|
||||
config,
|
||||
user: {}
|
||||
};
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(({ config, user }) => {
|
||||
renderApp({ config, user });
|
||||
});
|
7
frontend/src/main.js
Normal file
7
frontend/src/main.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import App from './App.svelte';
|
||||
|
||||
var app = new App({
|
||||
target: document.body
|
||||
});
|
||||
|
||||
export default app;
|
|
@ -1,5 +0,0 @@
|
|||
const AboutPage = (props) => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default AboutPage;
|
|
@ -1,5 +0,0 @@
|
|||
const AccountPage = (props) => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default AccountPage;
|
|
@ -1,174 +0,0 @@
|
|||
import { login, forgotPassword } from '../../reducers/login';
|
||||
import { useState } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Visibility from '@material-ui/icons/Visibility';
|
||||
import VisibilityOff from '@material-ui/icons/VisibilityOff';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
|
||||
const styles = (theme) => { };
|
||||
|
||||
const LoginPage = (props) => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [forgotPassword, setForgotPassword] = useState(false);
|
||||
|
||||
const handleForgotPassword = () => {
|
||||
if (!!email) {
|
||||
props.forgotPassword(email);
|
||||
setForgotPassword(false);
|
||||
setError(false);
|
||||
setEmail('');
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Grid container alignItems='center' justify='center'>
|
||||
<Grid item>
|
||||
<form>
|
||||
<div>
|
||||
<Typography align='center' variant='h6' gutterBottom>
|
||||
{forgotPassword ? 'Reset Password' : 'Sign in'}
|
||||
</Typography>
|
||||
<TextField
|
||||
autoComplete='current-email'
|
||||
error={error}
|
||||
label='Email'
|
||||
name='email'
|
||||
type='email'
|
||||
value={email}
|
||||
variant='outlined'
|
||||
onChange={(event) => {
|
||||
return setEmail(event.target.value ?? '');
|
||||
}}
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
forgotPassword ? props.forgotPassword(email) : props.login(email, password);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{forgotPassword === false ? (
|
||||
<>
|
||||
<div>
|
||||
<TextField
|
||||
autoComplete='current-password'
|
||||
label='Password'
|
||||
name='password'
|
||||
type={visible ? 'text' : 'password'}
|
||||
value={password}
|
||||
variant='outlined'
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value ?? '');
|
||||
}}
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
props.login(email, password);
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<IconButton
|
||||
aria-label='toggle password visibility'
|
||||
onClick={() => {
|
||||
return setVisible(!visible);
|
||||
}}
|
||||
edge='end'>
|
||||
{visible ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
id='forgot-password-toggle-button'
|
||||
color='secondary'
|
||||
onClick={() => {
|
||||
return setForgotPassword(true);
|
||||
}}
|
||||
size='small'>
|
||||
Forgot Password?
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
id='login-button'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
props.login(email, password);
|
||||
}}>
|
||||
Login
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Grid container>
|
||||
<Grid item>
|
||||
<Button
|
||||
id='forgot-password-cancel-button'
|
||||
variant='contained'
|
||||
fullWidth
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
setForgotPassword(false);
|
||||
setError(false);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
id='forgot-password-submit-button'
|
||||
variant='contained'
|
||||
color='secondary'
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
handleForgotPassword();
|
||||
}}>
|
||||
Continue
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
id='login-with-discord'
|
||||
variant='contained'
|
||||
fullWidth
|
||||
color='secondary'
|
||||
onClick={()=>{
|
||||
|
||||
}}
|
||||
>Sign in with Discord</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {};
|
||||
},
|
||||
(dispatch, props) => {
|
||||
return {
|
||||
login: (email, password) => {
|
||||
dispatch(login(email, password));
|
||||
},
|
||||
forgotPassword: (email) => {
|
||||
dispatch(forgotPassword(email));
|
||||
}
|
||||
};
|
||||
}
|
||||
)(withStyles(styles)(LoginPage));
|
|
@ -1,17 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
|
||||
const styles = (theme) => { };
|
||||
|
||||
const OauthPage = (props) => {
|
||||
return <div>test</div>
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {};
|
||||
},
|
||||
(dispatch, props) => {
|
||||
return {};
|
||||
}
|
||||
)(withStyles(styles)(OauthPage));
|
|
@ -1,5 +0,0 @@
|
|||
const RootPage = (props) => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default RootPage;
|
|
@ -1,162 +0,0 @@
|
|||
import { signup } from '../../reducers/login';
|
||||
import { useState } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Visibility from '@material-ui/icons/Visibility';
|
||||
import VisibilityOff from '@material-ui/icons/VisibilityOff';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
|
||||
const styles = (theme) => {};
|
||||
|
||||
const SignupPage = (props) => {
|
||||
//const { classes } = props;
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const checkSignup = () => {
|
||||
if (password !== confirmPassword) {
|
||||
setError(true);
|
||||
} else {
|
||||
setError(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container alignItems='center' justify='center'>
|
||||
<Grid item alignItems='center' justify='center'>
|
||||
<form>
|
||||
<div>
|
||||
<Typography align='center' variant='h6' gutterBottom>
|
||||
Sign Up
|
||||
</Typography>
|
||||
<TextField
|
||||
autoComplete='new-email'
|
||||
label='Email'
|
||||
name='email'
|
||||
type='email'
|
||||
value={email}
|
||||
variant='outlined'
|
||||
fullWidth
|
||||
onChange={(event) => {
|
||||
return setEmail(event.target.value ?? '');
|
||||
}}
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
checkSignup();
|
||||
if (!error) {
|
||||
props.signup(email, password);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<TextField
|
||||
autoComplete='new-password'
|
||||
label='Password'
|
||||
name='password'
|
||||
type={visible ? 'text' : 'password'}
|
||||
value={password}
|
||||
variant='outlined'
|
||||
fullWidth
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value ?? '');
|
||||
}}
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
checkSignup();
|
||||
if (!error) {
|
||||
props.signup(email, password);
|
||||
}
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<IconButton
|
||||
aria-label='toggle password visibility'
|
||||
onClick={() => {
|
||||
return setVisible(!visible);
|
||||
}}
|
||||
edge='end'>
|
||||
{visible ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
autoComplete='confirm-password'
|
||||
label='Confirm Password'
|
||||
name='confirm-password'
|
||||
type={visible ? 'text' : 'password'}
|
||||
error={error}
|
||||
value={confirmPassword}
|
||||
variant='outlined'
|
||||
fullWidth
|
||||
onChange={(event) => {
|
||||
setConfirmPassword(event.target.value ?? '');
|
||||
}}
|
||||
onKeyPress={(event) => {
|
||||
checkSignup();
|
||||
if (event.key === 'Enter') {
|
||||
if (!error) {
|
||||
props.signup(email, password);
|
||||
}
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<IconButton
|
||||
aria-label='toggle confirm password visibility'
|
||||
onClick={() => {
|
||||
return setVisible(!visible);
|
||||
}}
|
||||
edge='end'>
|
||||
{visible ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
id='login-button'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
checkSignup();
|
||||
if (!error) {
|
||||
props.signup(email, password);
|
||||
}
|
||||
}}>
|
||||
Sign Up
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {};
|
||||
},
|
||||
(dispatch, props) => {
|
||||
return {
|
||||
signup: (email, password) => {
|
||||
dispatch(signup(email, password));
|
||||
}
|
||||
};
|
||||
}
|
||||
)(withStyles(styles)(SignupPage));
|
|
@ -1,5 +0,0 @@
|
|||
const TodoPage = (props) => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default TodoPage;
|
|
@ -1,28 +0,0 @@
|
|||
import { createAction, createAsyncAction } from './utils';
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
const actions = {
|
||||
refresh: 'LOCAL_STORAGE_REFRESH'
|
||||
};
|
||||
|
||||
export const getConfigValue = createAsyncAction((dispatch, getState, config, key) => {
|
||||
const payload = {
|
||||
key: key,
|
||||
value: JSON.parse(localStorage.getItem(key)) || undefined
|
||||
};
|
||||
return dispatch(refreshConfigValue(payload));
|
||||
});
|
||||
export const setConfigValue = createAsyncAction((dispatch, getState, config, key, value) => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
return dispatch(refreshConfigValue({ key: key, value: value }));
|
||||
});
|
||||
|
||||
export const refreshConfigValue = createAction(actions.refresh, (payload) => {
|
||||
return payload;
|
||||
});
|
||||
|
||||
export default createReducer({}, (builder) => {
|
||||
builder.addDefaultCase((state, action) => {
|
||||
state[action.payload?.key] = action.payload?.value;
|
||||
});
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
import { combineReducers } from '@reduxjs/toolkit';
|
||||
import LocalStorageReducer from './localStorage';
|
||||
import LoginReducer from './login';
|
||||
import ConfigReducer from './config';
|
||||
|
||||
export default combineReducers({
|
||||
localStorage: LocalStorageReducer,
|
||||
login: LoginReducer,
|
||||
config: ConfigReducer
|
||||
});
|
|
@ -1,28 +0,0 @@
|
|||
import { createAction, createAsyncAction } from './utils';
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
const actions = {
|
||||
refresh: 'LOCAL_STORAGE_REFRESH'
|
||||
};
|
||||
|
||||
export const readLocalStorage = createAsyncAction((dispatch, getState, config, key) => {
|
||||
const payload = {
|
||||
key: key,
|
||||
value: JSON.parse(localStorage.getItem(key)) || undefined
|
||||
};
|
||||
return dispatch(refreshLocalStorage(payload));
|
||||
});
|
||||
export const updateLocalStorage = createAsyncAction((dispatch, getState, config, key, value) => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
return dispatch(refreshLocalStorage({ key: key, value: value }));
|
||||
});
|
||||
|
||||
export const refreshLocalStorage = createAction(actions.refresh, (payload) => {
|
||||
return payload;
|
||||
});
|
||||
|
||||
export default createReducer({}, (builder) => {
|
||||
builder.addDefaultCase((state, action) => {
|
||||
state[action.payload?.key] = action.payload?.value;
|
||||
});
|
||||
});
|
|
@ -1,120 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import { createAction, createAsyncAction } from './utils';
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { updateLocalStorage } from './localStorage';
|
||||
|
||||
const actions = {
|
||||
update: 'UPDATE_LOGIN_DETAILS'
|
||||
};
|
||||
|
||||
const updateLoginDetails = createAction(actions.update, (payload) => {
|
||||
return payload;
|
||||
});
|
||||
|
||||
export const login = createAsyncAction((dispatch, getState, config, email, password) => {
|
||||
axios
|
||||
.post(`${config.apiUrl}/user/login`, {
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
.then(
|
||||
(success) => {
|
||||
console.error('success', success);
|
||||
dispatch(
|
||||
updateLoginDetails({
|
||||
id: success.data['userid'],
|
||||
token: success.data['session_token'],
|
||||
error: false
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
updateLocalStorage('userDetails', {
|
||||
id: success.data['userid'],
|
||||
token: success.data['session_token']
|
||||
})
|
||||
);
|
||||
window.location.pathname = '/';
|
||||
},
|
||||
(reject) => {
|
||||
console.error(reject);
|
||||
dispatch(
|
||||
updateLoginDetails({
|
||||
id: undefined,
|
||||
token: undefined,
|
||||
error: true
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
updateLocalStorage('userDetails', {
|
||||
id: undefined,
|
||||
token: undefined
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const signup = createAsyncAction((dispatch, getState, config, email, password) => {
|
||||
axios
|
||||
.post(`${config.apiUrl}/user/signup`, {
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
.then(
|
||||
(success) => {
|
||||
console.error('success', success);
|
||||
window.location.pathname = '/login';
|
||||
},
|
||||
(reject) => {
|
||||
console.error(reject);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const forgotPassword = createAsyncAction((dispatch, getState, config, email) => {});
|
||||
|
||||
export const logout = createAsyncAction((dispatch, getState, config) => {
|
||||
const details = getState().login;
|
||||
axios
|
||||
.post(`${config.apiUrl}/user/logout`, {
|
||||
userid: details.id,
|
||||
session_token: details.token
|
||||
})
|
||||
.then(
|
||||
(success) => {
|
||||
dispatch(
|
||||
updateLoginDetails({
|
||||
id: undefined,
|
||||
token: undefined,
|
||||
error: false
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
updateLocalStorage('userDetails', {
|
||||
id: undefined,
|
||||
token: undefined
|
||||
})
|
||||
);
|
||||
|
||||
window.location.pathname = '/login';
|
||||
},
|
||||
(reject) => {
|
||||
console.warn(reject);
|
||||
console.warn('could not log out.');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export default createReducer(
|
||||
{
|
||||
id: undefined,
|
||||
token: undefined,
|
||||
error: false
|
||||
},
|
||||
(builder) => {
|
||||
builder.addCase(actions.update, (state, action) => {
|
||||
console.error(state, action);
|
||||
state = { ...state, ...action?.payload };
|
||||
});
|
||||
}
|
||||
);
|
|
@ -1,16 +0,0 @@
|
|||
export const createAction = (type, payload) => {
|
||||
return (...args) => {
|
||||
return {
|
||||
type: type,
|
||||
payload: payload(...args)
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const createAsyncAction = (payload) => {
|
||||
return (...args) => {
|
||||
return function (dispatch, getState, config) {
|
||||
payload(dispatch, getState, getState()?.config, ...args);
|
||||
};
|
||||
};
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
|
@ -1,5 +0,0 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
|
@ -1,13 +0,0 @@
|
|||
import grey from '@material-ui/core/colors/grey';
|
||||
|
||||
const theme = {
|
||||
palette: {
|
||||
primary: {
|
||||
main: grey[200]
|
||||
},
|
||||
secondary: {
|
||||
main: grey[200]
|
||||
}
|
||||
}
|
||||
};
|
||||
export default theme;
|
Loading…
Reference in a new issue