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