Inital commit

This commit is contained in:
Jimmy 2022-02-24 01:07:09 +13:00
commit 8bc3cee328
55 changed files with 2292 additions and 0 deletions

8
README.md Normal file
View File

@ -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.

57
app.go Normal file
View File

@ -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
}

43
app/seed_test.go Normal file
View File

@ -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())
}

8
config.toml Normal file
View File

@ -0,0 +1,8 @@
[database]
Host="localhost"
Name="test"
[session]
SecretKey="ef48c62fae0d0e7fb295fbdf5232b8e578e58ff7c20bf28b4e61b0f24df8362e5b2a6f55d402c279360e95918ae35b79c1afd784d53a487c9414a0c0d79a74dd"
SessionKey="otfe"
timeout=3600 #in sec

33
controllers/controller.go Normal file
View File

@ -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)
}

66
controllers/post.go Normal file
View File

@ -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)
}

72
controllers/session.go Normal file
View File

@ -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)
}

102
controllers/session_test.go Normal file
View File

@ -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()
}

29
controllers/static.go Normal file
View File

@ -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")
}

91
controllers/user.go Normal file
View File

@ -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)
}

59
misc/auth/auth.go Normal file
View File

@ -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)
}

113
misc/auth/auth_test.go Normal file
View File

@ -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)
}

12
misc/b64/b64.go Normal file
View File

@ -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
}

60
misc/config/config.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,7 @@
package config
import "testing"
func TestGetConfigFile(t *testing.T) {
t.Log(Get())
}

31
misc/cookie/cookie.go Normal file
View File

@ -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)})
}

View File

@ -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")
}

55
misc/helpers/helpers.go Normal file
View File

@ -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
}

View File

@ -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())
}
}

36
misc/helpers/test.go Normal file
View File

@ -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()
}
}

20
misc/rand/rand.go Normal file
View File

@ -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
}

41
misc/test/seed.go Normal file
View File

@ -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()
}

50
models/group.go Normal file
View File

@ -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
}

25
models/group_test.go Normal file
View File

@ -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())
}

78
models/model.go Normal file
View File

@ -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
}

37
models/model_test.go Normal file
View File

@ -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"))
}

33
models/post.go Normal file
View File

@ -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
}

1
models/post_test.go Normal file
View File

@ -0,0 +1 @@
package models_test

55
models/session.go Normal file
View File

@ -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
}

51
models/session_test.go Normal file
View File

@ -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)
}

11
models/testhelpers.go Normal file
View File

@ -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)
}
}

80
models/user.go Normal file
View File

@ -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
}

79
models/user_test.go Normal file
View File

@ -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
}

28
public/css/main.css Normal file
View File

@ -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
}

View File

@ -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;
}

248
public/css/side-menu.css Normal file
View File

@ -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
public/js/main.js Normal file
View File

0
public/js/request.js Normal file
View File

46
public/js/ui.js Normal file
View File

@ -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));

14
runner.conf Normal file
View File

@ -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:

BIN
tmp/runner-build Normal file

Binary file not shown.

View File

@ -0,0 +1,3 @@
{{define "footer"}}
<footer></footer>
{{end}}

View File

@ -0,0 +1,5 @@
{{define "header"}}
<header>
{{template "nav" .}}
</header>
{{end}}

22
views/layouts/layout.gtpl Normal file
View File

@ -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}}

20
views/layouts/nav.gtpl Normal file
View File

@ -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}}

17
views/post/edit.gtpl Normal file
View File

@ -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}}

11
views/post/new.gtpl Normal file
View File

@ -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}}

11
views/post/post.gtpl Normal file
View File

@ -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}}

17
views/post/posts.gtpl Normal file
View File

@ -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}}

3
views/static/home.gtpl Normal file
View File

@ -0,0 +1,3 @@
{{define "content"}}
{{end}}

13
views/static/login.gtpl Normal file
View File

@ -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}}

17
views/user/edit.gtpl Normal file
View File

@ -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}}

17
views/user/new.gtpl Normal file
View File

@ -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}}

11
views/user/user.gtpl Normal file
View File

@ -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}}

17
views/user/users.gtpl Normal file
View File

@ -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}}