timekeeper

[Abandoned unfinished] CGI web application in C for time tracking. (My first, just a learning project)
Log | Files | Refs | README

commit e46326f376410983fc97685498425a6c5a361760
parent 3e3f2431e1b27841db2b36dd68c630323ff7c36c
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Tue,  5 Mar 2024 22:07:51 -0800

Move backend user functions to user.c

Also move SQL statements to stmt.c and small, general purpose
utilities to util.h (currently only macros).

Diffstat:
MMakefile | 4++--
Astmt.c | 31+++++++++++++++++++++++++++++++
Astmt.h | 17+++++++++++++++++
Mtimekeeper.c | 181++-----------------------------------------------------------------------------
Auser.c | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Auser.h | 23+++++++++++++++++++++++
Autil.h | 3+++
7 files changed, 215 insertions(+), 179 deletions(-)

diff --git a/Makefile b/Makefile @@ -4,8 +4,8 @@ cflags = ${CFLAGS} -O0 -Wall -Wextra -I/usr/local/include lddflags = ${LDDFLAGS} -static -L/usr/local/lib -lkcgi -lkcgihtml -lz -lsqlbox -lsqlite3 -lm -lpthread prefix = /var/www/htdocs/${name}.primus.lan -src = page.c html.c pages/util.c -hdr = ${src:.c=.h} +src = page.c html.c user.c stmt.c pages/util.c +hdr = ${src:.c=.h} util.h obj = ${src:.c=.o} all: ${name} diff --git a/stmt.c b/stmt.c @@ -0,0 +1,31 @@ +#include <stddef.h> +#include <stdint.h> +#include <sqlbox.h> + +#include "stmt.h" + +struct sqlbox_pstmt pstmts[] = { + [StmtInit0] = { .stmt = "PRAGMA foreign_keys = ON" }, + [StmtInit1] = { .stmt = "CREATE TABLE IF NOT EXISTS auth (hash TEXT PRIMARY KEY, name TEXT UNIQUE)" }, + [StmtInit2] = { .stmt = "CREATE TABLE IF NOT EXISTS userdata (id TEXT PRIMARY KEY REFERENCES auth (hash), times BLOB DEFAULT '[]')" }, + [StmtInit3] = { .stmt = "CREATE TRIGGER IF NOT EXISTS userdatahash AFTER INSERT ON auth FOR EACH ROW BEGIN\n" + "INSERT INTO userdata (id) VALUES (NEW.hash); END" }, + [StmtInitLast] = { .stmt = "CREATE TRIGGER IF NOT EXISTS userdelete AFTER DELETE ON auth FOR EACH ROW BEGIN\n" + "DELETE FROM userdata WHERE id IS OLD.hash; END" }, + [StmtAddUser] = { .stmt = "INSERT INTO auth (hash, name) VALUES (?, ?)" }, + [StmtGetUserByName] = { .stmt = "SELECT * FROM auth WHERE name IS ?" }, + [StmtGetUserByHash] = { .stmt = "SELECT * FROM auth WHERE hash IS ?" }, + [StmtDeleteUser] = { .stmt = "DELETE FROM auth WHERE hash IS ?" }, + [StmtNewTimeRow] = { .stmt = "UPDATE userdata SET times =\n" + "json_insert(times, '$[#]', json_array(null, null, null, null)) WHERE id IS ? AND\n" + "(json_array_length(times) = 0 OR json_extract(times, '$[#-1][3]') IS NOT null)" }, + /* Currently, the user can set the end time with a + * break open and the break won't be ended. This is + * intentional, but also it should set the break end + * time to the end time in that case. + */ + [StmtSetTime] = { .stmt = "UPDATE userdata SET times =\n" + "json_replace(times, '$[#-1][' || ? || ']', ?) WHERE id IS ? AND json_extract(times, '$[#-1][' || ? || ']') IS null" }, + [StmtGetTimes] = { .stmt = "SELECT json_array_length(times) * 4 FROM userdata UNION ALL\n" + "SELECT value FROM userdata, json_tree(times) WHERE userdata.id IS ? AND type IS NOT 'array'" } +}; diff --git a/stmt.h b/stmt.h @@ -0,0 +1,17 @@ +enum StmtID { + StmtInit0, + StmtInit1, + StmtInit2, + StmtInit3, + StmtInitLast, + StmtAddUser, + StmtGetUserByHash, + StmtGetUserByName, + StmtDeleteUser, + StmtNewTimeRow, + StmtSetTime, + StmtGetTimes, + StmtMax +}; + +extern struct sqlbox_pstmt pstmts[]; diff --git a/timekeeper.c b/timekeeper.c @@ -22,17 +22,13 @@ #include "page.h" #include "html.h" +#include "user.h" +#include "stmt.h" #include "pages/util.h" -#define Promises "stdio rpath wpath cpath proc recvfd unix sendfd" -#define Len(X) (sizeof(X) / sizeof(X[0])) -#define Max(A,B) ((A) > (B) ? (A) : (B)) -#define Min(A,B) ((A) < (B) ? (A) : (B)) +#include "util.h" -#define NameMin 3 -#define NameMax 64 -#define PassMin 8 -#define PassMax 72 +#define Promises "stdio rpath wpath cpath proc recvfd unix sendfd" enum Field { KeyUsername, @@ -54,22 +50,6 @@ enum Page { PageMax }; -enum StmtID { - StmtInit0, - StmtInit1, - StmtInit2, - StmtInit3, - StmtInitLast, - StmtAddUser, - StmtGetUserByHash, - StmtGetUserByName, - StmtDeleteUser, - StmtNewTimeRow, - StmtSetTime, - StmtGetTimes, - StmtMax -}; - enum TimeField { StartTime, BreakStartTime, @@ -77,11 +57,6 @@ enum TimeField { EndTime }; -struct user { - char *name; - char *hash; -}; - static char *pages[] = { [PageIndex] = "index", [PageLogin] = "login", @@ -120,13 +95,6 @@ kvalid_pass(struct kpair *p) return 1; } -enum LoginStatus { - LoginUntried = -1, - LoginValid, - LoginInvalid, - LoginError -}; - int initdb(struct pagedata *pd) { @@ -138,122 +106,6 @@ initdb(struct pagedata *pd) return 0; } -int -adduser(struct pagedata *pd, char *name, char *key) -{ - char *salt, *hash; - - salt = bcrypt_gensalt(8); - hash = bcrypt(key, salt); - if (!hash) - return 1; - - struct sqlbox_parm p[] = { - { .sparm = hash, .type = SQLBOX_PARM_STRING }, - { .sparm = name, .type = SQLBOX_PARM_STRING } - }; - - if (sqlbox_exec(pd->db, pd->dbid, StmtAddUser, Len(p), p, 0) != SQLBOX_CODE_OK) - return 1; - return 0; -} - -void -freeuser(struct user *user) -{ - if (!user) - return; - free(user->hash); - free(user->name); -} - -struct user * -getuser(struct pagedata *pd, char *field, char *value) -{ - struct user *user; - size_t stmtid; - enum StmtID gets; - struct sqlbox_parmset *res; - struct sqlbox_parm p[] = { - { .sparm = value, .type = SQLBOX_PARM_STRING } - }; - - if (strcmp(field, "name") == 0) - gets = StmtGetUserByName; - else if (strcmp(field, "hash") == 0) - gets = StmtGetUserByHash; - else - err(1, "Expected valid field"); - - if (!(stmtid = sqlbox_prepare_bind(pd->db, pd->dbid, gets, Len(p), p, 0))) - err(1, "sdqlb"); - - if (!(res = sqlbox_step(pd->db, stmtid))) - err(1, "step"); - - if (!res->psz && res->code == SQLBOX_CODE_OK) { - kutil_info(&pd->req, NULL, "getuser: %s: %s not found", value, field); - sqlbox_finalise(pd->db, stmtid); - return NULL; - } - - user = calloc(1, sizeof(*user)); - if (!user) - err(1, "IDC"); - - if (res->psz != 2) - errx(1, "%zu: Invalid number of fields in user-data", res->psz); - user->hash = strdup(res->ps[0].sparm); - if (!user->hash) - err(1, "IDC"); - user->name = strdup(res->ps[1].sparm); - if (!user->name) - err(1, "IDC"); - - if (!sqlbox_finalise(pd->db, stmtid)) - err(1, "finalize"); - - return user; -} - -int -deleteuser(struct pagedata *pd, char *hash) -{ - struct sqlbox_parm p = { - .sparm = hash, .type = SQLBOX_PARM_STRING - }; - - return sqlbox_exec(pd->db, 0, StmtDeleteUser, 1, &p, 0) != SQLBOX_CODE_OK; -} - -enum LoginStatus -loginuser(struct user **userp, struct pagedata *pd, char *name, char *key) -{ - struct user *user; - char *testhash; - - user = getuser(pd, "name", name); - if (!user) - return LoginInvalid; - - testhash = bcrypt(key, user->hash); - if (!testhash) { - freeuser(user); - return LoginError; - } - - if (strcmp(user->hash, testhash) != 0) { - freeuser(user); - return LoginInvalid; - } - - if (userp) - *userp = user; - else - freeuser(user); - return LoginValid; -} - /* getlogin is taken */ struct user * sitegetlogin(struct pagedata *pd) @@ -823,31 +675,6 @@ main(void) struct sqlbox_src srcs[] = { { .fname = "/tmp/timekeeper.db", .mode = SQLBOX_SRC_RWC } }; - struct sqlbox_pstmt pstmts[] = { - [StmtInit0] = { .stmt = "PRAGMA foreign_keys = ON" }, - [StmtInit1] = { .stmt = "CREATE TABLE IF NOT EXISTS auth (hash TEXT PRIMARY KEY, name TEXT UNIQUE)" }, - [StmtInit2] = { .stmt = "CREATE TABLE IF NOT EXISTS userdata (id TEXT PRIMARY KEY REFERENCES auth (hash), times BLOB DEFAULT '[]')" }, - [StmtInit3] = { .stmt = "CREATE TRIGGER IF NOT EXISTS userdatahash AFTER INSERT ON auth FOR EACH ROW BEGIN\n" - "INSERT INTO userdata (id) VALUES (NEW.hash); END" }, - [StmtInitLast] = { .stmt = "CREATE TRIGGER IF NOT EXISTS userdelete AFTER DELETE ON auth FOR EACH ROW BEGIN\n" - "DELETE FROM userdata WHERE id IS OLD.hash; END" }, - [StmtAddUser] = { .stmt = "INSERT INTO auth (hash, name) VALUES (?, ?)" }, - [StmtGetUserByName] = { .stmt = "SELECT * FROM auth WHERE name IS ?" }, - [StmtGetUserByHash] = { .stmt = "SELECT * FROM auth WHERE hash IS ?" }, - [StmtDeleteUser] = { .stmt = "DELETE FROM auth WHERE hash IS ?" }, - [StmtNewTimeRow] = { .stmt = "UPDATE userdata SET times =\n" - "json_insert(times, '$[#]', json_array(null, null, null, null)) WHERE id IS ? AND\n" - "(json_array_length(times) = 0 OR json_extract(times, '$[#-1][3]') IS NOT null)" }, - /* Currently, the user can set the end time with a - * break open and the break won't be ended. This is - * intentional, but also it should set the break end - * time to the end time in that case. - */ - [StmtSetTime] = { .stmt = "UPDATE userdata SET times =\n" - "json_replace(times, '$[#-1][' || ? || ']', ?) WHERE id IS ? AND json_extract(times, '$[#-1][' || ? || ']') IS null" }, - [StmtGetTimes] = { .stmt = "SELECT json_array_length(times) * 4 FROM userdata UNION ALL\n" - "SELECT value FROM userdata, json_tree(times) WHERE userdata.id IS ? AND type IS NOT 'array'" } - }; enum kcgi_err (*pagefunctions[])(struct pagedata *) = { [PageIndex] = pageindex, [PageLogin] = pagelogin, diff --git a/user.c b/user.c @@ -0,0 +1,135 @@ +#define const + +#include <sys/types.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <kcgi.h> +#include <kcgihtml.h> + +#include <err.h> +#include <pwd.h> + +#include <sqlbox.h> + +#include "page.h" +#include "stmt.h" +#include "user.h" +#include "util.h" + +int +adduser(struct pagedata *pd, char *name, char *key) +{ + char *salt, *hash; + + salt = bcrypt_gensalt(8); + hash = bcrypt(key, salt); + if (!hash) + return 1; + + struct sqlbox_parm p[] = { + { .sparm = hash, .type = SQLBOX_PARM_STRING }, + { .sparm = name, .type = SQLBOX_PARM_STRING } + }; + + if (sqlbox_exec(pd->db, pd->dbid, StmtAddUser, Len(p), p, 0) != SQLBOX_CODE_OK) + return 1; + return 0; +} + +void +freeuser(struct user *user) +{ + if (!user) + return; + free(user->hash); + free(user->name); +} + +struct user * +getuser(struct pagedata *pd, char *field, char *value) +{ + struct user *user; + size_t stmtid; + enum StmtID gets; + struct sqlbox_parmset *res; + struct sqlbox_parm p[] = { + { .sparm = value, .type = SQLBOX_PARM_STRING } + }; + + if (strcmp(field, "name") == 0) + gets = StmtGetUserByName; + else if (strcmp(field, "hash") == 0) + gets = StmtGetUserByHash; + else + err(1, "Expected valid field"); + + if (!(stmtid = sqlbox_prepare_bind(pd->db, pd->dbid, gets, Len(p), p, 0))) + err(1, "sdqlb"); + + if (!(res = sqlbox_step(pd->db, stmtid))) + err(1, "step"); + + if (!res->psz && res->code == SQLBOX_CODE_OK) { + kutil_info(&pd->req, NULL, "getuser: %s: %s not found", value, field); + sqlbox_finalise(pd->db, stmtid); + return NULL; + } + + user = calloc(1, sizeof(*user)); + if (!user) + err(1, "IDC"); + + if (res->psz != 2) + errx(1, "%zu: Invalid number of fields in user-data", res->psz); + user->hash = strdup(res->ps[0].sparm); + if (!user->hash) + err(1, "IDC"); + user->name = strdup(res->ps[1].sparm); + if (!user->name) + err(1, "IDC"); + + if (!sqlbox_finalise(pd->db, stmtid)) + err(1, "finalize"); + + return user; +} + +int +deleteuser(struct pagedata *pd, char *hash) +{ + struct sqlbox_parm p = { + .sparm = hash, .type = SQLBOX_PARM_STRING + }; + + return sqlbox_exec(pd->db, 0, StmtDeleteUser, 1, &p, 0) != SQLBOX_CODE_OK; +} + +enum LoginStatus +loginuser(struct user **userp, struct pagedata *pd, char *name, char *key) +{ + struct user *user; + char *testhash; + + user = getuser(pd, "name", name); + if (!user) + return LoginInvalid; + + testhash = bcrypt(key, user->hash); + if (!testhash) { + freeuser(user); + return LoginError; + } + + if (strcmp(user->hash, testhash) != 0) { + freeuser(user); + return LoginInvalid; + } + + if (userp) + *userp = user; + else + freeuser(user); + return LoginValid; +} diff --git a/user.h b/user.h @@ -0,0 +1,23 @@ +#define NameMin 3 +#define NameMax 64 +#define PassMin 8 +#define PassMax 72 + +enum LoginStatus { + LoginUntried = -1, + LoginValid, + LoginInvalid, + LoginError +}; + +struct user { + char *name; + char *hash; +}; + +int adduser(struct pagedata *pd, char *name, char *key); +void freeuser(struct user *user); +struct user *getuser(struct pagedata *pd, char *field, char *value); +int deleteuser(struct pagedata *pd, char *hash); +enum LoginStatus loginuser(struct user **userp, struct pagedata *pd, + char *name, char *key); diff --git a/util.h b/util.h @@ -0,0 +1,3 @@ +#define Len(X) (sizeof(X) / sizeof(X[0])) +#define Max(A,B) ((A) > (B) ? (A) : (B)) +#define Min(A,B) ((A) < (B) ? (A) : (B))