summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--account/addtoken.go56
-rw-r--r--account/adduser.go43
-rw-r--r--account/getid.go33
-rw-r--r--account/login.go50
-rw-r--r--account/register.go49
-rw-r--r--account/user.go (renamed from user/user.go)3
-rw-r--r--auth/genid.go14
-rw-r--r--auth/hashpass.go13
-rw-r--r--build/ci/drone.yml6
-rw-r--r--cmd/perseus/main.go13
-rw-r--r--core/version.go6
-rw-r--r--handler/web/login.go81
-rw-r--r--handler/web/page.go7
-rw-r--r--handler/web/register.go80
-rw-r--r--password/check.go (renamed from auth/checkpass.go)11
-rw-r--r--password/check_test.go (renamed from auth/checkpass_test.go)26
-rw-r--r--password/hash.go11
-rw-r--r--password/hash_test.go (renamed from auth/hashpass_test.go)18
-rw-r--r--password/randstr.go13
-rw-r--r--storage/init.go (renamed from storage/sqlite3/init.go)15
-rw-r--r--storage/sqlite3/db.go13
-rw-r--r--storage/storage.go16
-rw-r--r--user/adduser.go42
-rw-r--r--user/getid.go29
-rw-r--r--web/templates/login.html (renamed from web/login.html)2
-rw-r--r--web/templates/register.html (renamed from web/register.html)2
26 files changed, 475 insertions, 177 deletions
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/user/user.go b/account/user.go
index 078215e..769e8fc 100644
--- a/user/user.go
+++ b/account/user.go
@@ -1,4 +1,4 @@
-package user
+package account
 
 // User holds information about the user.
 type User struct {
@@ -6,4 +6,5 @@ type User struct {
 	Username string
 	Password string
 	Hash     string
+	Token    string
 }
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/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/auth/checkpass.go b/password/check.go
index 64e3c9f..b84e9fe 100644
--- a/auth/checkpass.go
+++ b/password/check.go
@@ -1,14 +1,13 @@
-package auth
+// Password package contains functions related to passwords.
+package password
 
-import (
-	"golang.org/x/crypto/bcrypt"
-)
+import "golang.org/x/crypto/bcrypt"
 
-// checkPass takes a string and hash as input and returns an error. If
+// 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 checkPass(password, hash string) error {
+func Check(password, hash string) error {
 	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
 	return err
 }
diff --git a/auth/checkpass_test.go b/password/check_test.go
index c8afaee..e55c169 100644
--- a/auth/checkpass_test.go
+++ b/password/check_test.go
@@ -1,9 +1,9 @@
-package auth
+package password
 
 import "testing"
 
-// TestCheckPass tests the checkPass function.
-func TestCheckPass(t *testing.T) {
+// TestCheck tests the Check function.
+func TestCheck(t *testing.T) {
 	var err error
 	passhash := make(map[string]string)
 
@@ -13,24 +13,22 @@ func TestCheckPass(t *testing.T) {
 	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.
+	// 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 := genID(8)
-		passhash[p], err = hashPass(p)
+		p := RandStr(8)
+		passhash[p], err = Hash(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.
+	// 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 = checkPass(p, h)
+		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/auth/hashpass_test.go b/password/hash_test.go
index 7ea9480..4f37393 100644
--- a/auth/hashpass_test.go
+++ b/password/hash_test.go
@@ -1,21 +1,21 @@
-package auth
+package password
 
 import "testing"
 
-// TestHashPass tests the checkPass function.
-func TestHashPass(t *testing.T) {
+// TestHash tests the Hash function.
+func TestHash(t *testing.T) {
 	var err error
 	passhash := make(map[string]string)
 
-	// We generate random hashes with hashPass, random string is
-	// generate by genID func.
+	// We generate random hashes with Hash, random string is
+	// generate by RandStr func.
 	for i := 1; i <= 8; i++ {
-		p := genID(8)
-		passhash[p], err = hashPass(p)
+		p := RandStr(8)
+		passhash[p], err = Hash(p)
 
 		// Here we test if the hashPass func runs sucessfully.
 		if err != nil {
-			t.Errorf("hashPass func failed for password: %s",
+			t.Errorf("Hash func failed for password: %s",
 				p)
 		}
 	}
@@ -24,7 +24,7 @@ func TestHashPass(t *testing.T) {
 	// hashes. We assume that checkPass func returns correct
 	// values.
 	for p, h := range passhash {
-		err = checkPass(p, h)
+		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/sqlite3/init.go b/storage/init.go
index e79d3ff..2103498 100644
--- a/storage/sqlite3/init.go
+++ b/storage/init.go
@@ -1,4 +1,4 @@
-package sqlite3
+package storage
 
 import (
 	"database/sql"
@@ -17,8 +17,7 @@ func initErr(db *DB, err error) {
 	log.Fatalf("Initialization Error :: %s", err.Error())
 }
 
-// Init initializes a sqlite3 database.
-func Init(db *DB) {
+func initDB(db *DB) {
 	var err error
 
 	// We set the database path, first the environment variable
@@ -36,7 +35,7 @@ func Init(db *DB) {
 	db.Conn, err = sql.Open("sqlite3", db.Path)
 	if err != nil {
 		log.Printf("sqlite3/init.go: %s\n",
-			"Failed to open database connection")
+			"failed to open database connection")
 		initErr(db, err)
 	}
 
@@ -50,11 +49,11 @@ func Init(db *DB) {
        token    TEXT NOT NULL,
        genTime TEXT NOT NULL);`,
 
-		`CREATE TABLE IF NOT EXISTS users (
+		`CREATE TABLE IF NOT EXISTS accounts (
        id       TEXT PRIMARY KEY,
        type     TEXT NOT NULL DEFAULT user,
        username VARCHAR(128) NOT NULL UNIQUE,
-       password TEXT NOT NULL,
+       hash     TEXT NOT NULL,
        regTime  TEXT NOT NULL);`,
 	}
 
@@ -67,7 +66,7 @@ func Init(db *DB) {
 
 		if err != nil {
 			log.Printf("sqlite3/init.go: %s\n",
-				"Failed to prepare statement")
+				"failed to prepare statement")
 			log.Println(s)
 			initErr(db, err)
 		}
@@ -76,7 +75,7 @@ func Init(db *DB) {
 		stmt.Close()
 		if err != nil {
 			log.Printf("sqlite3/init.go: %s\n",
-				"Failed to execute statement")
+				"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/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/web/login.html b/web/templates/login.html
index cd0d6a7..88024e3 100644
--- a/web/login.html
+++ b/web/templates/login.html
@@ -40,7 +40,7 @@
 	&nbsp;/&nbsp;
 	<a href="https://andinus.nand.sh/perseus">Perseus</a>
 	<span style="float:right">
-	  Perseus {{ .Version }}
+	  Perseus {{ if .Version}} {{ . }} {{ end }}
 	  &nbsp;/&nbsp;
 	  <a href="https://tildegit.org/andinus/perseus">
 	    Source Code
diff --git a/web/register.html b/web/templates/register.html
index c351ae1..998e012 100644
--- a/web/register.html
+++ b/web/templates/register.html
@@ -40,7 +40,7 @@
 	&nbsp;/&nbsp;
 	<a href="https://andinus.nand.sh/perseus">Perseus</a>
 	<span style="float:right">
-	  Perseus {{ .Version }}
+	  Perseus {{ if .Version}} {{ . }} {{ end }}
 	  &nbsp;/&nbsp;
 	  <a href="https://tildegit.org/andinus/perseus">
 	    Source Code