Inital commit
This commit is contained in:
commit
8bc3cee328
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
[database]
|
||||||
|
Host="localhost"
|
||||||
|
Name="test"
|
||||||
|
|
||||||
|
[session]
|
||||||
|
SecretKey="ef48c62fae0d0e7fb295fbdf5232b8e578e58ff7c20bf28b4e61b0f24df8362e5b2a6f55d402c279360e95918ae35b79c1afd784d53a487c9414a0c0d79a74dd"
|
||||||
|
SessionKey="otfe"
|
||||||
|
timeout=3600 #in sec
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestGetConfigFile(t *testing.T) {
|
||||||
|
t.Log(Get())
|
||||||
|
}
|
|
@ -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)})
|
||||||
|
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package models_test
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
|
@ -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:
|
Binary file not shown.
|
@ -0,0 +1,3 @@
|
||||||
|
{{define "footer"}}
|
||||||
|
<footer></footer>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{{define "header"}}
|
||||||
|
<header>
|
||||||
|
{{template "nav" .}}
|
||||||
|
</header>
|
||||||
|
{{end}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{define "content"}}
|
||||||
|
|
||||||
|
{{end}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue