From a6826055bf4e6a23f80da047ccfe4509a209f3a6 Mon Sep 17 00:00:00 2001 From: Andinus Date: Sun, 29 Mar 2020 16:10:59 +0530 Subject: Initial perseus rewrite --- account/addtoken.go | 56 ++++++++++++++++++++++++++++++ account/adduser.go | 43 +++++++++++++++++++++++ account/getid.go | 33 ++++++++++++++++++ account/login.go | 50 +++++++++++++++++++++++++++ account/register.go | 49 ++++++++++++++++++++++++++ account/user.go | 10 ++++++ auth/checkpass.go | 14 -------- auth/checkpass_test.go | 40 --------------------- auth/genid.go | 14 -------- auth/hashpass.go | 13 ------- auth/hashpass_test.go | 34 ------------------ build/ci/drone.yml | 6 ++-- cmd/perseus/main.go | 13 ++++--- core/version.go | 6 ---- handler/web/login.go | 81 +++++++++++++++++++++++++++++++++++++++++++ handler/web/page.go | 7 ++-- handler/web/register.go | 80 ++++++++++++++++++++++++++++++++++++++++++ password/check.go | 13 +++++++ password/check_test.go | 38 ++++++++++++++++++++ password/hash.go | 11 ++++++ password/hash_test.go | 34 ++++++++++++++++++ password/randstr.go | 13 +++++++ storage/init.go | 83 ++++++++++++++++++++++++++++++++++++++++++++ storage/sqlite3/db.go | 13 ------- storage/sqlite3/init.go | 84 --------------------------------------------- storage/storage.go | 16 ++++++--- user/adduser.go | 42 ----------------------- user/getid.go | 29 ---------------- user/user.go | 9 ----- web/login.html | 52 ---------------------------- web/register.html | 52 ---------------------------- web/templates/login.html | 52 ++++++++++++++++++++++++++++ web/templates/register.html | 52 ++++++++++++++++++++++++++++ 33 files changed, 720 insertions(+), 422 deletions(-) create mode 100644 account/addtoken.go create mode 100644 account/adduser.go create mode 100644 account/getid.go create mode 100644 account/login.go create mode 100644 account/register.go create mode 100644 account/user.go delete mode 100644 auth/checkpass.go delete mode 100644 auth/checkpass_test.go delete mode 100644 auth/genid.go delete mode 100644 auth/hashpass.go delete mode 100644 auth/hashpass_test.go delete mode 100644 core/version.go create mode 100644 handler/web/login.go create mode 100644 handler/web/register.go create mode 100644 password/check.go create mode 100644 password/check_test.go create mode 100644 password/hash.go create mode 100644 password/hash_test.go create mode 100644 password/randstr.go create mode 100644 storage/init.go delete mode 100644 storage/sqlite3/db.go delete mode 100644 storage/sqlite3/init.go delete mode 100644 user/adduser.go delete mode 100644 user/getid.go delete mode 100644 user/user.go delete mode 100644 web/login.html delete mode 100644 web/register.html create mode 100644 web/templates/login.html create mode 100644 web/templates/register.html diff --git a/account/addtoken.go b/account/addtoken.go new file mode 100644 index 0000000..1c36ad8 --- /dev/null +++ b/account/addtoken.go @@ -0,0 +1,56 @@ +package account + +import ( + "log" + "time" + + "tildegit.org/andinus/perseus/password" + "tildegit.org/andinus/perseus/storage" +) + +// addToken will generate a random token, add it to database and +// return the token. +func (u *User) addToken(db *storage.DB) error { + u.Token = password.RandStr(64) + + // Set user id from username. + err := u.GetID(db) + if err != nil { + log.Printf("account/addtoken.go: %s\n", + "failed to get id from username") + return err + } + + // Acquire write lock on the database. + db.Mu.Lock() + defer db.Mu.Unlock() + + // Start the transaction + tx, err := db.Conn.Begin() + defer tx.Rollback() + if err != nil { + log.Printf("account/addtoken.go: %s\n", + "failed to begin transaction") + return err + } + + stmt, err := db.Conn.Prepare(` +INSERT INTO access(id, token, genTime) values(?, ?, ?)`) + if err != nil { + log.Printf("account/addtoken.go: %s\n", + "failed to prepare statement") + return err + } + defer stmt.Close() + + _, err = stmt.Exec(u.ID, u.Token, time.Now().UTC()) + if err != nil { + log.Printf("account/addtoken.go: %s\n", + "failed to execute statement") + return err + } + + tx.Commit() + return err + +} diff --git a/account/adduser.go b/account/adduser.go new file mode 100644 index 0000000..8a8734f --- /dev/null +++ b/account/adduser.go @@ -0,0 +1,43 @@ +package account + +import ( + "log" + "time" + + "tildegit.org/andinus/perseus/storage" +) + +// addUser adds the user to record. +func (u *User) addUser(db *storage.DB) error { + // Acquire write lock on the database. + db.Mu.Lock() + defer db.Mu.Unlock() + + // Start the transaction + tx, err := db.Conn.Begin() + defer tx.Rollback() + if err != nil { + log.Printf("account/adduser.go: %s\n", + "failed to begin transaction") + return err + } + + stmt, err := db.Conn.Prepare(` +INSERT INTO accounts(id, username, hash, regTime) values(?, ?, ?, ?)`) + if err != nil { + log.Printf("account/adduser.go: %s\n", + "failed to prepare statement") + return err + } + defer stmt.Close() + + _, err = stmt.Exec(u.ID, u.Username, u.Hash, time.Now().UTC()) + if err != nil { + log.Printf("account/adduser.go: %s\n", + "failed to execute statement") + return err + } + + tx.Commit() + return err +} diff --git a/account/getid.go b/account/getid.go new file mode 100644 index 0000000..4f6da69 --- /dev/null +++ b/account/getid.go @@ -0,0 +1,33 @@ +package account + +import ( + "log" + + "tildegit.org/andinus/perseus/storage" +) + +// GetID returns id from username. +func (u *User) GetID(db *storage.DB) error { + // Acquire read lock on database. + db.Mu.RLock() + defer db.Mu.RUnlock() + + // Get password for this user from the database. + stmt, err := db.Conn.Prepare("SELECT id FROM accounts WHERE username = ?") + if err != nil { + log.Printf("account/getid.go: %s\n", + "failed to prepare statement") + return err + } + defer stmt.Close() + + var id string + err = stmt.QueryRow(u.Username).Scan(&id) + if err != nil { + log.Printf("account/getid.go: %s\n", + "query failed") + } + u.ID = id + + return err +} diff --git a/account/login.go b/account/login.go new file mode 100644 index 0000000..c81fcbd --- /dev/null +++ b/account/login.go @@ -0,0 +1,50 @@ +package account + +import ( + "log" + + "tildegit.org/andinus/perseus/password" + "tildegit.org/andinus/perseus/storage" +) + +// Login takes in login details and returns an error. If error doesn't +// equal nil then consider login failed. It will also set the u.Token +// field. +func (u *User) Login(db *storage.DB) error { + // Acquire read lock on the database. + db.Mu.RLock() + + // Get password for this user from the database. + stmt, err := db.Conn.Prepare("SELECT hash FROM accounts WHERE username = ?") + if err != nil { + log.Printf("account/login.go: %s\n", + "failed to prepare statement") + return err + } + defer stmt.Close() + + var hash string + err = stmt.QueryRow(u.Username).Scan(&hash) + if err != nil { + log.Printf("account/login.go: %s\n", + "query failed") + return err + } + u.Hash = hash + + // Check user's password. + err = password.Check(u.Password, u.Hash) + if err != nil { + log.Printf("account/login.go: %s%s\n", + "user login failed, username: ", u.Username) + return err + } + db.Mu.RUnlock() + + err = u.addToken(db) + if err != nil { + log.Printf("account/login.go: %s\n", + "addtoken failed") + } + return err +} diff --git a/account/register.go b/account/register.go new file mode 100644 index 0000000..e008370 --- /dev/null +++ b/account/register.go @@ -0,0 +1,49 @@ +package account + +import ( + "errors" + "log" + "regexp" + "strings" + + "tildegit.org/andinus/perseus/password" + "tildegit.org/andinus/perseus/storage" +) + +// Register takes in registration details and returns an error. If +// error doesn't equal nil then the registration was unsuccessful. +func (u User) Register(db *storage.DB) error { + var err error + u.ID = password.RandStr(64) + u.Username = strings.ToLower(u.Username) + + // Validate username. It must be alphanumeric and less than + // 128 characters. + re := regexp.MustCompile("^[a-zA-Z0-9]*$") + if !re.MatchString(u.Username) { + return errors.New("account/register.go: invalid username") + } + if len(u.Username) > 128 { + return errors.New("account/register.go: username too long") + } + + // Validate password + if len(u.Password) < 8 { + return errors.New("account/register.go: password too short") + } + + u.Hash, err = password.Hash(u.Password) + if err != nil { + log.Printf("account/register.go: %s\n", + "password.Hash func failed") + return err + } + + err = u.addUser(db) + if err != nil { + log.Printf("account/register.go: %s\n", + "addUser func failed") + } + return err + +} diff --git a/account/user.go b/account/user.go new file mode 100644 index 0000000..769e8fc --- /dev/null +++ b/account/user.go @@ -0,0 +1,10 @@ +package account + +// User holds information about the user. +type User struct { + ID string + Username string + Password string + Hash string + Token string +} diff --git a/auth/checkpass.go b/auth/checkpass.go deleted file mode 100644 index 64e3c9f..0000000 --- a/auth/checkpass.go +++ /dev/null @@ -1,14 +0,0 @@ -package auth - -import ( - "golang.org/x/crypto/bcrypt" -) - -// checkPass takes a string and hash as input and returns an error. If -// the error is not nil then the consider the password wrong. We're -// returning error instead of a bool so that we can print failed -// logins to log and logging shouldn't happen here. -func checkPass(password, hash string) error { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err -} diff --git a/auth/checkpass_test.go b/auth/checkpass_test.go deleted file mode 100644 index c8afaee..0000000 --- a/auth/checkpass_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package auth - -import "testing" - -// TestCheckPass tests the checkPass function. -func TestCheckPass(t *testing.T) { - var err error - passhash := make(map[string]string) - - // First we check with static values, these should always pass. - passhash["ter4cQ=="] = "$2a$10$ANkaNEFFQ4zxDwTwvAUfoOCqpVIdgtPFopFOTMSrFy39WkaMAYLIC" - passhash["G29J6A=="] = "$2a$10$1oH1PyhncIHcHJWbLt3Gv.OjClUoFoaEDaFpQ9E9atfRbIrVxsbwm" - passhash["Z1S/kQ=="] = "$2a$10$fZ05kKmb7bh4vBLebpK1u.3bUNQ6eeX5ghT/GZaekgS.5bx4.Ru1e" - passhash["J861dQ=="] = "$2a$10$nXb6Btn6n3AWMAUkDh9bFObvQw5V9FLKhfX.E1EzRWgVDuqIp99u2" - - // We also check with values generated with hashPass, this may - // fail if hashPass itself fails in that case it's not - // checkPass error so the test shouldn't fail but warning - // should be sent. We use genID func to generate random inputs - // for this test. - for i := 1; i <= 4; i++ { - p := genID(8) - passhash[p], err = hashPass(p) - if err != nil { - t.Log("hashPass func failed") - } - } - - // We test the checkPass func by ranging over all values of - // passhash. We assume that hashPass func returns correct - // hashes. - for p, h := range passhash { - err = checkPass(p, h) - if err != nil { - t.Errorf("password: %s, hash: %s didn't match.", - p, h) - } - } - -} diff --git a/auth/genid.go b/auth/genid.go deleted file mode 100644 index fea7ac9..0000000 --- a/auth/genid.go +++ /dev/null @@ -1,14 +0,0 @@ -package auth - -import ( - "crypto/rand" - "encoding/base64" -) - -// genID generates a random id string of length n. Don't forget to -// seed the random number generator otherwise it won't be random. -func genID(n int) string { - b := make([]byte, n/2) - rand.Read(b) - return base64.StdEncoding.EncodeToString(b) -} diff --git a/auth/hashpass.go b/auth/hashpass.go deleted file mode 100644 index 4e5041a..0000000 --- a/auth/hashpass.go +++ /dev/null @@ -1,13 +0,0 @@ -package auth - -import ( - "golang.org/x/crypto/bcrypt" -) - -// hashPass takes a string as input and returns the hash of the -// password. -func hashPass(password string) (string, error) { - // 10 is the default cost. - bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10) - return string(bytes), err -} diff --git a/auth/hashpass_test.go b/auth/hashpass_test.go deleted file mode 100644 index 7ea9480..0000000 --- a/auth/hashpass_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package auth - -import "testing" - -// TestHashPass tests the checkPass function. -func TestHashPass(t *testing.T) { - var err error - passhash := make(map[string]string) - - // We generate random hashes with hashPass, random string is - // generate by genID func. - for i := 1; i <= 8; i++ { - p := genID(8) - passhash[p], err = hashPass(p) - - // Here we test if the hashPass func runs sucessfully. - if err != nil { - t.Errorf("hashPass func failed for password: %s", - p) - } - } - - // Here we are testing if the hashPass func returns correct - // hashes. We assume that checkPass func returns correct - // values. - for p, h := range passhash { - err = checkPass(p, h) - if err != nil { - t.Errorf("password: %s, hash: %s didn't match.", - p, h) - } - } - -} diff --git a/build/ci/drone.yml b/build/ci/drone.yml index 6d68a3f..37590ac 100644 --- a/build/ci/drone.yml +++ b/build/ci/drone.yml @@ -6,12 +6,12 @@ steps: - name: vet image: golang:1.13 commands: - - go vet ./... + - go vet ./... - name: test image: golang:1.13 commands: - - go test -v ./auth + - go test -v ./password --- kind: pipeline @@ -24,4 +24,4 @@ steps: GOARCH: amd64 GOOS: openbsd commands: - - go build ./cmd/perseus + - go build ./cmd/perseus diff --git a/cmd/perseus/main.go b/cmd/perseus/main.go index 6abd3a4..ca1a50d 100644 --- a/cmd/perseus/main.go +++ b/cmd/perseus/main.go @@ -15,25 +15,24 @@ func main() { db := storage.Init() defer db.Conn.Close() - envPort, exists := os.LookupEnv("PERSEUS_PORT") - if !exists { + envPort := os.Getenv("PERSEUS_PORT") + if envPort == "" { envPort = "8080" } - addr := fmt.Sprintf("127.0.0.1:%s", envPort) srv := &http.Server{ - Addr: addr, + Addr: fmt.Sprintf("127.0.0.1:%s", envPort), WriteTimeout: 8 * time.Second, ReadTimeout: 8 * time.Second, } http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) { - web.HandleRegister(w, r, db) + web.RegisterHandler(w, r, db) }) http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { - web.HandleLogin(w, r, db) + web.LoginHandler(w, r, db) }) - log.Printf("main/main.go: listening on port %s...", envPort) + log.Printf("perseus: listening on port %s...", envPort) log.Fatal(srv.ListenAndServe()) } diff --git a/core/version.go b/core/version.go deleted file mode 100644 index 3e45c6f..0000000 --- a/core/version.go +++ /dev/null @@ -1,6 +0,0 @@ -package core - -// Version will return the current version. -func Version() string { - return "v0.1.0" -} diff --git a/handler/web/login.go b/handler/web/login.go new file mode 100644 index 0000000..3ad15a1 --- /dev/null +++ b/handler/web/login.go @@ -0,0 +1,81 @@ +package web + +import ( + "fmt" + "html/template" + "log" + "net/http" + "time" + + "tildegit.org/andinus/perseus/account" + "tildegit.org/andinus/perseus/storage" +) + +// LoginHandler handles login. +func LoginHandler(w http.ResponseWriter, r *http.Request, db *storage.DB) { + p := Page{} + var err error + + t, err := template.ParseFiles("web/templates/login.html") + if err != nil { + log.Printf("web/login.go: 500 Internal Server Error :: %s", err.Error()) + http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) + return + } + + switch r.Method { + case http.MethodGet: + t.Execute(w, p) + + case http.MethodPost: + if err = r.ParseForm(); err != nil { + log.Printf("web/login.go: 400 Bad Request :: %s", err.Error()) + http.Error(w, "400 Bad Request", http.StatusBadRequest) + return + } + + // Get form values + u := account.User{} + u.Username = r.FormValue("username") + u.Password = r.FormValue("password") + + // Perform login + err = u.Login(db) + + if err != nil { + log.Printf("web/login.go: %s :: %s", + "login failed", + err.Error()) + + error := []string{} + error = append(error, + fmt.Sprintf("Login failed")) + + p.Error = error + t.Execute(w, p) + return + } + + // Login successful, set token + cookie := http.Cookie{ + Name: "token", + Value: u.Token, + // Expire the cookie after 16 days from + // current UTC time. + Expires: time.Now().UTC().Add(16 * 24 * time.Hour), + SameSite: http.SameSiteLaxMode, + HttpOnly: true, + } + http.SetCookie(w, &cookie) + success := []string{} + success = append(success, + fmt.Sprintf("Login successful")) + p.Success = success + t.Execute(w, p) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + log.Printf("web/login.go: %v not allowed on %v", r.Method, r.URL) + } + +} diff --git a/handler/web/page.go b/handler/web/page.go index 1f457de..91e8e56 100644 --- a/handler/web/page.go +++ b/handler/web/page.go @@ -1,11 +1,8 @@ package web -import ( - "html/template" -) +import "html/template" -// Page holds page information that is sent to all webpages rendered -// by perseus. +// Page holds page information. type Page struct { SafeList []template.HTML List []string diff --git a/handler/web/register.go b/handler/web/register.go new file mode 100644 index 0000000..1a80651 --- /dev/null +++ b/handler/web/register.go @@ -0,0 +1,80 @@ +package web + +import ( + "fmt" + "html/template" + "log" + "net/http" + "strings" + + "tildegit.org/andinus/perseus/account" + "tildegit.org/andinus/perseus/storage" +) + +// RegisterHandler handles registration. +func RegisterHandler(w http.ResponseWriter, r *http.Request, db *storage.DB) { + p := Page{} + var err error + + t, err := template.ParseFiles("web/templates/register.html") + if err != nil { + log.Printf("web/register.go: 500 Internal Server Error :: %s", err.Error()) + http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) + return + } + + p.Notice = []string{ + "Only [a-z] & [0-9] allowed for username", + "Password length must be greater than 8 characters", + } + + switch r.Method { + case http.MethodGet: + t.Execute(w, p) + + case http.MethodPost: + if err = r.ParseForm(); err != nil { + log.Printf("web/register.go: 400 Bad Request :: %s", err.Error()) + http.Error(w, "400 Bad Request", http.StatusBadRequest) + return + } + + // Get form values + u := account.User{} + u.Username = r.FormValue("username") + u.Password = r.FormValue("password") + + // Perform registration + err = u.Register(db) + + if err != nil { + log.Printf("web/register.go: %s :: %s", + "registration failed", + err.Error()) + + error := []string{} + error = append(error, + fmt.Sprintf("Registration failed")) + + // Check if the error was because of username + // not being unique. + if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") { + error = append(error, + fmt.Sprintf("Username not unique")) + } + p.Error = error + } else { + success := []string{} + success = append(success, + fmt.Sprintf("Registration successful")) + p.Success = success + } + + t.Execute(w, p) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + log.Printf("web/register.go: %v not allowed on %v", r.Method, r.URL) + } + +} diff --git a/password/check.go b/password/check.go new file mode 100644 index 0000000..b84e9fe --- /dev/null +++ b/password/check.go @@ -0,0 +1,13 @@ +// Password package contains functions related to passwords. +package password + +import "golang.org/x/crypto/bcrypt" + +// Check takes a string and hash as input and returns an error. If +// the error is not nil then the consider the password wrong. We're +// returning error instead of a bool so that we can print failed +// logins to log and logging shouldn't happen here. +func Check(password, hash string) error { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err +} diff --git a/password/check_test.go b/password/check_test.go new file mode 100644 index 0000000..e55c169 --- /dev/null +++ b/password/check_test.go @@ -0,0 +1,38 @@ +package password + +import "testing" + +// TestCheck tests the Check function. +func TestCheck(t *testing.T) { + var err error + passhash := make(map[string]string) + + // First we check with static values, these should always pass. + passhash["ter4cQ=="] = "$2a$10$ANkaNEFFQ4zxDwTwvAUfoOCqpVIdgtPFopFOTMSrFy39WkaMAYLIC" + passhash["G29J6A=="] = "$2a$10$1oH1PyhncIHcHJWbLt3Gv.OjClUoFoaEDaFpQ9E9atfRbIrVxsbwm" + passhash["Z1S/kQ=="] = "$2a$10$fZ05kKmb7bh4vBLebpK1u.3bUNQ6eeX5ghT/GZaekgS.5bx4.Ru1e" + passhash["J861dQ=="] = "$2a$10$nXb6Btn6n3AWMAUkDh9bFObvQw5V9FLKhfX.E1EzRWgVDuqIp99u2" + + // We also check with values generated with Hash, this may + // fail if Hash itself fails in that case it's not Check error + // so the test shouldn't fail but warning should be sent. We + // use genID func to generate random inputs for this test. + for i := 1; i <= 4; i++ { + p := RandStr(8) + passhash[p], err = Hash(p) + if err != nil { + t.Log("hashPass func failed") + } + } + + // We test the Check func by ranging over all values of + // passhash. We assume that Hash func returns correct hashes. + for p, h := range passhash { + err = Check(p, h) + if err != nil { + t.Errorf("password: %s, hash: %s didn't match.", + p, h) + } + } + +} diff --git a/password/hash.go b/password/hash.go new file mode 100644 index 0000000..3dd949b --- /dev/null +++ b/password/hash.go @@ -0,0 +1,11 @@ +package password + +import "golang.org/x/crypto/bcrypt" + +// Hash takes a string as input and returns the hash of the +// password. +func Hash(password string) (string, error) { + // 10 is the default cost. + out, err := bcrypt.GenerateFromPassword([]byte(password), 10) + return string(out), err +} diff --git a/password/hash_test.go b/password/hash_test.go new file mode 100644 index 0000000..4f37393 --- /dev/null +++ b/password/hash_test.go @@ -0,0 +1,34 @@ +package password + +import "testing" + +// TestHash tests the Hash function. +func TestHash(t *testing.T) { + var err error + passhash := make(map[string]string) + + // We generate random hashes with Hash, random string is + // generate by RandStr func. + for i := 1; i <= 8; i++ { + p := RandStr(8) + passhash[p], err = Hash(p) + + // Here we test if the hashPass func runs sucessfully. + if err != nil { + t.Errorf("Hash func failed for password: %s", + p) + } + } + + // Here we are testing if the hashPass func returns correct + // hashes. We assume that checkPass func returns correct + // values. + for p, h := range passhash { + err = Check(p, h) + if err != nil { + t.Errorf("password: %s, hash: %s didn't match.", + p, h) + } + } + +} diff --git a/password/randstr.go b/password/randstr.go new file mode 100644 index 0000000..86ae9a1 --- /dev/null +++ b/password/randstr.go @@ -0,0 +1,13 @@ +package password + +import ( + "crypto/rand" + "encoding/base64" +) + +// RandStr will return a random base64 encoded string of length n. +func RandStr(n int) string { + b := make([]byte, n/2) + rand.Read(b) + return base64.StdEncoding.EncodeToString(b) +} diff --git a/storage/init.go b/storage/init.go new file mode 100644 index 0000000..2103498 --- /dev/null +++ b/storage/init.go @@ -0,0 +1,83 @@ +package storage + +import ( + "database/sql" + "log" + "os" + + _ "github.com/mattn/go-sqlite3" +) + +// initErr will log the error and close the database connection if +// necessary. +func initErr(db *DB, err error) { + if db.Conn != nil { + db.Conn.Close() + } + log.Fatalf("Initialization Error :: %s", err.Error()) +} + +func initDB(db *DB) { + var err error + + // We set the database path, first the environment variable + // PERSEUS_DBPATH is checked. If it doesn't exist then use set + // it to the default (perseus.db). Note that this is LookupEnv + // so if the user has set PERSEUS_DBPATH="" then it'll return + // true for exists as it should because technically user has + // set the env var, the sql.Open statement will fail though. + envDBPath, exists := os.LookupEnv("PERSEUS_DBPATH") + if !exists { + envDBPath = "perseus.db" + } + db.Path = envDBPath + + db.Conn, err = sql.Open("sqlite3", db.Path) + if err != nil { + log.Printf("sqlite3/init.go: %s\n", + "failed to open database connection") + initErr(db, err) + } + + sqlstmt := []string{ + // Users can login with multiple devices and so + // multiple tokens will be created. This shouldn't be + // used for login, logins should be verified with + // users table only. + `CREATE TABLE IF NOT EXISTS access ( + id TEXT NOT NULL, + token TEXT NOT NULL, + genTime TEXT NOT NULL);`, + + `CREATE TABLE IF NOT EXISTS accounts ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL DEFAULT user, + username VARCHAR(128) NOT NULL UNIQUE, + hash TEXT NOT NULL, + regTime TEXT NOT NULL);`, + } + + // We range over statements and execute them one by one, this + // is during initialization so it doesn't matter if it takes + // few more ms. This way we know which statement caused the + // program to fail. + for _, s := range sqlstmt { + stmt, err := db.Conn.Prepare(s) + + if err != nil { + log.Printf("sqlite3/init.go: %s\n", + "failed to prepare statement") + log.Println(s) + initErr(db, err) + } + + _, err = stmt.Exec() + stmt.Close() + if err != nil { + log.Printf("sqlite3/init.go: %s\n", + "failed to execute statement") + log.Println(s) + initErr(db, err) + } + } +} diff --git a/storage/sqlite3/db.go b/storage/sqlite3/db.go deleted file mode 100644 index e949bba..0000000 --- a/storage/sqlite3/db.go +++ /dev/null @@ -1,13 +0,0 @@ -package sqlite3 - -import ( - "database/sql" - "sync" -) - -// DB holds the database connection, mutex & path. -type DB struct { - Path string - Mu *sync.RWMutex - Conn *sql.DB -} diff --git a/storage/sqlite3/init.go b/storage/sqlite3/init.go deleted file mode 100644 index e79d3ff..0000000 --- a/storage/sqlite3/init.go +++ /dev/null @@ -1,84 +0,0 @@ -package sqlite3 - -import ( - "database/sql" - "log" - "os" - - _ "github.com/mattn/go-sqlite3" -) - -// initErr will log the error and close the database connection if -// necessary. -func initErr(db *DB, err error) { - if db.Conn != nil { - db.Conn.Close() - } - log.Fatalf("Initialization Error :: %s", err.Error()) -} - -// Init initializes a sqlite3 database. -func Init(db *DB) { - var err error - - // We set the database path, first the environment variable - // PERSEUS_DBPATH is checked. If it doesn't exist then use set - // it to the default (perseus.db). Note that this is LookupEnv - // so if the user has set PERSEUS_DBPATH="" then it'll return - // true for exists as it should because technically user has - // set the env var, the sql.Open statement will fail though. - envDBPath, exists := os.LookupEnv("PERSEUS_DBPATH") - if !exists { - envDBPath = "perseus.db" - } - db.Path = envDBPath - - db.Conn, err = sql.Open("sqlite3", db.Path) - if err != nil { - log.Printf("sqlite3/init.go: %s\n", - "Failed to open database connection") - initErr(db, err) - } - - sqlstmt := []string{ - // Users can login with multiple devices and so - // multiple tokens will be created. This shouldn't be - // used for login, logins should be verified with - // users table only. - `CREATE TABLE IF NOT EXISTS access ( - id TEXT NOT NULL, - token TEXT NOT NULL, - genTime TEXT NOT NULL);`, - - `CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - type TEXT NOT NULL DEFAULT user, - username VARCHAR(128) NOT NULL UNIQUE, - password TEXT NOT NULL, - regTime TEXT NOT NULL);`, - } - - // We range over statements and execute them one by one, this - // is during initialization so it doesn't matter if it takes - // few more ms. This way we know which statement caused the - // program to fail. - for _, s := range sqlstmt { - stmt, err := db.Conn.Prepare(s) - - if err != nil { - log.Printf("sqlite3/init.go: %s\n", - "Failed to prepare statement") - log.Println(s) - initErr(db, err) - } - - _, err = stmt.Exec() - stmt.Close() - if err != nil { - log.Printf("sqlite3/init.go: %s\n", - "Failed to execute statement") - log.Println(s) - initErr(db, err) - } - } -} diff --git a/storage/storage.go b/storage/storage.go index 24f770d..33dd29f 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -1,17 +1,23 @@ package storage import ( + "database/sql" "sync" - - "tildegit.org/andinus/perseus/storage/sqlite3" ) +// DB holds the database connection, mutex & path. +type DB struct { + Path string + Mu *sync.RWMutex + Conn *sql.DB +} + // Init initializes the database. -func Init() *sqlite3.DB { - var db sqlite3.DB = sqlite3.DB{ +func Init() *DB { + db := DB{ Mu: new(sync.RWMutex), } - sqlite3.Init(&db) + initDB(&db) return &db } diff --git a/user/adduser.go b/user/adduser.go deleted file mode 100644 index d1dbcde..0000000 --- a/user/adduser.go +++ /dev/null @@ -1,42 +0,0 @@ -package user - -import ( - "log" - "time" - - "tildegit.org/andinus/perseus/storage/sqlite3" -) - -// AddUser adds the user to record. -func (u User) AddUser(db *sqlite3.DB) error { - // Acquire write lock on the database. - db.Mu.Lock() - defer db.Mu.Unlock() - - // Start the transaction - tx, err := db.Conn.Begin() - if err != nil { - log.Printf("user/adduser.go: %s\n", - "failed to begin transaction") - return err - } - - usrStmt, err := db.Conn.Prepare(` -INSERT INTO users(id, username, password, regTime) values(?, ?, ?, ?)`) - if err != nil { - log.Printf("user/adduser.go: %s\n", - "failed to prepare statement") - return err - } - defer usrStmt.Close() - - _, err = usrStmt.Exec(u.ID, u.Username, u.Password, time.Now().UTC()) - if err != nil { - log.Printf("user/adduser.go: %s\n", - "failed to execute statement") - return err - } - - tx.Commit() - return err -} diff --git a/user/getid.go b/user/getid.go deleted file mode 100644 index 9cf8870..0000000 --- a/user/getid.go +++ /dev/null @@ -1,29 +0,0 @@ -package user - -import ( - "log" - - "tildegit.org/andinus/perseus/storage/sqlite3" -) - -// GetID returns id from username. -func (u *User) GetID(db *sqlite3.DB) error { - // Get password for this user from the database. - stmt, err := db.Conn.Prepare("SELECT id FROM users WHERE username = ?") - if err != nil { - log.Printf("user/getid.go: %s\n", - "failed to prepare statement") - return err - } - defer stmt.Close() - - var id string - err = stmt.QueryRow(u.Username).Scan(&id) - if err != nil { - log.Printf("user/getid.go: %s\n", - "query failed") - } - u.ID = id - - return err -} diff --git a/user/user.go b/user/user.go deleted file mode 100644 index 078215e..0000000 --- a/user/user.go +++ /dev/null @@ -1,9 +0,0 @@ -package user - -// User holds information about the user. -type User struct { - ID string - Username string - Password string - Hash string -} diff --git a/web/login.html b/web/login.html deleted file mode 100644 index cd0d6a7..0000000 --- a/web/login.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - Login · Perseus - - - - - -
-

Perseus

{{ if .SafeList }} - {{end}} {{ if .List }} - {{end}} {{ if .Error }} - {{end}} {{ if .Success }} - {{end}} {{ if .Notice }} - {{end}} -
-

Username

- -

Password

- - -
-
-
-

- Andinus -  /  - Perseus - - Perseus {{ .Version }} -  /  - - Source Code - - -

-
- - diff --git a/web/register.html b/web/register.html deleted file mode 100644 index c351ae1..0000000 --- a/web/register.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - Register · Perseus - - - - - -
-

Perseus

{{ if .SafeList }} - {{end}} {{ if .List }} - {{end}} {{ if .Error }} - {{end}} {{ if .Success }} - {{end}} {{ if .Notice }} - {{end}} -
-

Username

- -

Password

- - -
-
-
-

- Andinus -  /  - Perseus - - Perseus {{ .Version }} -  /  - - Source Code - - -

-
- - diff --git a/web/templates/login.html b/web/templates/login.html new file mode 100644 index 0000000..88024e3 --- /dev/null +++ b/web/templates/login.html @@ -0,0 +1,52 @@ + + + + + + Login · Perseus + + + + + +
+

Perseus

{{ if .SafeList }} + {{end}} {{ if .List }} + {{end}} {{ if .Error }} + {{end}} {{ if .Success }} + {{end}} {{ if .Notice }} + {{end}} +
+

Username

+ +

Password

+ + +
+
+
+

+ Andinus +  /  + Perseus + + Perseus {{ if .Version}} {{ . }} {{ end }} +  /  + + Source Code + + +

+
+ + diff --git a/web/templates/register.html b/web/templates/register.html new file mode 100644 index 0000000..998e012 --- /dev/null +++ b/web/templates/register.html @@ -0,0 +1,52 @@ + + + + + + Register · Perseus + + + + + +
+

Perseus

{{ if .SafeList }} + {{end}} {{ if .List }} + {{end}} {{ if .Error }} + {{end}} {{ if .Success }} + {{end}} {{ if .Notice }} + {{end}} +
+

Username

+ +

Password

+ + +
+
+
+

+ Andinus +  /  + Perseus + + Perseus {{ if .Version}} {{ . }} {{ end }} +  /  + + Source Code + + +

+
+ + -- cgit 1.4.1-2-gfad0