diff --git a/flake.nix b/flake.nix index 783381a..7ce64ef 100644 --- a/flake.nix +++ b/flake.nix @@ -103,6 +103,8 @@ ]; }; } - ); + ) // { + nixosModules = { timeclonk = import ./module.nix; }; + }; } diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..e1eb9c5 --- /dev/null +++ b/module.nix @@ -0,0 +1,107 @@ +{ config, options, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.timeclonk; + opt = options.services.timeclonk; + settingsFormat = pkgs.formats.toml { }; + +in + +{ + + ###### interface + options = { + services.timeclonk = { + enable = mkEnableOption (lib.mdDoc "timeclonk; markdown based multi user zettelkasten"); + + user = mkOption { + type = types.str; + default = "timeclonk"; + example = "timeclonk-user"; + description = "linux user account in which to run timeclonk."; + }; + group = lib.mkOption { + type = lib.types.str; + default = "timeclonk"; + description = lib.mdDoc "linux group under which timeclonk runs."; + }; + + settings = lib.mkOption { + inherit (settingsFormat) type; + default = '' + ip = '127.0.0.1' + port = 8000 + createdirs = true + altmainsite = [] + file_tmp_path = './temp' + file_path = './files' + + [orgauth_config] + mainsite = 'http://localhost:8000' + appname = 'timeclonk' + emaildomain = 'timeclonk.com' + db = './timeclonk.db' + admin_email = 'admin@admin.admin' + regen_login_tokens = true + email_token_expiration_ms = 86400000 + reset_token_expiration_ms = 86400000 + invite_token_expiration_ms = 604800000 + open_registration = false + send_emails = false + non_admin_invite = true + remote_registration = true + ''; + description = '' + timeclonk config.toml file. + ''; + }; + + listenPort = mkOption { + type = types.nullOr types.int; + default = null; + example = 8011; + description = "Listen on a specific IP port."; + }; + + }; + }; + + ###### implementation + config = mkIf cfg.enable { + + systemd.services.timeclonk = { + description = "timeclonk"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig.User = cfg.user; + serviceConfig.Group = cfg.group; + + script = '' + cd "/home/${cfg.user}" + mkdir -p timeclonk + cd timeclonk + echo "${cfg.settings}" > config.toml + RUST_LOG=info ${pkgs.timeclonk}/bin/timeclonk-server -c config.toml + ''; + }; + + users.groups = { + ${cfg.group} = { }; + }; + + users.users = lib.mkMerge [ + (lib.mkIf (cfg.user == "timeclonk") { + ${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = "/home/${cfg.user}"; + createHome = true; + }; + }) + ]; + }; +} diff --git a/server/config.toml b/server/config.toml index 4c8bf6f..3d7ae39 100644 --- a/server/config.toml +++ b/server/config.toml @@ -1,10 +1,10 @@ ip = "0.0.0.0" -port = 8010 +port = 8000 createdirs = false [orgauth_config] db = "./timeclonk.db" -mainsite = "http://localhost:8010" +mainsite = "http://localhost:8000" appname = "TIMECLONK" emaildomain = "timeclonk.com" admin_email = "admin@admin.admin" diff --git a/server/src/main.rs b/server/src/main.rs index a00f22e..fb19669 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -16,10 +16,11 @@ use config::Config; use log::{error, info}; use messages::{PublicMessage, ServerResponse, UserMessage}; use orgauth::data::WhatMessage; -use orgauth::endpoints::Callbacks; +use orgauth::util; use serde_json; use std::env; use std::error::Error; +use std::io::stdin; use std::path::PathBuf; use std::str::FromStr; use timer; @@ -114,11 +115,7 @@ async fn user( &item.data, req.connection_info() ); - let mut cb = Callbacks { - on_new_user: Box::new(sqldata::on_new_user), - on_delete_user: Box::new(sqldata::on_delete_user), - extra_login_data: Box::new(sqldata::extra_login_data_callback), - }; + let mut cb = sqldata::timeclonk_callbacks(); match orgauth::endpoints::user_interface( &session, @@ -150,11 +147,7 @@ async fn admin( &item.data, req.connection_info() ); - let mut cb = Callbacks { - on_new_user: Box::new(sqldata::on_new_user), - extra_login_data: Box::new(sqldata::extra_login_data_callback), - on_delete_user: Box::new(sqldata::on_delete_user), - }; + let mut cb = sqldata::timeclonk_callbacks(); match orgauth::endpoints::admin_interface_check( &session, &data.orgauth_config, @@ -238,9 +231,9 @@ async fn new_email(data: web::Data, req: HttpRequest) -> HttpResponse { fn defcon() -> Config { let oc = orgauth::data::Config { db: PathBuf::from("./timeclonk.db"), - mainsite: "http://localhost:8001".to_string(), + mainsite: "http://localhost:8000".to_string(), appname: "timeclonk".to_string(), - emaildomain: "localhost:8001".to_string(), + emaildomain: "localhost:8000".to_string(), admin_email: "admin@admin.admin".to_string(), regen_login_tokens: false, login_token_expiration_ms: Some(7 * 24 * 60 * 60 * 1000), // 7 days in milliseconds @@ -258,20 +251,9 @@ fn defcon() -> Config { } } -fn load_config() -> Config { - match orgauth::util::load_string("config.toml") { - Err(e) => { - error!("error loading config.toml: {:?}", e); - defcon() - } - Ok(config_str) => match toml::from_str(config_str.as_str()) { - Ok(c) => c, - Err(e) => { - error!("error loading config.toml: {:?}", e); - defcon() - } - }, - } +pub fn load_config(filename: &str) -> Result> { + info!("loading config: {}", filename); + Ok(toml::from_str(&util::load_string(&filename)?)?) } fn main() { @@ -287,22 +269,65 @@ async fn err_main() -> Result<(), Box> { .version("1.0") .author("Ben Burdette") .about("team time clock web server") + // .arg( + // Arg::with_name("export") + // .short("e") + // .long("export") + // .value_name("FILE") + // .help("Export database to json") + // .takes_value(true), + // ) + .arg( + Arg::with_name("config") + .short("c") + .long("config") + .value_name("FILE") + .help("specify config file") + .takes_value(true), + ) .arg( - Arg::with_name("export") - .short("e") - .long("export") + Arg::with_name("write_config") + .short("w") + .long("write_config") .value_name("FILE") - .help("Export database to json") + .help("write default config file") + .takes_value(true), + ) + .arg( + Arg::with_name("promote_to_admin") + .short("p") + .long("promote_to_admin") + .value_name("user name") + .help("grant admin privileges to user") + .takes_value(true), + ) + .arg( + Arg::with_name("create_admin_user") + .short("a") + .long("create_admin_user") + .value_name("user name") + .help("create new admin user") .takes_value(true), ) .get_matches(); + // writing a config file? + if let Some(filename) = matches.value_of("write_config") { + util::write_string(filename, toml::to_string_pretty(&defcon())?.as_str())?; + info!("default config written to file: {}", filename); + return Ok(()); + } + + // specifying a config file? otherwise try to load the default. + let mut config = match matches.value_of("config") { + Some(filename) => load_config(filename)?, + None => load_config("config.toml")?, + }; + // are we exporting the DB? match matches.value_of("export") { Some(_exportfile) => { // do that exporting... - let config = load_config(); - sqldata::dbinit( config.orgauth_config.db.as_path(), config.orgauth_config.login_token_expiration_ms, @@ -323,8 +348,6 @@ async fn err_main() -> Result<(), Box> { info!("server init!"); - let mut config = load_config(); - if config.static_path == None { for (key, value) in env::vars() { if key == "TIMECLONK_STATIC_PATH" { @@ -351,6 +374,41 @@ async fn err_main() -> Result<(), Box> { } }); + // promoting a user to admin? + if let Some(uid) = matches.value_of("promote_to_admin") { + let conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + let mut user = orgauth::dbfun::read_user_by_name(&conn, uid)?; + user.admin = true; + orgauth::dbfun::update_user(&conn, &user)?; + + info!("promoted user {} to admin", uid); + return Ok(()); + } + + // creating an admin user? + if let Some(username) = matches.value_of("create_admin_user") { + // prompt for password. + println!("Enter password for admin user '{}':", username); + let mut pwd = String::new(); + stdin().read_line(&mut pwd)?; + let mut cb = sqldata::timeclonk_callbacks(); + + let conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + // make new registration i + let rd = orgauth::data::RegistrationData { + uid: username.to_string(), + pwd: pwd.trim().to_string(), + email: "".to_string(), + }; + + println!("rd: {:?}", rd); + + orgauth::dbfun::new_user(&conn, &rd, None, None, true, None, &mut cb.on_new_user)?; + + println!("admin user created: {}", username); + return Ok(()); + } + let c = config.clone(); HttpServer::new(move || { let staticpath = c.static_path.clone().unwrap_or(PathBuf::from("static/")); diff --git a/server/src/sqldata.rs b/server/src/sqldata.rs index 2d0040b..4503b64 100644 --- a/server/src/sqldata.rs +++ b/server/src/sqldata.rs @@ -7,12 +7,21 @@ use crate::migrations as tm; use barrel::backend::Sqlite; use log::info; use orgauth::data::RegistrationData; +use orgauth::endpoints::Callbacks; use orgauth::util::now; use rusqlite::{params, Connection}; use std::path::Path; use std::str::FromStr; use std::time::Duration; +pub fn timeclonk_callbacks() -> Callbacks { + Callbacks { + on_new_user: Box::new(on_new_user), + extra_login_data: Box::new(extra_login_data_callback), + on_delete_user: Box::new(on_delete_user), + } +} + pub fn on_new_user( conn: &Connection, _rd: &RegistrationData,