commit 8bc3cee328b54bc3ccaac812632fed9d4253760e Author: Jimmy 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, "Login") { + 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 `
` 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 `
` 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` `
` is the parent `
` 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 `
  • `. + */ + +#menu .pure-menu-selected, +#menu .pure-menu-heading { + background: #1f8dd6; +} + +/* + This styles a link within a selected menu item `
  • `. + */ + +#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 `
    ` 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 `
    ` 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` `
    ` is the parent `
    ` 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 `
  • `. + */ + #menu .pure-menu-selected, + #menu .pure-menu-heading { + background: #1f8dd6; + } + /* + This styles a link within a selected menu item `
  • `. + */ + #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"}} +
    +{{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"}} +
    + {{template "nav" .}} +
    +{{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"}} + + + + + + {{.Title}} + + + + + + {{template "header" .}} + {{template "content" .}} + {{template "footer" .}} + + + + + +{{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"}} + +{{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"}} +
    +
    + New User +
    Username
    + +
    Email
    + +
    Password
    + +
    Password Repeat
    +
    + +
    +
    + +{{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"}} +
    +
    + New Post +
    Title
    + + + +
    +
    +{{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"}} + +

    {{.Post.Ttile}}

    +

    Username: {{.User.Username}}

    +

    Email: {{.User.Email}}

    +

    Password: {{.User.Password}}

    +
    + +
    +

    {{getId .User.ID}}

    +{{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"}} +

    {{.Title}}

    +
      + {{range $i, $a := .Posts}} +
    • + {{$a.ID}} +
    • + {{end}} +
    + +
    + +
    +{{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"}} +

    Login

    +{{.Err}} +
    +
    Username or Email
    + +
    Password
    + +
    + +
    + +{{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"}} +
    +
    + New User +
    Username
    + +
    Email
    + +
    Password
    + +
    Password Repeat
    +
    + +
    +
    + +{{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"}} +
    +
    + New User +
    Username
    + +
    Email
    + +
    Password
    + +
    Password Repeat
    +
    + +
    +
    + +{{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"}} + +

    {{.User.Name}}

    +

    Username: {{.User.Username}}

    +

    Email: {{.User.Email}}

    +

    Password: {{.User.Password}}

    +
    + +
    +

    {{getId .User.ID}}

    +{{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"}} +

    {{.Title}}

    + + +
    + +
    +{{end}} + + +