From 61d1aa04d8d44b17bfe6dace90088669fc6c3df8 Mon Sep 17 00:00:00 2001 From: Kyle McFarland Date: Wed, 31 Jan 2018 00:51:07 -0600 Subject: Initial import * Registration system's almost done * Just part way through implementing tasks So not much done yet, but it's a start. --- .gitignore | 6 + bower.json | 23 +++ bylaws.php | 6 + common/config.php.example | 34 ++++ common/config_cls.php | 26 +++ common/db.php | 310 ++++++++++++++++++++++++++++ common/db_classes.php | 346 ++++++++++++++++++++++++++++++++ common/tables/dividend_credits.php | 36 ++++ common/tables/members.php | 47 +++++ common/tables/messages.php | 5 + common/tables/task_claims.php | 32 +++ common/tables/task_dividend_credits.php | 32 +++ common/tables/tasks.php | 45 +++++ common/tables/tcc_history.php | 31 +++ composer.json | 7 + css/mcoop.css | 18 ++ index.php | 14 ++ js/tasks.js | 46 +++++ login.php | 32 +++ logout.php | 15 ++ profile.php | 115 +++++++++++ register.php | 57 ++++++ tasks.php | 13 ++ tasks_api.php | 28 +++ tmpl/base.tmpl | 64 ++++++ tmpl/bylaws.tmpl | 0 tmpl/index.tmpl | 19 ++ tmpl/login.tmpl | 18 ++ tmpl/logout.tmpl | 9 + tmpl/profile.tmpl | 24 +++ tmpl/register.tmpl | 30 +++ tmpl/tasks.tmpl | 29 +++ tmpl/validate.tmpl | 19 ++ tmpl/validation_email.tmpl | 9 + todo.txt | 25 +++ validate.php | 40 ++++ 36 files changed, 1610 insertions(+) create mode 100644 .gitignore create mode 100644 bower.json create mode 100644 bylaws.php create mode 100644 common/config.php.example create mode 100644 common/config_cls.php create mode 100644 common/db.php create mode 100644 common/db_classes.php create mode 100644 common/tables/dividend_credits.php create mode 100644 common/tables/members.php create mode 100644 common/tables/messages.php create mode 100644 common/tables/task_claims.php create mode 100644 common/tables/task_dividend_credits.php create mode 100644 common/tables/tasks.php create mode 100644 common/tables/tcc_history.php create mode 100644 composer.json create mode 100644 css/mcoop.css create mode 100644 index.php create mode 100644 js/tasks.js create mode 100644 login.php create mode 100644 logout.php create mode 100644 profile.php create mode 100644 register.php create mode 100644 tasks.php create mode 100644 tasks_api.php create mode 100644 tmpl/base.tmpl create mode 100644 tmpl/bylaws.tmpl create mode 100644 tmpl/index.tmpl create mode 100644 tmpl/login.tmpl create mode 100644 tmpl/logout.tmpl create mode 100644 tmpl/profile.tmpl create mode 100644 tmpl/register.tmpl create mode 100644 tmpl/tasks.tmpl create mode 100644 tmpl/validate.tmpl create mode 100644 tmpl/validation_email.tmpl create mode 100644 todo.txt create mode 100644 validate.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a90e814 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bower_components +vendor +setup_bower.sh +composer.lock +cache +common/config.php diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..95fb3bc --- /dev/null +++ b/bower.json @@ -0,0 +1,23 @@ +{ + "name": "mcoop", + "authors": [ + "Kyle McFarland " + ], + "description": "mcoop is a simple Co-Op management webapp", + "main": "index.html", + "license": "Apache-2.0", + "homepage": "http://wiki.gpio.ca/wiki/mcoop", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "bootstrap": "^4.0.0", + "tether": "^1.4.3", + "jquery": "^3.3.1" + } +} diff --git a/bylaws.php b/bylaws.php new file mode 100644 index 0000000..9df06a9 --- /dev/null +++ b/bylaws.php @@ -0,0 +1,6 @@ + diff --git a/common/config.php.example b/common/config.php.example new file mode 100644 index 0000000..14cb84b --- /dev/null +++ b/common/config.php.example @@ -0,0 +1,34 @@ +", + "[http://mcoop.example.com/]", + "[Site Name]" +); + +$db = new Database($config); + +if (!isset($logout)) + $logout = false; + +$sess_info = new SessionInfo($db, $logout); + +$twig_loader = new \Twig_Loader_Filesystem("./tmpl/"); +$twig = new \Twig_Environment($twig_loader, array( + "cache" => "./cache/", + "auto_reload" => true +)); + +?> diff --git a/common/config_cls.php b/common/config_cls.php new file mode 100644 index 0000000..b4174be --- /dev/null +++ b/common/config_cls.php @@ -0,0 +1,26 @@ +pdo_connstring = $pdo_connstring; + $this->db_username = $db_uname; + $this->db_password = $db_passwd; + $this->recaptcha_sitekey = $recaptcha_sitekey; + $this->recaptcha_secret = $recaptcha_sec; + $this->email_from_address = $email_from_addr; + $this->webapp_base_uri = $webapp_base_uri; + $this->website_name = $website_name; + } +} + +?> diff --git a/common/db.php b/common/db.php new file mode 100644 index 0000000..71dbf81 --- /dev/null +++ b/common/db.php @@ -0,0 +1,310 @@ + $members_table_decl, + "dividend_credits" => $dividend_credits_table_decl, + "tasks" => $tasks_table_decl, + "task_claims" => $task_claims_table_decl, + "task_dividend_credits" => $task_dividend_credits_table_decl, + "tcc_history" => $tcc_history_table_decl +); + +class UnknownMember extends \Exception { + public $searched = null; + public $value = null; + + function __construct($searched, $value) { + $this->searched = $searched; + $this->value = $value; + + $message = "Unable to find a member with $searched = $value"; + parent::__construct($message); + } +} + +class RegistrationError extends \Exception { + public $invalid_part = null; + public $reason = null; + + function __construct($invalid_part, $reason) { + $this->invalid_part = $invalid_part; + $this->reason = $reason; + + $message = "Registration Error ($invalid_part): $reason"; + parent::__construct($message); + } +} + +class LoginError extends \Exception {} + +class Database { + public $config = null; + public $conn = null; + public $target_tableversions = array( + "unexpected_errors" => 1, + ); + public $statements = array(); + public $upgraders = array(); + public $table_decls = array(); + + function __construct($config) { + $this->config = $config; + global $table_decls; + $this->table_decls = $table_decls; + $this->connect(); + } + + function connect() { + // XXX/TODO: the exceptions from this need to be caught, see the warning on http://php.net/manual/en/pdo.connections.php + $this->conn = new \PDO( + $this->config->pdo_connstring, + $this->config->db_username, + $this->config->db_password, + array(\PDO::ATTR_PERSISTENT => true) + ); + $this->create_tableversions_table(); + $this->create_unexpected_errors_table(); + // TODO: make table creation optional for everything except tableversions and unexpected_errors + // (you can change the $create argument to setup_table_decls to false for that now) + $this->setup_table_decls(); + $this->auto_upgrade_all(); + //$this->create_members_table(); + //$this->upgraders["members"] = new MembersUpgrader($this->conn); + //$this->auto_upgrade_all(); + //var_dump($this->conn); + } + + function setup_table_decls($create=true) { + $conn = $this->conn; + foreach ($this->table_decls as $tname => $td) { + if ($create) { + $td->create_table($this); + } + $td->set_statements($this); + $clsname = $td->upgrader_classname; + $this->upgraders[$tname] = new $clsname($conn); + } + } + + function get_cur_table_version($table_name) { + $this->statements["get_tableversion"]->execute(array(":name" => $table_name)); + $x = $this->statements["get_tableversion"]->fetchAll(\PDO::FETCH_COLUMN); + $ver = (int)$x[0]; + return $ver; + } + + function auto_upgrade($upgrader) { + // This shouldn't be called until at the very least after create_tableversions_table, since it adds set_tableversion to statements + $table_name = $upgrader->table_name; + $old_ver = $this->get_cur_table_version($table_name); + $new_ver = $this->table_decls[$table_name]->newest_tableversion; + if ($old_ver < $new_ver) { + error_log("upgrading $table_name from $old_ver to $new_ver"); + // XXX/TODO: this is assuming it doesn't error, going to get really ugly if it does + $upgraded_to = $upgrader->upgrade($old_ver, $new_ver); + $this->statements["set_tableversion"]->execute(array(":table_name" => $table_name, ":version" => $upgraded_to)); + //print_r($upgraded_to); + return $upgraded_to; + } + } + + function auto_upgrade_all() { + foreach ($this->upgraders as $table_name => $upgrader) { + $this->auto_upgrade($upgrader); + } + } + + function create_tableversions_table() { + $conn = $this->conn; + $conn->exec("CREATE TABLE IF NOT EXISTS `table_versions` (`name` TEXT CHARACTER SET utf8 NOT NULL, `version` INT, PRIMARY KEY (`name`(1024)));"); + $this->statements["set_tableversion"] = $conn->prepare("INSERT INTO table_versions VALUES (:table_name, :version) on duplicate key UPDATE version=:version"); + $this->statements["get_tableversion"] = $conn->prepare("SELECT version FROM table_versions WHERE name = :name"); + } + + function create_unexpected_errors_table() { + // TODO: this table needs a datetime field so you know when the error happened + $conn = $this->conn; + $conn->exec("CREATE TABLE `unexpected_errors` (`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `action` TEXT CHARACTER SET utf8 NOT NULL, `error_sql_code` CHAR(5) CHARACTER SET ascii NOT NULL, `error_server_code` INT, `error_server_description` TEXT CHARACTER SET utf8);"); + $err_info = $conn->errorInfo(); + //var_dump($err_info); + if ($err_info[0] == "00000") { + $this->statements["set_tableversion"]->execute(array( + ":table_name" => "unexpected_errors", + ":version" => $this->target_tableversions["unexpected_errors"] + )); + } + $this->statements["insert_unexpected_error"] = $conn->prepare("INSERT INTO unexpected_errors (action, error_sql_code, error_server_code, error_server_description) VALUES (:action, :sql_code, :server_code, :server_description)"); + } + + function create_members_table() { + $conn = $this->conn; + $conn->exec( + "CREATE TABLE `members` ( +`userid` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, +`username` TEXT CHARACTER SET utf8 NOT NULL, +`email` TEXT CHARACTER SET utf8 NOT NULL, +`last_updated_table_version` INT NOT NULL, +`full_name` TEXT CHARACTER SET utf8, +`validation_code` CHAR(64) CHARACTER SET ascii, +`validated` BOOL DEFAULT false, +`full_member` BOOL DEFAULT false, +`argon2_password_hash` VARCHAR(256) CHARACTER SET ascii, +`reset_password_hash` VARCHAR(256) CHARACTER SET ascii, +`reset_requested` BOOL DEFAULT false, +UNIQUE KEY (`username`(256)), +UNIQUE KEY (`email`(256)) +);"); + $err_info = $conn->errorInfo(); + // the SQL errorcode for a table already existing seems to be 42S01 + // but I think we don't need to check for it specifically here, + // just make sure there was no error + //print_r($err_info); + //var_dump($err_info); + if ($err_info[0] == "00000") { + $this->statements["set_tableversion"]->execute(array( + ":table_name" => "members", + ":version" => $this->target_tableversions["members"] + )); + } else if ($err_info[0] != "42S01") { + $this->statements["insert_unexpected_error"]->execute(array( + ":action" => "create_members_table", + ":sql_code" => $err_info[0], + ":server_code" => $err_info[1], + ":server_description" => $err_info[2] + )); + } + $this->statements["register_member"] = $conn->prepare("INSERT INTO members (username, email, last_updated_table_version, full_name, argon2_password_hash) VALUES (:username, :email, :version, :full_name, :argon2_phash)"); + $this->statements["get_member_passhash_by_username"] = $conn->prepare("SELECT argon2_password_hash FROM members WHERE username = :username"); + $this->statements["get_member_by_username"] = $conn->prepare("SELECT * FROM members WHERE username = ?"); + $this->statements["get_member_by_email"] = $conn->prepare("SELECT * FROM members WHERE email = ?"); + $this->statements["get_members_by_uname_or_email"] = $conn->prepare("SELECT * FROM members WHERE username = :username OR email = :email"); + } + + function validate_username($username) { + if (!preg_match("/^[a-zA-Z0-9]{3,}$/", $username)) { + throw new RegistrationError("username", "Invalid username (must be at least 3 characters long, only english alphanumeric characters are allowed"); + } + $conn = $this->conn; + $username_check = $conn->prepare("SELECT * FROM members WHERE username = :username"); + $username_check->execute(array(":username" => $username)); + $x = $username_check->fetchAll(\PDO::FETCH_COLUMN); + if ($x) { + throw new RegistrationError("username", "Username already registered"); + } + return $username; + } + + function validate_email($email) { + $filt_email = filter_var($email, FILTER_VALIDATE_EMAIL); + if ($filt_email != $email) { + throw new RegistrationError("email", "Doesn't validate as a valid email address"); + } + $conn = $this->conn; + $email_check = $conn->prepare("SELECT * FROM members where email = :email"); + $email_check->execute(array(":email" => $filt_email)); + $x = $email_check->fetchAll(\PDO::FETCH_COLUMN); + if ($x) { + throw new RegistrationError("email", "An account with that email address already exists, you can Reset your password if you need a new password"); + } + return $filt_email; + } + + function validate_fullname($full_name) { + $filt_fname = filter_var($full_name, FILTER_SANITIZE_STRING, array( + "flags" => FILTER_FLAG_NO_ENCODE_QUOTES | FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK + )); + return $filt_fname; + } + + // TODO: reset password functionality that sends a randomly generated password to your email address + function register($username, $email, $password, $full_name, $validate_email=false, $email_twig_env=null) { + $filt_email = $this->validate_email($email); + $filt_username = $this->validate_username($username); + $filt_fname = $this->validate_fullname($full_name); + $statements = $this->statements; + $conn = $this->conn; + $pass_hash = password_hash($password, PASSWORD_ARGON2I); + $conn->beginTransaction(); + $success = $statements["register_member"]->execute(array( + ":username" => $username, + ":email" => $filt_email, + ":version" => $this->table_decls["members"]->newest_tableversion, + ":full_name" => $filt_fname, + ":argon2_phash" => $pass_hash + )); + // TODO: check that it was actually successful + if (!$success) { + $einfo = $statements["register_member"]->errorInfo(); + error_log("mcoop member registration failed inserting into the database: " . var_export($einfo, true) . " ($username, $email, $password, $pass_hash, $full_name)"); + throw new RegistrationError("database", "registration failed internally, contact the admin"); + } + if ($validate_email) { + $this->send_validation_email($username, $filt_email, $email_twig_env); + } + $conn->commit(); + // TODO: setup the login session here too, not sure if session_start should be called in the script setup code actually instead of just here (and just set the variable in $_SESSION here) + //session_start(); + $_SESSION["logged_in"] = true; + $_SESSION["login_username"] = $username; + // TODO: include login time in here and other things so you can invalidate sessions/log people out if a password reset gets requested or anything else + //session_write_close(); + } + + function send_validation_email($username, $email, $email_twig_env) { + // generate a validation code, just use some random bytes, hash them and use the hexdigest for now + $rbytes = random_bytes(64); + $h = hash("sha256", $rbytes, false); + $conn = $this->conn; + $stmt = $conn->prepare("UPDATE members SET validation_code = :vcode WHERE username = :username AND email = :email"); + $success = $stmt->execute(array( + ":vcode" => $h, + ":username" => $username, + ":email" => $email + )); + if (!$success) { + $einfo = $stmt->errorInfo(); + error_log("mcoop: send_validation_email failed updating members: " . var_export($einfo, true) . " ($h, $username, $email)"); + throw new RegistrationError("database", "internal error generating validation code, contact the admin"); + } + $headers = "From: " . $this->config->email_from_address; + $sitename = $this->config->website_name; + $vurl = urljoin($this->config->webapp_base_uri, "validate.php?un=$username&vcode=$h"); + $deactivate_url = urljoin($this->config->webapp_base_uri, "deactivate.php?un=$username&vcode=$h"); + $body = $email_twig_env->render("validation_email.tmpl", array( + "email" => $email, + "website_name" => $sitename, + "vurl" => $vurl, + "deactivate_url" => $deactivate_url + )); + mail($email, "Activate your $sitename account", $body, $headers); + } + + function login($username, $password, $session_info) { + $m = DBMember::load_by($this, "username", $username); + // TODO: password reset logic + if (password_verify($password, $m->argon2_password_hash)) { + $_SESSION["logged_in"] = true; + $_SESSION["login_username"] = $username; + $session_info->re_init(); + return true; + } else { + throw new LoginError("Invalid Password"); + } + } + + function get_members($only_verified=true) { + + } +} +?> diff --git a/common/db_classes.php b/common/db_classes.php new file mode 100644 index 0000000..dde26eb --- /dev/null +++ b/common/db_classes.php @@ -0,0 +1,346 @@ +table_name = $table_name; + $this->sql_error_code = $sql_error_code; + $this->server_error_code = $server_error_code; + $this->server_error_message = $server_error_message; + + $message = "Failed to create table '$table_name': ($sql_error_code, $server_error_code, $server_error_message)"; + parent::__construct($message); + } +} + +interface TableUpgrader { + function upgrade($from_version, $to_version); +} + +class BaseIncrementalTableUpgrader implements TableUpgrader { + public $upgrade_method_names = array(); + public $table_name = null; + public $conn = null; + + function upgrade($from_version, $to_version) { + // first make sure a full update is possible with what's in method_names + $full_upgrade_path = true; + for ($i = $from_version + 1; $i <= $to_version; $i++) { + $method_name = $this->upgrade_method_names[$i]; + if (!is_callable(array($this, $method_name))) { + $full_upgrade_path = false; + error_log(get_class($this) . "::upgrade_method_names for $i not callable: " . var_export($method_name, true)); + } + } + if (!$full_upgrade_path) { + die("couldn't upgrade the database, check error_log"); + } else { + $highest_version = $from_version; + for ($i = $from_version + 1; $i <= $to_version; $i++) { + $method_name = $this->upgrade_method_names[$i]; + // XXX/TODO: this really needs trying around, not sure how to deal with upgrades that fail though, might want a downgrade function to rollback if you fail somewhere between, or just leave it in the middle state + $this->$method_name(); + $highest_version = $i; + } + return $highest_version; + } + } +} + +class SimpleTableDecl { + public $table_name = null; + public $statements_sql = array(); + // it might make sense to make this an array in the future, but for now just make it a simple string for creating the newest version + public $create_table_sql = null; + public $newest_tableversion = null; // int + public $upgrader_classname = null; // string, this should be fully namespaced so it can be created dynamically + // TODO: table dependencies variables for creating and/or for the statements? + + function create_table($db) { + $conn = $db->conn; + $conn->exec($this->create_table_sql); + $err_info = $conn->errorInfo(); + // the SQL errorcode for a table already existing seems to be 42S01 + // but I think we don't need to check for it specifically here, + // just make sure there was no error + //print_r($err_info); + //var_dump($err_info); + if ($err_info[0] == "00000") { + $db->statements["set_tableversion"]->execute(array( + ":table_name" => $this->table_name, + ":version" => $this->newest_tableversion + )); + } else if ($err_info[0] != "42S01") { + $db->statements["insert_unexpected_error"]->execute(array( + ":action" => "create_table($this->table_name)", + ":sql_code" => $err_info[0], + ":server_code" => $err_info[1], + ":server_description" => $err_info[2] + )); + throw new TableCreationError($this->table_name, $err_info[0], $err_info[1], $err_info[2]); + } + } + + function set_statements($db) { + foreach ($this->statements_sql as $name => $sql) { + if (array_key_exists($name, $db->statements)) { + // TODO: I should probably also raise an exception here + error_log("mcoop: table_decl for $this->table_name: key $name already exists in the db's statements, not replacing"); + } else { + $db->statements[$name] = $db->conn->prepare($sql); + }; + } + } + + function __construct($table_name, $statements_sql, $create_table_sql, $newest_tableversion, $upgrader_classname) { + $this->table_name = $table_name; + $this->statements_sql = $statements_sql; + $this->create_table_sql = $create_table_sql; + $this->newest_tableversion = $newest_tableversion; + $this->upgrader_classname = $upgrader_classname; + } +} + +class SimpleDBMixin { + static function load_mult_with_statement($db, $st_name, $varray, $clsname=null, $keyprop=null) { + if ($clsname == null) + $clsname = get_called_class(); + $st = $db->statements[$st_name]; + $st->execute($varray); + $ret = array(); + $last_obj = null; + do { + $last_obj = $st->fetchObject($clsname); + //var_dump($st_name, $varray, $clsname, $keyprop, $st, $last_obj); + //var_dump($clsname); + if ($last_obj != FALSE) { + if ($keyprop) { + $key = $last_obj->$keyprop; + // TODO: deal with duplicates + $ret[$key] = $last_obj; + } else { + $ret[] = $last_obj; + } + } + } while ($last_obj != FALSE); + return $ret; + } +} + +// TODO: all of these classes need a get_json function so you don't serialize the entire thing (or look at how to override json +// serialization for classes/objects again) + +class DBMember extends SimpleDBMixin { + // from the members table: + //public $db = null; + public $userid = null; + public $username = null; + public $email = null; + public $last_updated_table_version = null; + public $full_name = null; + public $validation_code = null; + public $validated = null; + public $full_member = null; + public $argon2_password_hash = null; + public $reset_password_hash = null; + public $reset_requested = null; + // from the shares table: + //public $shares = array(); + // generated in __construct + public $display_name = null; + + function __construct() { + if (isset($this->full_name) && $this->full_name) + $this->display_name = $this->full_name; + else + $this->display_name = $this->username; + } + + static function load_by($db, $by, $value) { + // by should be either "username" or "email" here + $st = $db->statements["get_member_by_$by"]; + $st->execute(array($value)); + $self = $st->fetchObject("\mcoop\DBMember"); + if (!$self) { + throw new UnknownMember($by, $value); + } + //$self->db = $db; + return $self; + } + + static function load_public_info($db, $userid) { + $ret = DBMember::load_mult_with_statement($db, "get_public_member_info_by_userid", array(":userid" => $userid)); + if (!$ret) { + throw new UnknownMember("userid", $userid); + } else { + //$ret[0]->db = $db; + return $ret[0]; + } + } +} + +class TccHistoryEntry extends SimpleDBMixin { + // tcc_history table values + public $from_tdc_id = null; // this should probably index into a table in the task object + public $to_claim_id = null; // contains this object, sometimes (the tdc could also contain them, hmm) + public $action = null; + public $credits = null; + public $last_updated_table_version = null; + + static function get_history_by_claimid($db, $claimid, $clsname=null, $keyprop=null) { + return TccHistoryEntry::load_mult_with_statement($db, "get_tcc_history_by_claimid", array(":claimid" => $claimid), $clsname, $keyprop); + } + + static function get_history_by_tdc_id($db, $tdc_id, $clsname=null, $keyprop=null) { + return TccHistoryEntry::load_mult_with_statement($db, "get_tcc_history_by_tdc_id", array(":tdc_id" => $tdc_id), $clsname, $keyprop); + } +} + + +class SimpleTaskClaim extends SimpleDBMixin { + // claims table values + public $claim_id = null; + public $task_id = null; + public $userid = null; + public $last_updated_table_version = null; + public $description = null; + // loaded via. userid + public $member_public = null; + // loaded from tcc_history, see $this->load_tcc_history + public $tcc_history = null; + + static function get_claims_by_taskid($db, $taskid, $clsname=null, $keyprop=null) { + return SimpleTaskClaim::load_mult_with_statement($db, "get_task_claims_by_taskid", array(":task_id" => $taskid), $clsname, $keyprop); + } + + function load_member($db) { + $this->member_public = DBMember::load_public_info($db, $this->userid); + } + + function load_tcc_history($db) { + $this->tcc_history = TccHistoryEntry::get_history_by_claimid($db, $this->claim_id); + } + + function load_all($db) { + $this->load_member($db); + $this->load_tcc_history($db); + } +} + +class SimpleTdc extends SimpleDBMixin { + // tdc table values + public $tdc_id = null; + public $task_id = null; + public $posted_userid = null; + public $posted_by_coop = null; + public $total_credits = null; + public $remaining_credits = null; + public $last_updated_table_version = null; + // loaded via. posted_userid, will be null after $this->load_member() if posted_by_coop is true + public $member_public = null; + // Set to either $member_public->display_name or "By the Co-Operative" + // TODO: maybe use the website_name in $config with posted_by_coop + public $member_name = null; + + static function get_tdcs_by_taskid($db, $taskid, $clsname=null, $keyprop=null) { + return SimpleTdc::load_mult_with_statement($db, "get_tdcs_by_taskid", array(":task_id" => $taskid), $clsname, $keyprop); + } + + function load_member($db) { + if ($this->posted_by_coop) { + $this->member_name = "By the Co-Operative"; + } else { + $this->member_public = DBMember::load_public_info($db, $this->posted_userid); + $this->member_name = $this->member_public->display_name; + } + } + + function load_all($db) { + $this->load_member($db); + } +} + +class SimpleTask extends SimpleDBMixin { + // tasks table values + public $taskid = null; + public $admin_userid = null; + public $last_updated_table_version = null; + public $title = null; + public $description = null; + public $state = null; + // loaded via. admin_userid (DBMember with userid, username and full_name filled in, see $this->load_admin -- using a join might also be a good idea but then you have to replicate the display_name code or change DBMember's construct to pass in args directly) + public $member_public = null; + // loaded from task_dividend_credits, see $this->load_tdcs() + public $tdcs = null; + // loaded from task_claims, see $this->load_claims() or $this->load_full() + public $claims = null; + + static function get_all_simple($db, $clsname=null, $keyprop=null) { + return SimpleTask::load_mult_with_statement($db, "get_all_tasks_simple", array(), $clsname, $keyprop); + } + + static function get_all_full($db, $clsname=null, $keyprop=null) { + return SimpleTask::load_mult_with_statement($db, "get_all_tasks", array(), $clsname, $keyprop); + } + + function load_admin($db) { + $this->member_public = DBMember::load_public_info($db, $this->admin_userid); + } + + function load_claims($db, $full) { + $this->claims = SimpleTaskClaim::get_claims_by_taskid($db, $this->taskid, null, "claim_id"); + if ($full) { + foreach ($this->claims as $k => $claim) { + $claim->load_all($db); + } + } + } + + function load_tdcs($db, $full) { + $this->tdcs = SimpleTdc::get_tdcs_by_taskid($db, $this->taskid, null, "tdc_id"); + if ($full) { + foreach ($this->tdcs as $k => $tdc) { + $tdc->load_all($db); + } + } + } + + function load_all($db) { + $this->load_admin($db); + $this->load_tdcs($db, true); + $this->load_claims($db, true); + } +} + +class SessionInfo { + public $db = null; + public $login_member = null; + + function __construct($db, $logout) { + $this->db = $db; + session_start(); + if ($logout) { + $this->logout(); + } + $this->re_init(); + } + + function logout() { + session_unset(); + $this->login_member = null; + } + + function re_init() { + $this->login_member = null; + if (isset($_SESSION["logged_in"]) && $_SESSION["logged_in"] && isset($_SESSION["login_username"])) { + // TODO: validate login, either based on password, password reset time or possibly allow users to log out sessions explicitly (might require database session storage to make it easier) + $this->login_member = DBMember::load_by($this->db, "username", $_SESSION["login_username"]); + } + } +} +?> diff --git a/common/tables/dividend_credits.php b/common/tables/dividend_credits.php new file mode 100644 index 0000000..2f80ec0 --- /dev/null +++ b/common/tables/dividend_credits.php @@ -0,0 +1,36 @@ +conn->exec("ALTER TABLE dividend_ ADD COLUMN (test BOOL)"); + // No actual changes in this one, it was just to make sure the upgrader paths were working + } + + function __construct($conn) { + $this->conn = $conn; + $this->table_name = "dividend_credits"; + //$this->upgrade_method_names[2] = "from_1_to_2"; + } +} + +$dividend_credits_table_decl = new SimpleTableDecl( + "dividend_credits", + array( + "dividend_credits_ensure" => "INSERT IGNORE INTO dividend_credits (userid, year, credits, last_updated_table_version) VALUES (:userid, :year, 0, :table_ver)", + "add_to_dividend_credits" => "UPDATE dividend_credits SET credits=credits + :amount WHERE userid=:userid AND year=:year", + "remove_from_dividend_credits" => "UPDATE dividend_credits SET credits=credits - :amount WHERE userid=:userid AND year=:year", + "set_dividend_credits" => "UPDATE dividend_credits SET credits=:amount WHERE userid=:userid AND year=:year" + ), + "CREATE TABLE `dividend_credits` ( +`userid` INT NOT NULL, +`year` INT NOT NULL, +`credits` BIGINT NOT NULL, +`last_updated_table_version` INT NOT NULL, +PRIMARY KEY (`userid`, `year`) +);", + 1, + "\mcoop\DividendCreditsUpgrader" +); + diff --git a/common/tables/members.php b/common/tables/members.php new file mode 100644 index 0000000..bf1240d --- /dev/null +++ b/common/tables/members.php @@ -0,0 +1,47 @@ +conn->exec("ALTER TABLE members ADD COLUMN (test BOOL)"); + // No actual changes in this one, it was just to make sure the upgrader paths were working + } + + function __construct($conn) { + $this->conn = $conn; + $this->table_name = "members"; + $this->upgrade_method_names[2] = "from_1_to_2"; + } +} + +$members_table_decl = new SimpleTableDecl( + "members", + array( + "register_member" => "INSERT INTO members (username, email, last_updated_table_version, full_name, argon2_password_hash) VALUES (:username, :email, :version, :full_name, :argon2_phash)", + "get_member_passhash_by_username" => "SELECT argon2_password_hash FROM members WHERE username = :username", + "get_member_by_username" => "SELECT * FROM members WHERE username = ?", + "get_member_by_email" => "SELECT * FROM members WHERE email = ?", + "get_members_by_uname_or_email" => "SELECT * FROM members WHERE username = :username OR email = :email", + "get_public_member_info_by_userid" => "SELECT userid, username, full_name FROM members WHERE userid=:userid" + ), + "CREATE TABLE `members` ( +`userid` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, +`username` TEXT CHARACTER SET utf8 NOT NULL, +`email` TEXT CHARACTER SET utf8 NOT NULL, +`last_updated_table_version` INT NOT NULL, +`full_name` TEXT CHARACTER SET utf8, +`validation_code` CHAR(64) CHARACTER SET ascii, +`validated` BOOL DEFAULT false, +`full_member` BOOL DEFAULT false, +`argon2_password_hash` VARCHAR(256) CHARACTER SET ascii, +`reset_password_hash` VARCHAR(256) CHARACTER SET ascii, +`reset_requested` BOOL DEFAULT false, +UNIQUE KEY (`username`(256)), +UNIQUE KEY (`email`(256)) +);", + 2, + "\mcoop\MembersUpgrader" +); + +?> diff --git a/common/tables/messages.php b/common/tables/messages.php new file mode 100644 index 0000000..65d821d --- /dev/null +++ b/common/tables/messages.php @@ -0,0 +1,5 @@ + diff --git a/common/tables/task_claims.php b/common/tables/task_claims.php new file mode 100644 index 0000000..4f30964 --- /dev/null +++ b/common/tables/task_claims.php @@ -0,0 +1,32 @@ +conn = $conn; + $this->table_name = "task_claims"; + } +} + +$task_claims_table_decl = new SimpleTableDecl( + "task_claims", + array( + "create_task_claim" => "INSERT INTO task_claims (task_id, userid, last_updated_table_version, description) VALUES (:task_id, :userid, :table_ver, :description)", + // something that might be interesting is also keying on WHERE userid=:userid so the user that created it's the only one that can update it, I'll probably do something with perms though (that's a TODO) + "update_task_claim_desc" => "UPDATE task_claims SET description=:desc WHERE claim_id=:claim_id", + "get_task_claim_ids_by_taskid" => "SELECT claim_id FROM task_claims WHERE task_id=:task_id", + "get_task_claims_by_taskid" => "SELECT * FROM task_claims WHERE task_id=:task_id" + ), + "CREATE TABLE `task_claims` ( +`claim_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, +`task_id` INT NOT NULL, +`userid` INT NOT NULL, +`last_updated_table_version` INT NOT NULL, +`description` TEXT CHARACTER SET utf8 +);", + 1, + "\mcoop\TaskClaimsUpgrader" +); + +?> diff --git a/common/tables/task_dividend_credits.php b/common/tables/task_dividend_credits.php new file mode 100644 index 0000000..d6a0ce7 --- /dev/null +++ b/common/tables/task_dividend_credits.php @@ -0,0 +1,32 @@ +conn = $conn; + $this->table_name = "task_dividend_credits"; + } +} + +$task_dividend_credits_table_decl = new SimpleTableDecl( + "task_dividend_credits", + array( + "create_new_tdc" => "INSERT INTO task_dividend_credits (task_id, posted_userid, posted_by_coop, total_credits, remaining_credits, last_updated_table_version) VALUES (:task_id, :userid, :coop_post, :credits, :credits, :table_ver)", + "tdc_remove_credits" => "UPDATE task_dividend_credits SET remaining_credits=remaining_credits - :amount WHERE tdc_id = :tdc_id", + "get_tdcs_by_taskid" => "SELECT * FROM task_dividend_credits WHERE task_id=:task_id" + ), + "CREATE TABLE `task_dividend_credits` ( +`tdc_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, +`task_id` INT NOT NULL, +`posted_userid` INT NOT NULL, +`posted_by_coop` BOOL NOT NULL, +`total_credits` BIGINT NOT NULL, +`remaining_credits` BIGINT NOT NULL, +`last_updated_table_version` INT NOT NULL +);", + 1, + "\mcoop\TaskDividendCreditsUpgrader" +); + +?> diff --git a/common/tables/tasks.php b/common/tables/tasks.php new file mode 100644 index 0000000..af754d2 --- /dev/null +++ b/common/tables/tasks.php @@ -0,0 +1,45 @@ +conn->exec("ALTER TABLE tasks ADD COLUMN (test BOOL)"); + // No actual changes in this one, it was just to make sure the upgrader paths were working + } + + function __construct($conn) { + $this->conn = $conn; + $this->table_name = "tasks"; + //$this->upgrade_method_names[2] = "from_1_to_2"; + } +} + +$tasks_table_decl = new SimpleTableDecl( + "tasks", + array( + "create_task" => "INSERT INTO tasks (admin_userid, last_updated_table_version, title, description) VALUES (:userid, :table_ver, :title, :desc)", + // TODO: dynamically generating these queries based on what fields are modified instead of statically would be a good idea + "update_task_title" => "UPDATE tasks SET title=:title WHERE taskid=:taskid", + "update_task_td" => "UPDATE tasks SET title=:title, description=:desc WHERE taskid=:taskid", + "update_task_desc" => "UPDATE tasks SET description=:description WHERE taskid=:taskid", + "get_all_tasks_simple" => "SELECT taskid, admin_userid, last_updated_table_version, title, state FROM tasks", + "get_all_tasks" => "SELECT * FROM tasks" + ), + "CREATE TABLE `tasks` ( +`taskid` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, +`admin_userid` INT NOT NULL, +`last_updated_table_version` INT NOT NULL, +`title` TEXT CHARACTER SET utf8, +`description` TEXT CHARACTER SET utf8, +`state` ENUM ('open', 'closed') NOT NULL DEFAULT 'open' +);", + 1, + "\mcoop\TasksUpgrader" +); + +// TODO: tables for comments both on tasks and task claims would be a good idea (and probably necessary) + +// TODO: should either title be unique or maybe have an extra text task id kind of like bugzilla aliases? +// TODO: need a claims table, and a table listing dividend credits available for each task, also a notification table for the task admin so they know when someone wants to add dividend credits to a closed task so they can reopen the task if deemed appropriate (also comments like any bugtracker?) +?> diff --git a/common/tables/tcc_history.php b/common/tables/tcc_history.php new file mode 100644 index 0000000..362d702 --- /dev/null +++ b/common/tables/tcc_history.php @@ -0,0 +1,31 @@ +conn = $conn; + $this->table_name = "tcc_history"; + } +} + +$tcc_history_table_decl = new SimpleTableDecl( + "tcc_history", + array( + "get_tcc_history_by_claimid" => "SELECT * FROM tcc_history WHERE to_claim_id=:claimid", + "get_tcc_history_by_tdc_id" => "SELECT * FROM tcc_history WHERE from_tdc_id=:tdc_id" + ), + "CREATE TABLE `tcc_history` ( +`from_tdc_id` INT NOT NULL, +`to_claim_id` INT NOT NULL, +`action` ENUM ('award', 'rescind') NOT NULL, +`credits` BIGINT NOT NULL, +`last_updated_table_version` INT NOT NULL +);", + 1, + "\mcoop\TccHistoryUpgrader" +); + +?> diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6eaac62 --- /dev/null +++ b/composer.json @@ -0,0 +1,7 @@ +{ + "minimum-stability": "dev", + "require": { + "busybee/urljoin": "*", + "twig/twig": "^3.0@dev" + } +} diff --git a/css/mcoop.css b/css/mcoop.css new file mode 100644 index 0000000..dbf51ee --- /dev/null +++ b/css/mcoop.css @@ -0,0 +1,18 @@ +div.main_container { + background-color: #a59565; + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; + margin-right: -7px; + margin-left: -7px; +} + +div.hidden_results_table { + visibility: hidden; +} + +div.results_table { + visibility: visible; +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..7d5b10c --- /dev/null +++ b/index.php @@ -0,0 +1,14 @@ +render("index.tmpl", array( + "danger_alerts" => array(), + "success_alerts" => array(), + "sess_info" => $sess_info, + "sitename" => $config->website_name +)); + +?> diff --git a/js/tasks.js b/js/tasks.js new file mode 100644 index 0000000..10483b3 --- /dev/null +++ b/js/tasks.js @@ -0,0 +1,46 @@ +function handle_search_results(res) { + //alert(res[0].title) + if (res) { + var tc = $("#table_container"); + tc.removeClass("hidden_results_table"); + // TODO: check if results_table is in tc's classes and add it if it isn't (jQuery stuff) + } + for (index in res) { + obj = res[index] + var tb = $("#results_tbody"); + tb.empty(); + //alert(tb); + var tr = document.createElement("tr"); + var id_td = document.createElement("td"); + var id_text = document.createTextNode(String(obj.taskid)); + id_td.appendChild(id_text); + var title_td = document.createElement("td"); + var title_text = document.createTextNode(obj.title); + title_td.appendChild(title_text); + var state_td = document.createElement("td"); + var state_text = document.createTextNode(obj.state); + state_td.appendChild(state_text); + var admin_td = document.createElement("td"); + var admin_text = document.createTextNode(obj.member_public.display_name); + admin_td.appendChild(admin_text); + var credits_total = 0; + for (tdc_index in obj.tdcs) { + tdc = obj.tdcs[tdc_index]; + credits_total += Number(tdc.remaining_credits); + } + alert(credits_total); + var credits_td = document.createElement("td"); + var credits_text = document.createTextNode(String(credits_total)); + credits_td.appendChild(credits_text); + tr.appendChild(id_td); + tr.appendChild(title_td); + tr.appendChild(state_td); + tr.appendChild(admin_td); + tr.appendChild(credits_td); + tb.append(tr); + } +} + +function get_all_tasks() { + fetch("tasks_api.php?action=get_all_tasks_simple").then(res => res.json()).then(res => handle_search_results(res)); +} diff --git a/login.php b/login.php new file mode 100644 index 0000000..d310163 --- /dev/null +++ b/login.php @@ -0,0 +1,32 @@ +login($_POST["username"], $_POST["passwd"], $sess_info); + $success_alerts[] = 'Successfully logged in, Continue'; + } catch (UnknownMember $e) { + $danger_alerts[] = "Failed to login, no such user"; + } catch (LoginError $e) { + $danger_alerts[] = "Failed to login, {$e->getMessage()}"; + } +} + +echo $twig->render("login.tmpl", array( + "danger_alerts" => $danger_alerts, + "success_alerts" => $success_alerts, + "sess_info" => $sess_info +)); + +?> diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..e8fbba1 --- /dev/null +++ b/logout.php @@ -0,0 +1,15 @@ +render("logout.tmpl", array( + "danger_alerts" => array(), + "success_alerts" => array(), + "sess_info" => $sess_info +)); +?> diff --git a/profile.php b/profile.php new file mode 100644 index 0000000..9240441 --- /dev/null +++ b/profile.php @@ -0,0 +1,115 @@ +login_member)) { + header("Location: " . urljoin($config->webapp_base_uri, "login.php")); + exit(); +} + +$danger_alerts = array(); +$success_alerts = array(); + +function check_full_name($db, $sess_info, $fullname) { + $filt_fullname = $db->validate_fullname($fullname); + return ((bool)$filt_fullname && ($filt_fullname != $sess_info->login_member->full_name)); +} + +function update_full_name($db, $sess_info, $fullname, $twig_env) { + global $success_alerts; + global $danger_alerts; + $filt_fullname = $db->validate_fullname($fullname); + $userid = $sess_info->login_member->userid; + $conn = $db->conn; + $st = $conn->prepare("UPDATE members SET full_name = ? WHERE userid = ?"); + $success = $st->execute(array($filt_fullname, $userid)); + if ($success) { + $success_alerts[] = "Full name updated successfully"; + } else { + $einfo = $st->errorInfo(); + error_log("mcoop: profile.php failed updating full_name: " . var_export($einfo, true) . " ($userid, $filt_fullname)"); + $danger_alerts[] = "Internal error, please contact the admin"; + } + return $success; +} + +function check_email($db, $sess_info, $email) { + return ((bool)$email && ($email != $sess_info->login_member->email)); +} + +function update_email($db, $sess_info, $email, $twig_env) { + global $success_alerts; + global $danger_alerts; + $success = false; + $conn = $db->conn; + try { + $filt_email = $db->validate_email($email); + $userid = $sess_info->login_member->userid; + $username = $sess_info->login_member->username; + $conn->beginTransaction(); + $st = $conn->prepare("UPDATE members SET email = ? , validated=false WHERE userid = ?"); + $success = $st->execute(array($filt_email, $userid)); + if ($success) { + $db->send_validation_email($username, $filt_email, $twig_env); + $conn->commit(); + $success_alerts[] = "email updated successfully, you should get a new validation email at the new email address"; + } else { + $einfo = $st->errorInfo(); + error_log("mcoop: profile.php failed updating email: " . var_export($einfo, true) . " ($userid, $filt_email)"); + $danger_alerts[] = "Internal error, please contact the admin"; + } + } catch (RegistrationError $re) { + $success = false; + $danger_alerts[] = $re->reason; + if ($conn->inTransaction()) + $conn->rollBack(); + } + return $success; +} + + +// TODO: add password updating as well + +$update_vars = array(); +$varname_mappings = array( + "email" => array("\mcoop\check_email", "\mcoop\update_email"), + "fullname" => array("\mcoop\check_full_name", "\mcoop\update_full_name") +); + +$attempted = false; + +foreach ($varname_mappings as $k => $a) { + if (isset($_POST[$k])) { + $v = $_POST[$k]; + $check_func = $a[0]; + $res = $check_func($db, $sess_info, $v); + if ($res) { + $attempted = true; + $update_func = $a[1]; + $update_func($db, $sess_info, $v, $twig); + } + } +} + +if ($attempted) { + $sess_info->re_init(); +} + + +echo $twig->render("profile.tmpl", array( + "danger_alerts" => $danger_alerts, + "success_alerts" => $success_alerts, + "sess_info" => $sess_info +)); + +?> diff --git a/register.php b/register.php new file mode 100644 index 0000000..99cb2d1 --- /dev/null +++ b/register.php @@ -0,0 +1,57 @@ +recaptcha_secret); + $username = $_POST["username"]; + $email = $_POST["email"]; + $password = $_POST["passwd"]; + if (isset($_POST["fullname"])) { + $full_name = $_POST["fullname"]; + } else { + $full_name = null; + } + // TODO: we should really filter/validate g-recaptcha-response (still need to do) + $recaptcha_resp = $_POST["g-recaptcha-response"]; + // XXX: one downside of this is it checks the captcha before validating all the other fields, might want to move captcha validation to register() + $resp = $recaptcha->verify($recaptcha_resp); + $captcha_valid = $resp->isSuccess(); + $reg_successful = false; + if ($captcha_valid) { + try { + // TODO: validate_email (5th arg) + $db->register($username, $email, $password, $full_name, true, $twig); + $reg_successful = true; + $success_alerts[] = 'Registration successful, Click here to return to the webapp'; + } catch (RegistrationError $re) { + $reg_successful = false; + $error_text = $re->reason; + $danger_alerts[] = $error_text; + if ($db->conn->inTransaction()) + $db->conn->rollBack(); + } + } else { + $danger_alerts[] = "Captcha Invalid, please try again."; + } +} + +// TODO: move into a util file +echo $twig->render("register.tmpl", array( + "danger_alerts" => $danger_alerts, + "success_alerts" => $success_alerts, + "sess_info" => $sess_info +)); + +?> diff --git a/tasks.php b/tasks.php new file mode 100644 index 0000000..5834bdf --- /dev/null +++ b/tasks.php @@ -0,0 +1,13 @@ +render("tasks.tmpl", array( + "danger_alerts" => $danger_alerts, + "success_alerts" => $success_alerts, + "sess_info" => $sess_info +)); + +?> diff --git a/tasks_api.php b/tasks_api.php new file mode 100644 index 0000000..e65b0e9 --- /dev/null +++ b/tasks_api.php @@ -0,0 +1,28 @@ + $task) { + $task->load_admin($db); + $task->load_tdcs($db, false); + } + header("Content-Type: application/json"); + // TODO: this should probably use tasks->to_json instead so as to not serialize more than needed + echo json_encode($tasks); + //var_dump($tasks); +} + +$mapping = array( + "get_all_tasks_simple" => "\mcoop\get_all_tasks_simple" +); + +if (isset($_GET["action"])) { + $action = $_GET["action"]; + if (array_key_exists($action, $mapping)) { + $funcname = $mapping[$action]; + $funcname($db); + } +} +?> diff --git a/tmpl/base.tmpl b/tmpl/base.tmpl new file mode 100644 index 0000000..b0fa3f3 --- /dev/null +++ b/tmpl/base.tmpl @@ -0,0 +1,64 @@ + + + + {% block title %}{% endblock %} + + + {% block head_css_standard %} + + + + {% endblock %} + {% block head_extra %}{% endblock %} + + + +
+ {% block alerts %} + {% for alert in danger_alerts %} +
+ {{ alert | raw }} +
+ {% endfor %} + {% for alert in success_alerts %} +
+ {{ alert | raw }} +
+ {% endfor %} + {% endblock %} + +
+{% block page_contents %}{% endblock %} +
+
+ {% block body_js %} + + + {%- endblock %} + + diff --git a/tmpl/bylaws.tmpl b/tmpl/bylaws.tmpl new file mode 100644 index 0000000..e69de29 diff --git a/tmpl/index.tmpl b/tmpl/index.tmpl new file mode 100644 index 0000000..cea7e90 --- /dev/null +++ b/tmpl/index.tmpl @@ -0,0 +1,19 @@ +{% extends "base.tmpl" %} + +{% block title %}{{ sitename | escape }}{% endblock %} + +{% block page_contents %} +
+

Welcome to {{ sitename | escape }}

+

+

This is a sample mcoop install, the intent of mcoop is to be used to manage small Co-Operatives, so far there aren't many features but here's a TODO list for some of the intended features

+
    +
  • Allow both the Co-Operative and members to post bounties in terms of shares (or dividend credits) on specific tasks and let other members claim them once they've done an amount of work on the task.
  • +
  • For Co-Operatives mostly focused on development allow automatic dividend credit distribution based on contributions to the Co-Operatives repositories, distributing x credits/month with the distribution based on kloc/person in commits or similar metrics (requires integration with VCSes -- also see Section 35 of C37-3, also worth noting that normal business with the Co-Operative should also yield dividend credits)
  • +
  • Manage details of the directors and public-facing members for the public site ala. a simple CMS, or just integrate the list directly into mcoop and use it as your Co-Op's public site
  • +
+
+ Disclaimer: Mainline is not an actual Co-Operative and isn't registered, the author is just interested in them and wanted to create a management webapp that might eventually be used by real Co-Operatives. +
+
+{% endblock %} diff --git a/tmpl/login.tmpl b/tmpl/login.tmpl new file mode 100644 index 0000000..de3a6e9 --- /dev/null +++ b/tmpl/login.tmpl @@ -0,0 +1,18 @@ +{% extends "base.tmpl" %} + +{% block title %}Login{% endblock %} + +{% block page_contents %} +
+

Login

+

+
+
+
+
Username*
+
+
Password *
+
+
+
+{% endblock %} diff --git a/tmpl/logout.tmpl b/tmpl/logout.tmpl new file mode 100644 index 0000000..a507ed0 --- /dev/null +++ b/tmpl/logout.tmpl @@ -0,0 +1,9 @@ +{% extends "base.tmpl" %} + +{% block title %}Logout{% endblock %} + +{% block page_contents %} +
+
Successfully Logged Out
+
+{% endblock %} diff --git a/tmpl/profile.tmpl b/tmpl/profile.tmpl new file mode 100644 index 0000000..92bb780 --- /dev/null +++ b/tmpl/profile.tmpl @@ -0,0 +1,24 @@ +{% extends "base.tmpl" %} + +{% block title %}Profile{% endblock %} + +{% block page_contents %} +
+

Edit Profile

+

+
+ +
+
Username
+
{{ sess_info.login_member.username }}
+
Email
+
+
Full name
+
+
Old Password
+
+
New Password
+
+
+
+{% endblock %} diff --git a/tmpl/register.tmpl b/tmpl/register.tmpl new file mode 100644 index 0000000..f0878e9 --- /dev/null +++ b/tmpl/register.tmpl @@ -0,0 +1,30 @@ +{% extends "base.tmpl" %} + +{% block title %}Register{% endblock %} + +{% block body_js %} + + {{- parent() }} +{% endblock %} + +{% block page_contents %} +
+

Register for basic membership here

+

+

this doesn't register you as a full member, you have to confirm your email address and start Contributing first

+
+ +
+
Username*
+
+
Email Address *
+
+
Password *
+
+
Full Name
+
+
+
+
+
+{% endblock %} diff --git a/tmpl/tasks.tmpl b/tmpl/tasks.tmpl new file mode 100644 index 0000000..070e002 --- /dev/null +++ b/tmpl/tasks.tmpl @@ -0,0 +1,29 @@ +{% extends "base.tmpl" %} + +{% block title %}Tasks{% endblock %} + +{% block head_extra %} + +{% endblock %} + +{% block page_contents %} +
+

Task Search

+
+

+
+
+ + + + + + + + + + + + + +{% endblock %} diff --git a/tmpl/validate.tmpl b/tmpl/validate.tmpl new file mode 100644 index 0000000..ae32718 --- /dev/null +++ b/tmpl/validate.tmpl @@ -0,0 +1,19 @@ +{% extends "base.tmpl" %} + +{% block title %}Validate Email{% endblock %} + +{% block page_contents %} +
+

Validate Email Address

+

+

If for whatever reason you can't click on the link in the email you can manually enter your username and the validation code here to validate your email address, the validation code is everything to the right of vcode= (excluding the '=') in the url

+
+ +
+
Username
+
+
Validation Code
+
+
+
+{% endblock %} diff --git a/tmpl/validation_email.tmpl b/tmpl/validation_email.tmpl new file mode 100644 index 0000000..35076a4 --- /dev/null +++ b/tmpl/validation_email.tmpl @@ -0,0 +1,9 @@ +Hello {{ email }}, + +Thank you for registering at {{ website_name }}, to verify your email address please visit the following URL: +{{ vurl | raw }} + +If you didn't create an account you can either do nothing, in which case the person that tried registering with your email address will continue having limited access to the website until they change their email address, or request a deactivation at: +{{ deactivate_url | raw }} + +Deactivated accounts are deleted automatically after 28 days unless a reactivation is requested. diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..f02c3b2 --- /dev/null +++ b/todo.txt @@ -0,0 +1,25 @@ +* Tasks and Member list of course +* Bylaw management + * shares distribution policy, a few things about that are in 7(2) and 8(1) + * shareholder classes + * share classes + * Voting on Bylaw proposals by members +* Something that might make sense is preparing for initial incorporation, + utilities for getting everything in order (only thing is I'm not an expert + at co-operatives) + * initially it would only handle Saskatchewan requirements, eventually + create a DSL or a method of configuration to handle the different + laws and requirements in different jurisdictions +* Receiver stuff, see Part VIII (might just add a table of Receiverships for + specific properties, or as a column in the members table) +* General meetings things, Part IX +* Resolutions and +* committees, see section 73 +* director elections, see 74 +* Voting on sales, leases and exchanges as per 76 +* Specific officer roles, Section 94 +* Getting into the Membership things is more relevant, Part X (Section 98+) +* 106 record dates are relevant +* 110(3) makes sense for including in Bylaws/Articles or the site settings +* 111 mentions Proposals +* 112 has bylaw stuff diff --git a/validate.php b/validate.php new file mode 100644 index 0000000..26fc67d --- /dev/null +++ b/validate.php @@ -0,0 +1,40 @@ +validation_code == $_GET["vcode"]) { + $st = $db->conn->prepare("UPDATE members SET validated=true WHERE userid = ?"); + $success = $st->execute(array($m->userid)); + if ($success) { + $success_alerts[] = 'Successfully validated your email address, Continue'; + } else { + $einfo = $st->errorInfo(); + error_log("mcoop: validate.php failed updating members: " . var_export($einfo, true) . " ($m->userid)"); + $danger_alerts[] = "Failed to update the database to validate your email address, this is an internal error, please contact the admin"; + } + } else { + $danger_alerts[] = "Unable to validate email: incorrect validation code"; + } + } catch (UnknownMember $un) { + $danger_alerts[] = "Unable to validate email: no such user"; + } +} + +echo $twig->render("validate.tmpl", array( + "danger_alerts" => $danger_alerts, + "success_alerts" => $success_alerts, + "sess_info" => $sess_info, + "vattempted" => $vattempted +)); + +?> -- cgit v1.1
IDTitleStateAdminAvailable Credits