commit 8bc3cee328b54bc3ccaac812632fed9d4253760e
Author: Jimmy <me@jimmy.nz>
Date:   Thu Feb 24 01:07:09 2022 +1300

    Inital commit

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..93092cc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+# Otfe
+
+A MVC based website template written in Go
+
+There is currently a security flaw in the session code.
+
+Code should only be used as reference.
+
diff --git a/app.go b/app.go
new file mode 100644
index 0000000..00c0055
--- /dev/null
+++ b/app.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+
+	c "git.1248.nz/1248/Otfe/controllers"
+	a "git.1248.nz/1248/Otfe/misc/auth"
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"github.com/husobee/vestigo"
+)
+
+func main() {
+
+	fmt.Println("Starting")
+	log.Fatal(http.ListenAndServe(":8080", router()))
+}
+
+func router() *vestigo.Router {
+	router := vestigo.NewRouter()
+
+	var static c.Static
+	router.Get("/", a.User(static.Home))
+
+	router.Get("/public/*", http.FileServer(
+		http.Dir(helpers.GetAssets())).ServeHTTP)
+
+	//User routes
+	var user c.User
+	router.Get("/user", c.User{}.Index)
+	router.Get("/user/:username", a.Perm(user.Show, user.ShowSelf,
+		"user.show"))
+	router.Get("/user/new", user.New)
+	router.Post("/user/new", user.Create)
+	router.Get("/user/:username/edit", user.Edit)
+	router.Post("/user/:username/edit", user.Update)
+	router.Post("/user/:username/delete", user.Delete)
+	router.Get("/register", user.New)
+
+	//Session routes
+	var session c.Session
+	router.Get("/login", session.New)
+	router.Post("/login", session.Create)
+	router.Post("/logout", session.Delete)
+
+	//Post routes
+	router.Get("/post", c.Post{}.Index)
+	router.Get("/user/:id", c.Post{}.Show)
+	router.Get("/post/new", c.Post{}.New)
+	router.Post("/post/new", c.Post{}.Create)
+	router.Get("/post/:id/edit", c.Post{}.Edit)
+	router.Post("/post/:id/edit", c.Post{}.Update)
+	router.Post("/post/:id/delete", c.Post{}.Delete)
+
+	return router
+}
diff --git a/app/seed_test.go b/app/seed_test.go
new file mode 100644
index 0000000..9a11154
--- /dev/null
+++ b/app/seed_test.go
@@ -0,0 +1,43 @@
+package main
+
+import (
+	"testing"
+
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"git.1248.nz/1248/Otfe/models"
+	"github.com/globalsign/mgo/bson"
+)
+
+func TestSeed(t *testing.T) {
+	models.DBWipeCollection("group", "user", "session")
+	//admin user and group
+	adminGroup := models.NewGroup("admin")
+	adminGroup.Admin = true
+	adminGroup.ID = bson.NewObjectId()
+	adminGroup.Permissions["user.show"] = true
+
+	admin := models.User{}
+	admin.Username = "admin"
+	admin.Email = "admin"
+	admin.ID = bson.NewObjectId()
+	admin.Password, _ = helpers.HashPassword("admin")
+	admin.PrimaryGroup = adminGroup.ID
+	adminGroup.Users = append(adminGroup.Users, admin.ID)
+	helpers.Ok(t, adminGroup.Create())
+	helpers.Ok(t, admin.Create())
+
+	//user and user group
+	userGroup := models.NewGroup("user")
+	userGroup.ID = bson.NewObjectId()
+	userGroup.Admin = false
+	user := models.User{}
+	user.ID = bson.NewObjectId()
+	user.Username = "user"
+	user.Email = "u"
+	user.Password, _ = helpers.HashPassword("user")
+	user.PrimaryGroup = userGroup.ID
+	userGroup.Users = append(userGroup.Users, user.ID)
+	helpers.Ok(t, user.Create())
+	helpers.Ok(t, userGroup.Create())
+
+}
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000..afb07e5
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,8 @@
+[database]
+Host="localhost"
+Name="test"
+
+[session]
+SecretKey="ef48c62fae0d0e7fb295fbdf5232b8e578e58ff7c20bf28b4e61b0f24df8362e5b2a6f55d402c279360e95918ae35b79c1afd784d53a487c9414a0c0d79a74dd"
+SessionKey="otfe"
+timeout=3600 #in sec
\ No newline at end of file
diff --git a/controllers/controller.go b/controllers/controller.go
new file mode 100644
index 0000000..b273c2f
--- /dev/null
+++ b/controllers/controller.go
@@ -0,0 +1,33 @@
+package controllers
+
+import (
+	"html/template"
+	"net/http"
+
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"github.com/globalsign/mgo/bson"
+)
+
+/*type Controller interface {
+	Index(w http.ResponseWriter, r *http.Request)
+	Show(w http.ResponseWriter, r *http.Request)
+	New(w http.ResponseWriter, r *http.Request)
+	Create(w http.ResponseWriter, r *http.Request)
+	Edit(w http.ResponseWriter, r *http.Request)
+	Update(w http.ResponseWriter, r *http.Request)
+	Delete(w http.ResponseWriter, r *http.Request)
+}*/
+
+var funcMap = template.FuncMap{
+	"getId": func(id bson.ObjectId) string {
+		return "1"
+	},
+}
+
+func t(w http.ResponseWriter, data interface{}, layout string) {
+	views := helpers.GetRootDir() + "/views/"
+	tmpl := template.Must(template.New("layout").Funcs(funcMap).
+		ParseFiles(views+"/layouts/layout.gtpl", views+"/layouts/header.gtpl", views+"/layouts/footer.gtpl", views+"/layouts/nav.gtpl", views+layout))
+	tmpl.ExecuteTemplate(w, "layout", data)
+
+}
diff --git a/controllers/post.go b/controllers/post.go
new file mode 100644
index 0000000..695b943
--- /dev/null
+++ b/controllers/post.go
@@ -0,0 +1,66 @@
+package controllers
+
+import (
+	"fmt"
+	"net/http"
+
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"git.1248.nz/1248/Otfe/models"
+	"github.com/husobee/vestigo"
+)
+
+//User handlers
+type Post struct {
+	Title string
+}
+
+//Index of posts
+func (p Post) Index(w http.ResponseWriter, r *http.Request) {
+	p.Title = "Posts"
+	t(w, p, "/post/posts.gtpl")
+}
+
+//Show given user
+func (p Post) Show(w http.ResponseWriter, r *http.Request) {
+	t(w, p, "/post/post.gtpl")
+}
+
+//New user form
+func (p Post) New(w http.ResponseWriter, r *http.Request) {
+	t(w, p, "/post/new.gtpl")
+}
+
+//Create new a user
+func (p Post) Create(w http.ResponseWriter, r *http.Request) {
+	r.ParseForm()
+	var user models.User
+	var err error
+	user.Username = r.Form.Get("username")
+	user.Email = r.Form.Get("email")
+	user.Password, err = helpers.HashPassword(r.Form.Get("password"))
+	helpers.CheckError(err)
+	user.Create()
+	http.Redirect(w, r, "/user/"+user.Username, http.StatusFound)
+
+}
+
+//Edit form
+func (p Post) Edit(w http.ResponseWriter, r *http.Request) {
+	var data userData
+	data.User.Read("username", vestigo.Param(r, "username"))
+
+}
+
+//Update user
+func (p Post) Update(w http.ResponseWriter, r *http.Request) {
+
+}
+
+//Delete user
+func (p Post) Delete(w http.ResponseWriter, r *http.Request) {
+	fmt.Println("Deleting " + vestigo.Param(r, "username"))
+	var user models.User
+	user.Delete("username", vestigo.Param(r, "username"))
+	http.Redirect(w, r, "/user", http.StatusFound)
+
+}
diff --git a/controllers/session.go b/controllers/session.go
new file mode 100644
index 0000000..88f1c5f
--- /dev/null
+++ b/controllers/session.go
@@ -0,0 +1,72 @@
+package controllers
+
+import (
+	"errors"
+	"net/http"
+
+	"git.1248.nz/1248/Otfe/misc/cookie"
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"git.1248.nz/1248/Otfe/misc/rand"
+	"git.1248.nz/1248/Otfe/models"
+)
+
+//Session controllers
+type Session struct{}
+
+type pageData struct {
+	Title string
+	Err   string
+	User  models.User
+}
+
+//New login form
+func (s *Session) New(w http.ResponseWriter, r *http.Request) {
+	var err error
+	data := pageData{Title: "Login"}
+	data.Err, err = cookie.Read(r, "error")
+	if err == nil {
+		cookie.Delete(w, "error")
+	}
+	t(w, data, "/static/login.gtpl")
+}
+
+//Create a new session
+func (s *Session) Create(w http.ResponseWriter, r *http.Request) {
+	r.ParseForm()
+	//Get email and password and check they are not empty
+	email := r.Form.Get("email")
+	password := r.Form.Get("password")
+
+	//Check if user exists
+	var user models.User
+
+	//Check password is correct
+	if user.Read("email", email) == nil &&
+		helpers.CheckPasswordHash(password, user.Password) == nil {
+		id, _ := rand.B64String(32)
+		sess := models.Session{ID: id, UserID: user.ID}
+		sess.Create()
+		cookie.Create(w, "session", sess.ID)
+		http.Redirect(w, r, "/", http.StatusFound)
+	} else {
+		loginFail(w, r, errors.New("Email or password incorrect"))
+	}
+}
+
+//Delete session
+func (s *Session) Delete(w http.ResponseWriter, r *http.Request) {
+	id, err := cookie.Read(r, "session")
+	//Check user is logged in
+	if err == nil {
+		cookie.Delete(w, "session")
+		var session models.Session
+		session.Delete(id)
+		http.Redirect(w, r, "/", http.StatusFound)
+	}
+
+}
+
+func loginFail(w http.ResponseWriter, r *http.Request, err error) {
+	cookie.Create(w, "error", err.Error())
+	http.Redirect(w, r, "/login", http.StatusFound)
+}
diff --git a/controllers/session_test.go b/controllers/session_test.go
new file mode 100644
index 0000000..913a61a
--- /dev/null
+++ b/controllers/session_test.go
@@ -0,0 +1,102 @@
+package controllers_test
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+	"testing"
+
+	"git.1248.nz/1248/Otfe/controllers"
+	"git.1248.nz/1248/Otfe/misc/b64"
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"git.1248.nz/1248/Otfe/models"
+)
+
+func TestSessionNew(t *testing.T) {
+	var s controllers.Session
+	handler := http.HandlerFunc(s.New)
+	req, err := http.NewRequest("GET", "/login", nil)
+	w := httptest.NewRecorder()
+	if err != nil {
+		t.Fatal(err)
+	}
+	handler(w, req)
+	body := w.Body.String()
+	if !strings.Contains(body, "<title>Login</title>") {
+		t.Fail()
+	}
+}
+
+func TestSessionCreate(t *testing.T) {
+	//Create test user
+
+	var s controllers.Session
+	handler := http.HandlerFunc(s.Create)
+	req, w := setup(t, "POST", "/login")
+
+	addTofForm(req, "email=test", "password=test")
+	handler(w, req)
+
+	errorMessage := getCookie("error", w.Header()["Set-Cookie"])
+	t.Log(errorMessage)
+	t.Log(b64.Decode(errorMessage))
+
+	header := w.Header()
+	sessionid := getCookie("session", header["Set-Cookie"])
+	var session models.Session
+	if session.Read(sessionid) != nil {
+		t.Fatal("Could not find session")
+	}
+
+}
+
+/*func testloginFail(t *testing.T) {
+
+}*/
+
+func setup(t *testing.T, method string, url string) (*http.Request, *httptest.ResponseRecorder) {
+	req, err := http.NewRequest("POST", "/login", nil)
+	w := httptest.NewRecorder()
+	if err != nil {
+		t.Fatal(err)
+	}
+	return req, w
+
+}
+
+func createUser(t *testing.T, email string, password string) models.User {
+
+	password, err := helpers.HashPassword(password)
+	if err != nil {
+		t.Fatal("Failed to create password")
+	}
+	user := models.User{Email: email, Password: password}
+	if user.Create() != nil {
+		t.Fatal("failed to create user")
+	}
+
+	return user
+}
+
+func getCookie(name string, cookies []string) string {
+	for _, cookie := range cookies {
+		a := strings.Split(cookie, "=")
+		if a[0] == name {
+			return a[1]
+
+		}
+
+	}
+	return "Cookie not found"
+}
+
+func addTofForm(r *http.Request, values ...string) {
+	form, _ := url.ParseQuery(r.URL.RawQuery)
+	for _, value := range values {
+		v := strings.Split(value, "=")
+		form.Add(v[0], v[1])
+	}
+	r.URL.RawQuery = form.Encode()
+	r.Form.Encode()
+}
diff --git a/controllers/static.go b/controllers/static.go
new file mode 100644
index 0000000..a15e998
--- /dev/null
+++ b/controllers/static.go
@@ -0,0 +1,29 @@
+package controllers
+
+import (
+	"net/http"
+
+	"git.1248.nz/1248/Otfe/models"
+)
+
+//Static pages
+type Static struct{}
+
+type staticData struct {
+	Title string
+	User  models.User
+}
+
+type contextKey string
+
+func (c contextKey) String() string {
+	return string(c)
+}
+
+//Home page
+func (s *Static) Home(w http.ResponseWriter, r *http.Request, u models.User) {
+	data := staticData{Title: "Otfe"}
+	data.User = u
+	//fmt.Fprintln(w, data.User)
+	t(w, data, "/static/home.gtpl")
+}
diff --git a/controllers/user.go b/controllers/user.go
new file mode 100644
index 0000000..4edcc49
--- /dev/null
+++ b/controllers/user.go
@@ -0,0 +1,91 @@
+package controllers
+
+import (
+	"fmt"
+	"net/http"
+
+	"git.1248.nz/1248/Otfe/misc/auth"
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"git.1248.nz/1248/Otfe/models"
+	"github.com/husobee/vestigo"
+)
+
+type userData struct {
+	Title string
+	Users []models.User
+	User  models.User
+}
+
+//User handlers
+type User struct{}
+
+//Index list all users
+func (u User) Index(w http.ResponseWriter, r *http.Request) {
+	var err error
+	data := userData{Title: "Users"}
+	data.Users, err = data.User.ReadAll()
+	helpers.CheckError(err)
+	t(w, data, "/user/users.gtpl")
+}
+
+//Show given user
+func (u *User) Show(w http.ResponseWriter, r *http.Request, user models.User) {
+	var data userData
+	data.User.Read("username", vestigo.Param(r, "username"))
+	//matchUser(data.User, w, r)
+	data.Title = data.User.Username
+	t(w, data, "/user/user.gtpl")
+}
+
+//ShowSelf show given user if they are the same as the authenticated one
+func (u *User) ShowSelf(w http.ResponseWriter, r *http.Request, user models.User) {
+	if user.Username != vestigo.Param(r, "username") {
+		auth.UnAuth(w)
+		return
+	}
+	var data userData
+	data.User = user
+	data.Title = data.User.Username
+	t(w, data, "/user/user.gtpl")
+}
+
+//New user form
+func (u *User) New(w http.ResponseWriter, r *http.Request) {
+	data := userData{Title: "New User"}
+	t(w, data, "/user/new.gtpl")
+}
+
+//Create new a user
+func (u *User) Create(w http.ResponseWriter, r *http.Request) {
+	r.ParseForm()
+	var user models.User
+	var err error
+	user.Username = r.Form.Get("username")
+	user.Email = r.Form.Get("email")
+	user.Password, err = helpers.HashPassword(r.Form.Get("password"))
+	helpers.CheckError(err)
+	user.Create()
+	http.Redirect(w, r, "/user/"+user.Username, http.StatusFound)
+
+}
+
+//Edit form
+func (u *User) Edit(w http.ResponseWriter, r *http.Request) {
+	var data userData
+	data.User.Read("username", vestigo.Param(r, "username"))
+
+}
+
+//Update user
+func (u *User) Update(w http.ResponseWriter, r *http.Request) {
+
+}
+
+//Delete user
+func (u *User) Delete(w http.ResponseWriter, r *http.Request) {
+	fmt.Println("Deleting " + vestigo.Param(r, "username"))
+	var user models.User
+	user.Delete("username", vestigo.Param(r, "username"))
+	http.Redirect(w, r, "/user", http.StatusFound)
+
+}
diff --git a/misc/auth/auth.go b/misc/auth/auth.go
new file mode 100644
index 0000000..e4a23c0
--- /dev/null
+++ b/misc/auth/auth.go
@@ -0,0 +1,59 @@
+package auth
+
+import (
+	"errors"
+	"net/http"
+
+	"git.1248.nz/1248/Otfe/models"
+)
+
+type auth func(http.ResponseWriter, *http.Request, models.User)
+
+func User(h auth) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		user, _ := getUserSession(r)
+		h(w, r, user)
+	}
+}
+
+func Perm(handler auth, fallback auth, perm string) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		user, err := getUserSession(r)
+		if err != nil {
+			http.Redirect(w, r, "/login", http.StatusFound)
+			return
+		}
+		if user.HasPermission(perm) {
+			handler(w, r, user)
+		} else {
+			if fallback == nil {
+				UnAuth(w)
+			} else {
+				fallback(w, r, user)
+			}
+		}
+
+	}
+
+}
+
+func getUserSession(r *http.Request) (models.User, error) {
+	var session models.Session
+	var user models.User
+	//Check for session in db
+	err := session.Get(r)
+	if err == nil {
+		//Get user associated with the session
+		err = user.Read("_id", session.UserID)
+		if err == nil {
+			return user, nil
+
+		}
+	}
+	return user, errors.New("User not logged in")
+}
+
+func UnAuth(w http.ResponseWriter) {
+	http.Error(w, "You are not authorized to view this page",
+		http.StatusForbidden)
+}
diff --git a/misc/auth/auth_test.go b/misc/auth/auth_test.go
new file mode 100644
index 0000000..e6d6389
--- /dev/null
+++ b/misc/auth/auth_test.go
@@ -0,0 +1,113 @@
+package auth
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"git.1248.nz/1248/Otfe/misc/helpers/cookie"
+	"git.1248.nz/1248/Otfe/models"
+	"github.com/globalsign/mgo/bson"
+)
+
+func TestUser(t *testing.T) {
+	//Setup user with session
+	recorder := httptest.NewRecorder()
+	user, session := userSession(t)
+	request := request(t, session)
+	u := User(handler)
+	//Run
+	u(recorder, request)
+	//Check
+	body := recorder.Body.String()
+	if !strings.Contains(body, user.ID.Hex()) {
+		t.Fail()
+	}
+	//Setup without session
+	recorder = httptest.NewRecorder()
+	request, _ = http.NewRequest("GET", "/", nil)
+	//Run
+	u(recorder, request)
+	//Check
+	helpers.Equals(t, recorder.Body.String(),
+		"{ObjectIdHex(\"\")     ObjectIdHex(\"\") []}")
+
+}
+
+func TestPerm(t *testing.T) {
+	p := Perm(handler, UnAuth, "perm")
+	recorder := httptest.NewRecorder()
+	user, session := userSession(t)
+	request := request(t, session)
+	p(recorder, request)
+	if !strings.Contains(recorder.Body.String(),
+		"You are not authorized to view this page") {
+		t.Log("Authorization fail")
+		t.Fail()
+	}
+
+	p = Perm(handler, UnAuth, "test")
+	recorder = httptest.NewRecorder()
+	p(recorder, request)
+	if !strings.Contains(recorder.Body.String(), user.ID.Hex()) {
+		t.Log("Has permission fail")
+		t.Fail()
+	}
+
+	recorder = httptest.NewRecorder()
+	request, err := http.NewRequest("GET", "/", nil)
+	helpers.Ok(t, err)
+	p(recorder, request)
+	if !strings.Contains(recorder.Body.String(), "login") {
+		t.Log("Login fail")
+		t.Fail()
+	}
+
+}
+
+func TestGetUserSession(t *testing.T) {
+	user, session := userSession(t)
+	request := request(t, session)
+	//Test
+	user2, err := getUserSession(request)
+	helpers.Ok(t, err)
+	helpers.Equals(t, user, user2)
+
+}
+
+func userSession(t *testing.T) (models.User, models.Session) {
+	models.DBWipeCollection("user", "session", "group")
+
+	group := models.NewGroup("test")
+	group.ID = bson.NewObjectId()
+	group.Permissions["test"] = true
+	//group.Admin = true
+	helpers.Ok(t, group.Create())
+
+	user := models.User{Name: "test",
+		Email: "test"}
+	user.ID = bson.NewObjectId()
+	user.PrimaryGroup = group.ID
+	helpers.Ok(t, user.Create())
+
+	session := models.Session{UserID: user.ID}
+	session.ID = bson.NewObjectId()
+	helpers.Ok(t, session.Create())
+	return user, session
+}
+
+func request(t *testing.T, s models.Session) *http.Request {
+	cookie := &http.Cookie{Name: "session",
+		Value: cookie.Encode(s.ID.Hex())}
+	request, err := http.NewRequest("GET", "/", nil)
+	helpers.Ok(t, err)
+	request.AddCookie(cookie)
+	return request
+}
+
+func handler(w http.ResponseWriter, r *http.Request, u models.User) {
+	fmt.Fprint(w, u)
+}
diff --git a/misc/b64/b64.go b/misc/b64/b64.go
new file mode 100644
index 0000000..cd543f5
--- /dev/null
+++ b/misc/b64/b64.go
@@ -0,0 +1,12 @@
+package b64
+
+import "encoding/base64"
+
+func Encode(src string) string {
+	return base64.URLEncoding.EncodeToString([]byte(src))
+}
+
+func Decode(src string) (string, error) {
+	value, err := base64.URLEncoding.DecodeString(src)
+	return string(value), err
+}
diff --git a/misc/config/config.go b/misc/config/config.go
new file mode 100644
index 0000000..411ba56
--- /dev/null
+++ b/misc/config/config.go
@@ -0,0 +1,60 @@
+package config
+
+import (
+	"encoding/hex"
+	"path/filepath"
+
+	"git.1248.nz/1248/Otfe/misc/helpers"
+
+	"github.com/BurntSushi/toml"
+)
+
+//Configuration struct
+type Configuration struct {
+	DB      database `toml:"database"`
+	Session session
+}
+
+// Database stuct
+type database struct {
+	Host     string
+	Name     string
+	User     string
+	Password string
+}
+
+type session struct {
+	SecretKey  string
+	Sessionkey string
+	Timeout    int
+}
+
+var config *Configuration
+
+func init() {
+	Get()
+}
+
+// Get config info from toml config file
+func Get() *Configuration {
+	if config == nil {
+		_, err := toml.DecodeFile(getConfigFile(), &config)
+		helpers.CheckError(err)
+	}
+	return config
+}
+
+func getConfigFile() string {
+	return filepath.Join(helpers.GetRootDir(), "config.toml")
+}
+
+func GetSecretKey() []byte {
+	config := Get()
+	key, err := hex.DecodeString(config.Session.SecretKey)
+	helpers.CheckError(err)
+	return key
+}
+
+func GetSessionKey() string {
+	return Get().Session.Sessionkey
+}
diff --git a/misc/config/config_test.go b/misc/config/config_test.go
new file mode 100644
index 0000000..b9c7582
--- /dev/null
+++ b/misc/config/config_test.go
@@ -0,0 +1,7 @@
+package config
+
+import "testing"
+
+func TestGetConfigFile(t *testing.T) {
+	t.Log(Get())
+}
diff --git a/misc/cookie/cookie.go b/misc/cookie/cookie.go
new file mode 100644
index 0000000..5be2d1c
--- /dev/null
+++ b/misc/cookie/cookie.go
@@ -0,0 +1,31 @@
+package cookie
+
+import (
+	"errors"
+	"net/http"
+	"time"
+
+	"git.1248.nz/1248/Otfe/misc/b64"
+)
+
+func Create(w http.ResponseWriter, name string, value string) {
+	c := &http.Cookie{Name: name, Value: b64.Encode(value)}
+	http.SetCookie(w, c)
+}
+
+func Read(r *http.Request, name string) (string, error) {
+	c, err := r.Cookie(name)
+	if err != nil {
+		return "", errors.New("Cookie not found")
+	}
+	value, err := b64.Decode(c.Value)
+	if err != nil {
+		return "", errors.New("Failed to decode cookie")
+	}
+	return value, nil
+}
+
+func Delete(w http.ResponseWriter, name string) {
+	http.SetCookie(w, &http.Cookie{Name: name, MaxAge: -1, Expires: time.Unix(1, 0)})
+
+}
diff --git a/misc/cookie/cookie_test.go b/misc/cookie/cookie_test.go
new file mode 100644
index 0000000..649246b
--- /dev/null
+++ b/misc/cookie/cookie_test.go
@@ -0,0 +1,39 @@
+package cookie
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"git.1248.nz/1248/Otfe/misc/b64"
+	"git.1248.nz/1248/Otfe/misc/helpers"
+)
+
+func TestCreate(t *testing.T) {
+	recorder := httptest.NewRecorder()
+	Create(recorder, "test", "test")
+	request := &http.Request{Header: http.Header{
+		"Cookie": recorder.HeaderMap["Set-Cookie"]}}
+	cookie, err := request.Cookie("test")
+	if err != nil {
+		t.Fail()
+		return
+	}
+	value, err := b64.Decode(cookie.Value)
+	if err != nil || value != "test" {
+		t.Fail()
+	}
+}
+
+func TestRead(t *testing.T) {
+	cookie := &http.Cookie{Name: "test", Value: b64.Encode("test")}
+
+	request, err := http.NewRequest("GET", "", nil)
+	if err != nil {
+		t.Fail()
+		return
+	}
+	request.AddCookie(cookie)
+	value, err := Read(request, "test")
+	helpers.Equals(t, value, "test")
+}
diff --git a/misc/helpers/helpers.go b/misc/helpers/helpers.go
new file mode 100644
index 0000000..571ce93
--- /dev/null
+++ b/misc/helpers/helpers.go
@@ -0,0 +1,55 @@
+package helpers
+
+import (
+	"crypto/rand"
+	"encoding/hex"
+	"log"
+	"path/filepath"
+	"runtime"
+
+	"golang.org/x/crypto/bcrypt"
+)
+
+//CheckError checks for errors and logs them and stops the program
+func CheckError(err error) bool {
+	if err != nil {
+		log.Fatal(err)
+		return false
+	}
+	return true
+}
+
+func GetRootDir() string {
+	_, b, _, _ := runtime.Caller(0)
+	dir := filepath.Dir(b)
+	return filepath.Dir(filepath.Dir(dir))
+}
+
+func GetAssets() string {
+	return GetRootDir()
+}
+
+func HashPassword(password string) (string, error) {
+	hash, err := bcrypt.GenerateFromPassword([]byte(password), 14)
+	return string(hash), err
+}
+
+func CheckPasswordHash(password, hash string) error {
+	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
+	return err
+}
+
+func RandHex() string {
+	bytes := make([]byte, 12)
+	rand.Read(bytes)
+	return hex.EncodeToString(bytes)
+}
+
+func Bytes(n int) ([]byte, error) {
+	b := make([]byte, n)
+	_, err := rand.Read(b)
+	if err != nil {
+		return nil, err
+	}
+	return b, nil
+}
diff --git a/misc/helpers/helpers_test.go b/misc/helpers/helpers_test.go
new file mode 100644
index 0000000..de8e8bd
--- /dev/null
+++ b/misc/helpers/helpers_test.go
@@ -0,0 +1,35 @@
+package helpers_test
+
+import (
+	"testing"
+
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"git.1248.nz/1248/Otfe/models"
+)
+
+func TestGetRootDir(t *testing.T) {
+	t.Log("Root path:", helpers.GetRootDir())
+}
+
+func TestHashPassword(t *testing.T) {
+	user := models.User{Email: "a@a.com", Username: "a"}
+	user.Delete("username", "a")
+	var err error
+	password := "43539jgifdkvnm4935078uJKJR**$ufjqd98438uiAHFJean89q34JKDFJ"
+	user.Password, err = helpers.HashPassword(password)
+	if err != nil {
+		t.Fail()
+	}
+	user.Create()
+	var user2 models.User
+	user2.Read("username", "a")
+
+	t.Log(helpers.CheckPasswordHash(password, user2.Password))
+
+}
+
+func TestRandHex(t *testing.T) {
+	for i := 0; i < 20; i++ {
+		t.Log(helpers.RandHex())
+	}
+}
diff --git a/misc/helpers/test.go b/misc/helpers/test.go
new file mode 100644
index 0000000..2e04b46
--- /dev/null
+++ b/misc/helpers/test.go
@@ -0,0 +1,36 @@
+package helpers
+
+import (
+	"fmt"
+	"path/filepath"
+	"reflect"
+	"runtime"
+	"testing"
+)
+
+// assert fails the test if the condition is false.
+func Assert(tb testing.TB, condition bool, msg string, v ...interface{}) {
+	if !condition {
+		_, file, line, _ := runtime.Caller(1)
+		fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...)
+		tb.FailNow()
+	}
+}
+
+// ok fails the test if an err is not nil.
+func Ok(tb testing.TB, err error) {
+	if err != nil {
+		_, file, line, _ := runtime.Caller(1)
+		fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error())
+		tb.FailNow()
+	}
+}
+
+// equals fails the test if exp is not equal to act.
+func Equals(tb testing.TB, exp, act interface{}) {
+	if !reflect.DeepEqual(exp, act) {
+		_, file, line, _ := runtime.Caller(1)
+		fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act)
+		tb.FailNow()
+	}
+}
diff --git a/misc/rand/rand.go b/misc/rand/rand.go
new file mode 100644
index 0000000..7391016
--- /dev/null
+++ b/misc/rand/rand.go
@@ -0,0 +1,20 @@
+package rand
+
+import (
+	"crypto/rand"
+
+	"git.1248.nz/1248/Otfe/misc/b64"
+)
+
+//Bytes generates an random set of bytes n long
+func Bytes(n int) ([]byte, error) {
+	b := make([]byte, n)
+	_, err := rand.Read(b)
+	return b, err
+}
+
+//B64String generates a base 64 string n bytess long
+func B64String(n int) (string, error) {
+	b, err := Bytes(n)
+	return b64.Encode(string(b)), err
+}
diff --git a/misc/test/seed.go b/misc/test/seed.go
new file mode 100644
index 0000000..e9225b4
--- /dev/null
+++ b/misc/test/seed.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"git.1248.nz/1248/Otfe/models"
+	"github.com/globalsign/mgo/bson"
+)
+
+func main() {
+	models.DBWipeCollection("group", "user", "session")
+	//admin user and group
+	adminGroup := models.NewGroup("admin")
+	adminGroup.Admin = true
+	adminGroup.ID = bson.NewObjectId()
+	adminGroup.Permissions["user.show"] = true
+
+	admin := models.User{}
+	admin.Username = "admin"
+	admin.Email = "admin"
+	admin.ID = bson.NewObjectId()
+	admin.Password, _ = helpers.HashPassword("admin")
+	admin.PrimaryGroup = adminGroup.ID
+	adminGroup.Users = append(adminGroup.Users, admin.ID)
+	adminGroup.Create()
+	admin.Create()
+
+	//user and user group
+	userGroup := models.NewGroup("user")
+	userGroup.ID = bson.NewObjectId()
+	userGroup.Admin = false
+	user := models.User{}
+	user.ID = bson.NewObjectId()
+	user.Username = "user"
+	user.Email = "u"
+	user.Password, _ = helpers.HashPassword("user")
+	user.PrimaryGroup = userGroup.ID
+	userGroup.Users = append(userGroup.Users, user.ID)
+	user.Create()
+	userGroup.Create()
+
+}
diff --git a/models/group.go b/models/group.go
new file mode 100644
index 0000000..a7c77b6
--- /dev/null
+++ b/models/group.go
@@ -0,0 +1,50 @@
+package models
+
+import (
+	"github.com/jinzhu/gorm"
+)
+
+//Group type
+type Group struct {
+	gorm.Model
+	Name        string
+	Permissions map[string]bool
+	Admin       bool
+	Users       []string
+}
+
+func NewGroup(Name string) Group {
+	var group Group
+	group.Permissions = make(map[string]bool)
+	return group
+}
+
+//Create group
+func (g *Group) Create() error {
+	return create(&g)
+}
+
+//Read group
+func (g *Group) Read() error {
+	return read(&g)
+
+}
+
+//ReadAll groups
+func (g *Group) ReadAll() ([]Group, error) {
+	var groups []Group
+	var err error
+	err = readAll(&groups)
+	return groups, err
+}
+
+//Update group
+func (g *Group) Update() error {
+	return update(&g)
+}
+
+//Delete group
+func (g *Group) Delete() error {
+	err := delete(&g)
+	return err
+}
diff --git a/models/group_test.go b/models/group_test.go
new file mode 100644
index 0000000..b57938a
--- /dev/null
+++ b/models/group_test.go
@@ -0,0 +1,25 @@
+package models
+
+import (
+	"testing"
+
+	"github.com/globalsign/mgo/bson"
+)
+
+func TestCreateGroup(t *testing.T) {
+	group := NewGroup("test")
+	group.Users = append(group.Users, bson.NewObjectId())
+	group.Permissions["test"] = true
+	t.Log(group.Create())
+}
+
+func TestReadGroup(t *testing.T) {
+	var group Group
+	group.Read("name", "test")
+	t.Log(group)
+}
+
+func TestReadAllGroup(t *testing.T) {
+	var group Group
+	t.Log(group.ReadAll())
+}
diff --git a/models/model.go b/models/model.go
new file mode 100644
index 0000000..96ce40c
--- /dev/null
+++ b/models/model.go
@@ -0,0 +1,78 @@
+package models
+
+import (
+	"fmt"
+
+	"github.com/jinzhu/gorm"
+	_ "github.com/jinzhu/gorm/dialects/postgres" //
+)
+
+const (
+	host     = "localhost"
+	port     = 5432
+	user     = "test"
+	password = "test"
+	dbname   = "test"
+)
+
+var psqlInfo string
+var db *gorm.DB
+
+func init() {
+	psqlInfo = fmt.Sprintf("host=%s port=%d user=%s "+
+		"password=%s dbname=%s sslmode=disable",
+		host, port, user, password, dbname)
+	DB()
+
+}
+func DB() (*gorm.DB, error) {
+	if db == nil {
+		db, err := gorm.Open("postgres", psqlInfo)
+		db.LogMode(true)
+		return db, err
+	}
+	return db, nil
+
+}
+
+func create(m interface{}) error {
+	db, err := DB()
+	if err != nil {
+		return err
+	}
+	return db.Create(m).Error
+}
+
+func read(m interface{}) error {
+	db, err := DB()
+	if err != nil {
+		return err
+	}
+	return db.Where(m).First(m).Error
+}
+
+func readAll(m interface{}) error {
+	db, err := DB()
+	if err != nil {
+		return err
+	}
+	return db.Find(m).Error
+}
+
+func update(m interface{}) error {
+	db, err := DB()
+	defer db.Close()
+	if err != nil {
+		return err
+	}
+	return db.Save(m).Error
+}
+
+func delete(m interface{}) error {
+	db, err := DB()
+	defer db.Close()
+	if err != nil {
+		return err
+	}
+	return db.Delete(m).Error
+}
diff --git a/models/model_test.go b/models/model_test.go
new file mode 100644
index 0000000..1f896ba
--- /dev/null
+++ b/models/model_test.go
@@ -0,0 +1,37 @@
+package models
+
+import (
+	"testing"
+)
+
+func TestGetSession(t *testing.T) {
+	GetMongoSession()
+}
+
+func TestGetCollection(t *testing.T) {
+	GetCollection("test")
+}
+
+func TestCreate(t *testing.T) {
+	create("user", &User{Name: "Ale"})
+}
+
+func TestReadAll(t *testing.T) {
+	var u []User
+	readAll("user", "", nil, &u)
+	t.Log(u)
+}
+
+func TestRead(t *testing.T) {
+	var u User
+	read("user", "name", "Ann", &u)
+	t.Log(u)
+}
+
+func TestUpdate(t *testing.T) {
+	update("test", "name", "Ale", &User{Name: "Bob", Email: "z"})
+}
+
+func TestDelete(t *testing.T) {
+	t.Log(delete("user", "name", "Ann"))
+}
diff --git a/models/post.go b/models/post.go
new file mode 100644
index 0000000..7241cf4
--- /dev/null
+++ b/models/post.go
@@ -0,0 +1,33 @@
+package models
+
+import (
+	"time"
+
+	"github.com/jinzhu/gorm"
+)
+
+//Post model
+type Post struct {
+	gorm.Model
+	Title      string
+	Author     string
+	Published  time.Time
+	LastEdited time.Time
+	Content    []byte
+}
+
+//Create new post
+func (p Post) Create() error {
+	return create(&p)
+}
+
+func (p *Post) Read() (*Post, error) {
+	err := read(&p)
+	return p, err
+}
+
+func (p Post) ReadAll() ([]Post, error) {
+	var posts []Post
+	err := readAll(&posts)
+	return posts, err
+}
diff --git a/models/post_test.go b/models/post_test.go
new file mode 100644
index 0000000..0e60304
--- /dev/null
+++ b/models/post_test.go
@@ -0,0 +1 @@
+package models_test
diff --git a/models/session.go b/models/session.go
new file mode 100644
index 0000000..8e4d787
--- /dev/null
+++ b/models/session.go
@@ -0,0 +1,55 @@
+package models
+
+import (
+	"net/http"
+	"time"
+
+	"git.1248.nz/1248/Otfe/misc/cookie"
+	"github.com/globalsign/mgo/bson"
+	"github.com/jinzhu/gorm"
+)
+
+func init() {
+}
+
+//Session model
+type Session struct {
+	gorm.Model
+	UserID   bson.ObjectId
+	ExpireAt time.Time
+	IP       string
+}
+
+//Create session and set LoginTime
+func (s *Session) Create() error {
+	return create(&s)
+}
+
+//Read session
+func (s *Session) Read() error {
+	return read(&s)
+
+}
+
+//Update LastSeenTime
+func (s *Session) Update() error {
+	s.UpdatedAt = time.Now()
+	return update(&s)
+}
+
+//Delete session
+func (s *Session) Delete() error {
+	return delete(s)
+}
+
+//Get session from http request and check if it is valid
+func (s *Session) Get(r *http.Request) error {
+	//Read session cookie
+	var err error
+	s.Model.ID, err = cookie.Read(r, "session")
+
+	if err == nil {
+		err = s.Read()
+	}
+	return err
+}
diff --git a/models/session_test.go b/models/session_test.go
new file mode 100644
index 0000000..015cbb4
--- /dev/null
+++ b/models/session_test.go
@@ -0,0 +1,51 @@
+package models
+
+import (
+	"net/http"
+	"testing"
+
+	"git.1248.nz/1248/Otfe/misc/b64"
+	"git.1248.nz/1248/Otfe/misc/helpers"
+	"git.1248.nz/1248/Otfe/misc/rand"
+)
+
+func TestSessionCreate(t *testing.T) {
+	var s1, s2 Session
+	s1.ID, _ = rand.B64String(32)
+	if s1.Create() != nil {
+		t.Fail()
+	}
+	read("session", "_id", s1.ID, &s2)
+
+	if s1.ID != s2.ID {
+		t.Fail()
+	}
+}
+
+func TestSessionRead(t *testing.T) {
+	var s1, s2 Session
+	s1.ID, _ = rand.B64String(32)
+	if create("session", &s1) != nil {
+		t.Fatal("Failed to create session")
+	}
+	if s2.Read(s1.ID) != nil {
+		t.Fatal("Failed to read session")
+	}
+	if s1.ID != s2.ID {
+		t.Fatal("Ids don't match")
+	}
+}
+
+func TestGet(t *testing.T) {
+	DBWipeCollection("session")
+	var s1, s2 Session
+	s1.ID, _ = rand.B64String(32)
+	s1.Create()
+	c := &http.Cookie{Name: "session",
+		Value: b64.Encode(s1.ID)}
+	request, err := http.NewRequest("GET", "/", nil)
+	helpers.Ok(t, err)
+	request.AddCookie(c)
+	s2.Get(request)
+	helpers.Equals(t, s1, s2)
+}
diff --git a/models/testhelpers.go b/models/testhelpers.go
new file mode 100644
index 0000000..51dbedb
--- /dev/null
+++ b/models/testhelpers.go
@@ -0,0 +1,11 @@
+package models
+
+//DBWipeCollection removes all data from the
+//specifed collections
+func DBWipeCollection(collections ...string) {
+	for _, collection := range collections {
+		c, s := GetCollection(collection)
+		defer s.Close()
+		c.RemoveAll(nil)
+	}
+}
diff --git a/models/user.go b/models/user.go
new file mode 100644
index 0000000..a2fe529
--- /dev/null
+++ b/models/user.go
@@ -0,0 +1,80 @@
+package models
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/globalsign/mgo/bson"
+)
+
+//User model
+type User struct {
+	ID           bson.ObjectId   `bson:"_id,omitempty"`
+	Email        string          `bson:"email"`
+	Name         string          `bson:"name"`
+	Username     string          `bson:"username"`
+	Password     string          `bson:"password"`
+	PrimaryGroup bson.ObjectId   `bson:"primarygroup,omitempty"`
+	Groups       []bson.ObjectId `bson:"groups,omitempty"`
+	Session      string
+}
+
+//Create user
+func (u *User) Create() error {
+	var user User
+	read("user", "email", u.Email, &user)
+	if u.Email == user.Email {
+		return errors.New("Email all ready used")
+	}
+	return create("user", &u)
+
+}
+
+//Read user
+func (u *User) Read(key string, value interface{}) error {
+	err := read("user", key, value, &u)
+	if err != nil {
+		return errors.New("User doesn't exist")
+	}
+	return nil
+}
+
+//ReadAll users
+func (u *User) ReadAll() ([]User, error) {
+	var users []User
+	var err error
+	err = readAll("user", "", nil, &users)
+	return users, err
+
+}
+
+//Update user
+func (u *User) Update() error {
+	return update("user", "_id", u.ID, &u)
+}
+
+//Delete user
+func (u *User) Delete(key string, value string) error {
+	err := delete("user", key, value)
+	return err
+}
+
+//HasPermission check if a given user is admin or has a given permisssion
+func (u *User) HasPermission(perm string) bool {
+	var group Group
+	//Check primary group
+	err := group.Read("_id", u.PrimaryGroup)
+	fmt.Println(group.Admin)
+	if err == nil && (group.Admin == true || group.Permissions[perm] == true) {
+
+		return true
+	}
+	//Check other groups
+	for id := range u.Groups {
+		err = group.Read("_id", id)
+		if err == nil && (group.Admin || group.Permissions[perm]) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/models/user_test.go b/models/user_test.go
new file mode 100644
index 0000000..1c01984
--- /dev/null
+++ b/models/user_test.go
@@ -0,0 +1,79 @@
+package models
+
+import (
+	"testing"
+
+	"github.com/globalsign/mgo/bson"
+
+	"golang.org/x/crypto/bcrypt"
+)
+
+func TestSeed(t *testing.T) {
+	DBWipeCollection("user")
+	password1, _ := hashPassword("a")
+	user1 := User{Username: "Bob", Name: "Bob", Email: "bob@bob.com", Password: password1}
+	t.Log(user1.Create())
+	user2 := User{Username: "Fred", Name: "Fred", Email: "b"}
+	user2.Create()
+	user3 := User{Username: "Lucy", Name: "Lucy", Email: "c"}
+	user3.Create()
+	user4 := User{Username: "Ann", Name: "Ann", Email: "d"}
+	user4.Create()
+	user5 := User{Username: "Ted", Name: "Ted", Email: "e"}
+	user5.Create()
+	user6 := User{Username: "Egor", Name: "Egor", Email: "f"}
+	user6.Create()
+}
+
+func TestCreateUser(t *testing.T) {
+	user := User{Email: "iojkmiojko", Username: "rytiuhmhhjm,",
+		ID: bson.NewObjectId()}
+	t.Log(user.Create())
+}
+
+func TestUserRead(t *testing.T) {
+	var user User
+	t.Log(user.Read("name", "Ann"))
+	t.Log(user)
+}
+
+func TestUserReadAll(t *testing.T) {
+	var user User
+	t.Log(user.ReadAll())
+}
+
+func TestHasPermission(t *testing.T) {
+	c, s := GetCollection("user")
+	c.RemoveAll(nil)
+	s.Close()
+	c, s = GetCollection("group")
+	c.RemoveAll(nil)
+	s.Close()
+	group := NewGroup("test")
+	group.ID = bson.NewObjectId()
+	group.Create()
+	user := User{Name: "user", Email: "a", PrimaryGroup: group.ID}
+	user.Create()
+	//Check no permission
+	if user.HasPermission("") {
+		t.Fail()
+	}
+	//Check admin permission
+	group.Admin = true
+	group.Update()
+	if !user.HasPermission("") {
+		t.Fail()
+	}
+	group.Admin = false
+	//Check permission
+	group.Permissions["perm"] = true
+	group.Update()
+	if !user.HasPermission("perm") {
+		t.Fail()
+	}
+}
+
+func hashPassword(password string) (string, error) {
+	bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
+	return string(bytes), err
+}
diff --git a/public/css/main.css b/public/css/main.css
new file mode 100644
index 0000000..9e92bb1
--- /dev/null
+++ b/public/css/main.css
@@ -0,0 +1,28 @@
+body {color: #c0392b}
+
+h1 {color:blue}
+
+
+.inline {
+  display: inline;
+}
+
+.link-button {
+  background: none;
+  border: none;
+  cursor: pointer;
+  font-size: 1em;
+  font-family: serif;
+}
+.link-button:focus {
+  outline: none;
+}
+
+
+ul, li {
+  list-style-type: none;
+}
+
+li, a {
+  text-decoration: none
+}
\ No newline at end of file
diff --git a/public/css/side-menu-old-ie.css b/public/css/side-menu-old-ie.css
new file mode 100644
index 0000000..5cfc146
--- /dev/null
+++ b/public/css/side-menu-old-ie.css
@@ -0,0 +1,255 @@
+body {
+    color: #777;
+}
+
+.pure-img-responsive {
+    max-width: 100%;
+    height: auto;
+}
+
+/*
+Add transition to containers so they can push in and out.
+*/
+
+#layout,
+#menu,
+.menu-link {
+    -webkit-transition: all 0.2s ease-out;
+    -moz-transition: all 0.2s ease-out;
+    -ms-transition: all 0.2s ease-out;
+    -o-transition: all 0.2s ease-out;
+    transition: all 0.2s ease-out;
+}
+
+/*
+This is the parent `<div>` that contains the menu and the content area.
+*/
+
+#layout {
+    position: relative;
+    left: 0;
+    padding-left: 0;
+}
+
+#layout.active #menu {
+    left: 150px;
+    width: 150px;
+}
+
+#layout.active .menu-link {
+    left: 150px;
+}
+
+/*
+The content `<div>` is where all your content goes.
+*/
+
+.content {
+    margin: 0 auto;
+    padding: 0 2em;
+    max-width: 800px;
+    margin-bottom: 50px;
+    line-height: 1.6em;
+}
+
+.header {
+    margin: 0;
+    color: #333;
+    text-align: center;
+    padding: 2.5em 2em 0;
+    border-bottom: 1px solid #eee;
+}
+
+.header h1 {
+    margin: 0.2em 0;
+    font-size: 3em;
+    font-weight: 300;
+}
+
+.header h2 {
+    font-weight: 300;
+    color: #ccc;
+    padding: 0;
+    margin-top: 0;
+}
+
+.content-subhead {
+    margin: 50px 0 20px 0;
+    font-weight: 300;
+    color: #888;
+}
+
+/*
+The `#menu` `<div>` is the parent `<div>` that contains the `.pure-menu` that
+appears on the left side of the page.
+*/
+
+#menu {
+    margin-left: -150px;
+    /* "#menu" width */
+    width: 150px;
+    position: fixed;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    z-index: 1000;
+    /* so the menu or its navicon stays above all content */
+    background: #191818;
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
+}
+
+/*
+    All anchors inside the menu should be styled like this.
+    */
+
+#menu a {
+    color: #999;
+    border: none;
+    padding: 0.6em 0 0.6em 0.6em;
+}
+
+/*
+    Remove all background/borders, since we are applying them to #menu.
+    */
+
+#menu .pure-menu,
+#menu .pure-menu ul {
+    border: none;
+    background: transparent;
+}
+
+/*
+    Add that light border to separate items into groups.
+    */
+
+#menu .pure-menu ul,
+#menu .pure-menu .menu-item-divided {
+    border-top: 1px solid #333;
+}
+
+/*
+        Change color of the anchor links on hover/focus.
+        */
+
+#menu .pure-menu li a:hover,
+#menu .pure-menu li a:focus {
+    background: #333;
+}
+
+/*
+    This styles the selected menu item `<li>`.
+    */
+
+#menu .pure-menu-selected,
+#menu .pure-menu-heading {
+    background: #1f8dd6;
+}
+
+/*
+        This styles a link within a selected menu item `<li>`.
+        */
+
+#menu .pure-menu-selected a {
+    color: #fff;
+}
+
+/*
+    This styles the menu heading.
+    */
+
+#menu .pure-menu-heading {
+    font-size: 110%;
+    color: #fff;
+    margin: 0;
+}
+
+/* -- Dynamic Button For Responsive Menu -------------------------------------*/
+
+/*
+The button to open/close the Menu is custom-made and not part of Pure. Here's
+how it works:
+*/
+
+/*
+`.menu-link` represents the responsive menu toggle that shows/hides on
+small screens.
+*/
+
+.menu-link {
+    position: fixed;
+    display: block;
+    /* show this only on small screens */
+    top: 0;
+    left: 0;
+    /* "#menu width" */
+    background: #000;
+    background: rgba(0,0,0,0.7);
+    font-size: 10px;
+    /* change this value to increase/decrease button size */
+    z-index: 10;
+    width: 2em;
+    height: auto;
+    padding: 2.1em 1.6em;
+}
+
+.menu-link:hover,
+.menu-link:focus {
+    background: #000;
+}
+
+.menu-link span {
+    position: relative;
+    display: block;
+}
+
+.menu-link span,
+.menu-link span:before,
+.menu-link span:after {
+    background-color: #fff;
+    width: 100%;
+    height: 0.2em;
+}
+
+.menu-link span:before,
+.menu-link span:after {
+    position: absolute;
+    margin-top: -0.6em;
+    content: " ";
+}
+
+.menu-link span:after {
+    margin-top: 0.6em;
+}
+
+/* -- Responsive Styles (Media Queries) ------------------------------------- */
+
+/*
+Hides the menu at `48em`, but modify this based on your app's needs.
+*/
+
+.header,
+.content {
+    padding-left: 2em;
+    padding-right: 2em;
+}
+
+#layout {
+    padding-left: 150px;
+    /* left col width "#menu" */
+    left: 0;
+}
+
+#menu {
+    left: 150px;
+}
+
+.menu-link {
+    position: fixed;
+    left: 150px;
+    display: none;
+}
+
+#layout.active .menu-link {
+    left: 150px;
+}
\ No newline at end of file
diff --git a/public/css/side-menu.css b/public/css/side-menu.css
new file mode 100644
index 0000000..7abd61c
--- /dev/null
+++ b/public/css/side-menu.css
@@ -0,0 +1,248 @@
+body {
+    color: #777;
+}
+
+.pure-img-responsive {
+    max-width: 100%;
+    height: auto;
+}
+
+/*
+Add transition to containers so they can push in and out.
+*/
+#layout,
+#menu,
+.menu-link {
+    -webkit-transition: all 0.2s ease-out;
+    -moz-transition: all 0.2s ease-out;
+    -ms-transition: all 0.2s ease-out;
+    -o-transition: all 0.2s ease-out;
+    transition: all 0.2s ease-out;
+}
+
+/*
+This is the parent `<div>` that contains the menu and the content area.
+*/
+#layout {
+    position: relative;
+    left: 0;
+    padding-left: 0;
+}
+    #layout.active #menu {
+        left: 150px;
+        width: 150px;
+    }
+
+    #layout.active .menu-link {
+        left: 150px;
+    }
+/*
+The content `<div>` is where all your content goes.
+*/
+.content {
+    margin: 0 auto;
+    padding: 0 2em;
+    max-width: 800px;
+    margin-bottom: 50px;
+    line-height: 1.6em;
+}
+
+.header {
+     margin: 0;
+     color: #333;
+     text-align: center;
+     padding: 2.5em 2em 0;
+     border-bottom: 1px solid #eee;
+ }
+    .header h1 {
+        margin: 0.2em 0;
+        font-size: 3em;
+        font-weight: 300;
+    }
+     .header h2 {
+        font-weight: 300;
+        color: #ccc;
+        padding: 0;
+        margin-top: 0;
+    }
+
+.content-subhead {
+    margin: 50px 0 20px 0;
+    font-weight: 300;
+    color: #888;
+}
+
+
+
+/*
+The `#menu` `<div>` is the parent `<div>` that contains the `.pure-menu` that
+appears on the left side of the page.
+*/
+
+#menu {
+    margin-left: -150px; /* "#menu" width */
+    width: 150px;
+    position: fixed;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    z-index: 1000; /* so the menu or its navicon stays above all content */
+    background: #191818;
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
+}
+    /*
+    All anchors inside the menu should be styled like this.
+    */
+    #menu a {
+        color: #999;
+        border: none;
+        padding: 0.6em 0 0.6em 0.6em;
+    }
+
+    /*
+    Remove all background/borders, since we are applying them to #menu.
+    */
+     #menu .pure-menu,
+     #menu .pure-menu ul {
+        border: none;
+        background: transparent;
+    }
+
+    /*
+    Add that light border to separate items into groups.
+    */
+    #menu .pure-menu ul,
+    #menu .pure-menu .menu-item-divided {
+        border-top: 1px solid #333;
+    }
+        /*
+        Change color of the anchor links on hover/focus.
+        */
+        #menu .pure-menu li a:hover,
+        #menu .pure-menu li a:focus {
+            background: #333;
+        }
+
+    /*
+    This styles the selected menu item `<li>`.
+    */
+    #menu .pure-menu-selected,
+    #menu .pure-menu-heading {
+        background: #1f8dd6;
+    }
+        /*
+        This styles a link within a selected menu item `<li>`.
+        */
+        #menu .pure-menu-selected a {
+            color: #fff;
+        }
+
+    /*
+    This styles the menu heading.
+    */
+    #menu .pure-menu-heading {
+        font-size: 110%;
+        color: #fff;
+        margin: 0;
+    }
+
+/* -- Dynamic Button For Responsive Menu -------------------------------------*/
+
+/*
+The button to open/close the Menu is custom-made and not part of Pure. Here's
+how it works:
+*/
+
+/*
+`.menu-link` represents the responsive menu toggle that shows/hides on
+small screens.
+*/
+.menu-link {
+    position: fixed;
+    display: block; /* show this only on small screens */
+    top: 0;
+    left: 0; /* "#menu width" */
+    background: #000;
+    background: rgba(0,0,0,0.7);
+    font-size: 10px; /* change this value to increase/decrease button size */
+    z-index: 10;
+    width: 2em;
+    height: auto;
+    padding: 2.1em 1.6em;
+}
+
+    .menu-link:hover,
+    .menu-link:focus {
+        background: #000;
+    }
+
+    .menu-link span {
+        position: relative;
+        display: block;
+    }
+
+    .menu-link span,
+    .menu-link span:before,
+    .menu-link span:after {
+        background-color: #fff;
+        width: 100%;
+        height: 0.2em;
+    }
+
+        .menu-link span:before,
+        .menu-link span:after {
+            position: absolute;
+            margin-top: -0.6em;
+            content: " ";
+        }
+
+        .menu-link span:after {
+            margin-top: 0.6em;
+        }
+
+
+/* -- Responsive Styles (Media Queries) ------------------------------------- */
+
+/*
+Hides the menu at `48em`, but modify this based on your app's needs.
+*/
+@media (min-width: 48em) {
+
+    .header,
+    .content {
+        padding-left: 2em;
+        padding-right: 2em;
+    }
+
+    #layout {
+        padding-left: 150px; /* left col width "#menu" */
+        left: 0;
+    }
+    #menu {
+        left: 150px;
+    }
+
+    .menu-link {
+        position: fixed;
+        left: 150px;
+        display: none;
+    }
+
+    #layout.active .menu-link {
+        left: 150px;
+    }
+}
+
+@media (max-width: 48em) {
+    /* Only apply this when the window is small. Otherwise, the following
+    case results in extra padding on the left:
+        * Make the window small.
+        * Tap the menu to trigger the active state.
+        * Make the window large again.
+    */
+    #layout.active {
+        position: relative;
+        left: 150px;
+    }
+}
diff --git a/public/js/main.js b/public/js/main.js
new file mode 100644
index 0000000..e69de29
diff --git a/public/js/request.js b/public/js/request.js
new file mode 100644
index 0000000..e69de29
diff --git a/public/js/ui.js b/public/js/ui.js
new file mode 100644
index 0000000..6d7c20b
--- /dev/null
+++ b/public/js/ui.js
@@ -0,0 +1,46 @@
+(function (window, document) {
+
+    var layout   = document.getElementById('layout'),
+        menu     = document.getElementById('menu'),
+        menuLink = document.getElementById('menuLink'),
+        content  = document.getElementById('main');
+
+    function toggleClass(element, className) {
+        var classes = element.className.split(/\s+/),
+            length = classes.length,
+            i = 0;
+
+        for(; i < length; i++) {
+          if (classes[i] === className) {
+            classes.splice(i, 1);
+            break;
+          }
+        }
+        // The className is not found
+        if (length === classes.length) {
+            classes.push(className);
+        }
+
+        element.className = classes.join(' ');
+    }
+
+    function toggleAll(e) {
+        var active = 'active';
+
+        e.preventDefault();
+        toggleClass(layout, active);
+        toggleClass(menu, active);
+        toggleClass(menuLink, active);
+    }
+
+    menuLink.onclick = function (e) {
+        toggleAll(e);
+    };
+
+    content.onclick = function(e) {
+        if (menu.className.indexOf('active') !== -1) {
+            toggleAll(e);
+        }
+    };
+
+}(this, this.document));
diff --git a/runner.conf b/runner.conf
new file mode 100644
index 0000000..6d6a183
--- /dev/null
+++ b/runner.conf
@@ -0,0 +1,14 @@
+root:              ./
+tmp_path:          ./tmp
+build_name:        runner-build
+build_log:         runner-build-errors.log
+valid_ext:         .go, .tpl, .tmpl, .html, .ccs, .js, .toml
+no_rebuild_ext:    .tpl, .tmpl, .html
+ignored:           assets, tmp
+build_delay:       600
+colors:            1
+log_color_main:    cyan
+log_color_build:   yellow
+log_color_runner:  green
+log_color_watcher: magenta
+log_color_app:
\ No newline at end of file
diff --git a/tmp/runner-build b/tmp/runner-build
new file mode 100644
index 0000000..9d35331
Binary files /dev/null and b/tmp/runner-build differ
diff --git a/views/layouts/footer.gtpl b/views/layouts/footer.gtpl
new file mode 100644
index 0000000..3895184
--- /dev/null
+++ b/views/layouts/footer.gtpl
@@ -0,0 +1,3 @@
+{{define "footer"}}
+<footer></footer>
+{{end}}
\ No newline at end of file
diff --git a/views/layouts/header.gtpl b/views/layouts/header.gtpl
new file mode 100644
index 0000000..f04c9b6
--- /dev/null
+++ b/views/layouts/header.gtpl
@@ -0,0 +1,5 @@
+{{define "header"}}
+<header>
+   {{template "nav" .}} 
+</header>
+{{end}}
\ No newline at end of file
diff --git a/views/layouts/layout.gtpl b/views/layouts/layout.gtpl
new file mode 100644
index 0000000..43e6eb9
--- /dev/null
+++ b/views/layouts/layout.gtpl
@@ -0,0 +1,22 @@
+{{define "layout"}}
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <title>{{.Title}}</title>
+    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css">
+    
+    
+</head>
+<body>
+    {{template "header" .}}
+    {{template "content" .}}
+    {{template "footer" .}}
+    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.bundle.min.js" 
+        integrity="sha384-u/bQvRA/1bobcXlcEYpsEdFVK/vJs3+T+nXLsBYJthmdBuavHvAW6UsmqO2Gd/F9" crossorigin="anonymous"></script>
+    <script src="public/js/main.js"></script>
+</body>
+</body> 
+</html>
+{{end}}
\ No newline at end of file
diff --git a/views/layouts/nav.gtpl b/views/layouts/nav.gtpl
new file mode 100644
index 0000000..0a6bf0f
--- /dev/null
+++ b/views/layouts/nav.gtpl
@@ -0,0 +1,20 @@
+{{define "nav"}}
+<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
+    <a class="navbar-brand" href="#">Otfe</a>
+    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
+        <span class="navbar-toggler-icon"></span>
+    </button>
+
+    <div class="collapse navbar-collapse" id="navbarsExampleDefault">
+        <ul class="navbar-nav mr-auto">
+        <li class="nav-item active">
+            <a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a>
+        </li>
+        <li class="nav-item">
+            <a class="nav-link" href="#">Link</a>
+        </li>
+        
+        </ul>
+    </div>
+</nav> 
+{{end}}
\ No newline at end of file
diff --git a/views/post/edit.gtpl b/views/post/edit.gtpl
new file mode 100644
index 0000000..a543773
--- /dev/null
+++ b/views/post/edit.gtpl
@@ -0,0 +1,17 @@
+{{define "content"}}
+<form action="/user/new" method="post">
+    <fieldset>
+        <legend>New User</legend>
+        <div>Username</div>
+        <input type="text" name="username">
+        <div>Email</div>
+        <input type="email" name="email">
+        <div>Password</div>
+        <input type="password" name="password">
+        <div>Password Repeat</div>
+        <input type="password" name="password-repeat"><br>
+        <input type="submit" value="Submit">
+    </fieldset>
+</form>
+
+{{end}}
\ No newline at end of file
diff --git a/views/post/new.gtpl b/views/post/new.gtpl
new file mode 100644
index 0000000..2a95094
--- /dev/null
+++ b/views/post/new.gtpl
@@ -0,0 +1,11 @@
+{{define "content"}}
+<form action="/user/new" method="post">
+    <fieldset>
+        <legend>New Post</legend>
+        <div>Title</div>
+        <input type="text" name="title">
+        <input type="text" name="content">
+        <input type="submit" value="Submit">
+    </fieldset>
+</form>
+{{end}}
\ No newline at end of file
diff --git a/views/post/post.gtpl b/views/post/post.gtpl
new file mode 100644
index 0000000..010fe7a
--- /dev/null
+++ b/views/post/post.gtpl
@@ -0,0 +1,11 @@
+{{define "content"}}
+
+<h1>{{.Post.Ttile}}</h1>
+<h2>Username: {{.User.Username}}</h2>
+<h2>Email:  {{.User.Email}}</h2>
+<h2>Password: {{.User.Password}}</h2>
+<form action="/user/{{.User.Username}}/delete" method="post">
+    <button type="submit">Delete</button>
+</form>
+<h1>{{getId .User.ID}}</h1>
+{{end}}
\ No newline at end of file
diff --git a/views/post/posts.gtpl b/views/post/posts.gtpl
new file mode 100644
index 0000000..57f456a
--- /dev/null
+++ b/views/post/posts.gtpl
@@ -0,0 +1,17 @@
+{{define "content"}}
+<h1>{{.Title}}</h1>
+<ul>
+    {{range $i, $a := .Posts}}
+        <li>
+            <a href="/user/{{$a.ID}}">{{$a.ID}}</a>
+        </li>
+    {{end}}
+</ul>
+
+<form action="/user/new">
+    <button type="submit">New User</button>
+</form>
+{{end}}
+
+
+
diff --git a/views/static/home.gtpl b/views/static/home.gtpl
new file mode 100644
index 0000000..b34f97d
--- /dev/null
+++ b/views/static/home.gtpl
@@ -0,0 +1,3 @@
+{{define "content"}}
+    
+{{end}}
\ No newline at end of file
diff --git a/views/static/login.gtpl b/views/static/login.gtpl
new file mode 100644
index 0000000..2371ff0
--- /dev/null
+++ b/views/static/login.gtpl
@@ -0,0 +1,13 @@
+{{define "content"}}
+<h1>Login</h1>
+{{.Err}}
+<form action="/login" method="post">
+    <div>Username or Email</div>
+    <input type="text" name="email">
+    <div>Password</div>
+    <input type="password" name="password">
+    <br>
+    <button type="submit">Login</button>
+</form>
+
+{{end}}
\ No newline at end of file
diff --git a/views/user/edit.gtpl b/views/user/edit.gtpl
new file mode 100644
index 0000000..a543773
--- /dev/null
+++ b/views/user/edit.gtpl
@@ -0,0 +1,17 @@
+{{define "content"}}
+<form action="/user/new" method="post">
+    <fieldset>
+        <legend>New User</legend>
+        <div>Username</div>
+        <input type="text" name="username">
+        <div>Email</div>
+        <input type="email" name="email">
+        <div>Password</div>
+        <input type="password" name="password">
+        <div>Password Repeat</div>
+        <input type="password" name="password-repeat"><br>
+        <input type="submit" value="Submit">
+    </fieldset>
+</form>
+
+{{end}}
\ No newline at end of file
diff --git a/views/user/new.gtpl b/views/user/new.gtpl
new file mode 100644
index 0000000..a543773
--- /dev/null
+++ b/views/user/new.gtpl
@@ -0,0 +1,17 @@
+{{define "content"}}
+<form action="/user/new" method="post">
+    <fieldset>
+        <legend>New User</legend>
+        <div>Username</div>
+        <input type="text" name="username">
+        <div>Email</div>
+        <input type="email" name="email">
+        <div>Password</div>
+        <input type="password" name="password">
+        <div>Password Repeat</div>
+        <input type="password" name="password-repeat"><br>
+        <input type="submit" value="Submit">
+    </fieldset>
+</form>
+
+{{end}}
\ No newline at end of file
diff --git a/views/user/user.gtpl b/views/user/user.gtpl
new file mode 100644
index 0000000..dcaaf94
--- /dev/null
+++ b/views/user/user.gtpl
@@ -0,0 +1,11 @@
+{{define "content"}}
+
+<h1>{{.User.Name}}</h1>
+<h2>Username: {{.User.Username}}</h2>
+<h2>Email:  {{.User.Email}}</h2>
+<h2>Password: {{.User.Password}}</h2>
+<form action="/user/{{.User.Username}}/delete" method="post">
+    <button type="submit">Delete</button>
+</form>
+<h1>{{getId .User.ID}}</h1>
+{{end}}
\ No newline at end of file
diff --git a/views/user/users.gtpl b/views/user/users.gtpl
new file mode 100644
index 0000000..ad83125
--- /dev/null
+++ b/views/user/users.gtpl
@@ -0,0 +1,17 @@
+{{define "content"}}
+<h1>{{.Title}}</h1>
+<ul>
+    {{range $i, $a := .Users}}
+        <li>
+            <a href="/user/{{$a.Username}}">{{$a.Username}}</a>
+        </li>
+    {{end}}
+</ul>
+
+<form action="/user/new">
+    <button type="submit">New User</button>
+</form>
+{{end}}
+
+
+