chore: added model delete related to #2
This commit is contained in:
parent
af62db6ad1
commit
b8278bacf6
@ -7,7 +7,7 @@ tmp_dir = "tmp"
|
|||||||
bin = "./tmp/main"
|
bin = "./tmp/main"
|
||||||
cmd = "go build -o ./tmp/main ."
|
cmd = "go build -o ./tmp/main ."
|
||||||
delay = 0
|
delay = 0
|
||||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
exclude_dir = ["assets", "tmp", "vendor", "testData", "savedData"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
exclude_regex = ["_test.go"]
|
exclude_regex = ["_test.go"]
|
||||||
exclude_unchanged = false
|
exclude_unchanged = false
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,4 @@
|
|||||||
tmp/
|
tmp/
|
||||||
|
testData/
|
||||||
|
savedData/
|
||||||
|
!savedData/.keep
|
||||||
|
1
go.mod
1
go.mod
@ -3,6 +3,7 @@ module andr3h3nriqu3s.com/m
|
|||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/google/uuid v1.3.1 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
golang.org/x/crypto v0.13.0 // indirect
|
golang.org/x/crypto v0.13.0 // indirect
|
||||||
)
|
)
|
||||||
|
2
go.sum
2
go.sum
@ -1,3 +1,5 @@
|
|||||||
|
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||||
|
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||||
|
373
handler.go
373
handler.go
@ -8,6 +8,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -78,11 +80,56 @@ func LoadHtml(writer http.ResponseWriter, path string, data interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoadError(writer http.ResponseWriter, path string, base string, data AnyMap) {
|
||||||
|
if data == nil {
|
||||||
|
data = map[string]interface{} {
|
||||||
|
"Error": true,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data["Error"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.New("").Parse("{{template \"" + base + "\" . }}")
|
||||||
|
if err != nil {
|
||||||
|
panic("Lol")
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err = tmpl.ParseFiles(
|
||||||
|
"./views/"+path,
|
||||||
|
"./views/partials/header.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to load template %s\n", path)
|
||||||
|
fmt.Println(err)
|
||||||
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
|
if path == "500.html" {
|
||||||
|
writer.Write([]byte("<h1>Failed to load 500.html check console for more info</h1>"))
|
||||||
|
} else {
|
||||||
|
LoadHtml(writer, "500.html", nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpl.Execute(writer, data); err != nil {
|
||||||
|
fmt.Printf("Failed to execute template %s\n", path)
|
||||||
|
fmt.Println(err)
|
||||||
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
|
if path == "500.html" {
|
||||||
|
writer.Write([]byte("<h1>Failed to load 500.html check console for more info</h1>"))
|
||||||
|
} else {
|
||||||
|
LoadHtml(writer, "500.html", nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type AnyMap = map[string]interface{}
|
type AnyMap = map[string]interface{}
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
code int
|
code int
|
||||||
msg *string
|
msg *string
|
||||||
|
data AnyMap
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnswerType int
|
type AnswerType int
|
||||||
@ -133,9 +180,10 @@ type Handler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Handle struct {
|
type Handle struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
gets []HandleFunc
|
gets []HandleFunc
|
||||||
posts []HandleFunc
|
posts []HandleFunc
|
||||||
|
deletes []HandleFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeBody(r *http.Request) (string, *Error) {
|
func decodeBody(r *http.Request) (string, *Error) {
|
||||||
@ -149,9 +197,8 @@ func decodeBody(r *http.Request) (string, *Error) {
|
|||||||
|
|
||||||
func handleError(err *Error, w http.ResponseWriter, context *Context) {
|
func handleError(err *Error, w http.ResponseWriter, context *Context) {
|
||||||
|
|
||||||
data := context.toMap()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
data := context.addMap(err.data)
|
||||||
w.WriteHeader(err.code)
|
w.WriteHeader(err.code)
|
||||||
if err.code == http.StatusNotFound {
|
if err.code == http.StatusNotFound {
|
||||||
LoadBasedOnAnswer(context.Mode, w, "404.html", data)
|
LoadBasedOnAnswer(context.Mode, w, "404.html", data)
|
||||||
@ -168,69 +215,39 @@ func handleError(err *Error, w http.ResponseWriter, context *Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (x *Handle) Get(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
func (x *Handle) Get(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||||
nhandler :=
|
x.gets = append(x.gets, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn})
|
||||||
HandleFunc{
|
|
||||||
fn: fn,
|
|
||||||
path: path,
|
|
||||||
mode: NORMAL | HTML | HTMLFULL | JSON,
|
|
||||||
}
|
|
||||||
|
|
||||||
x.gets = append(x.gets, nhandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Handle) GetHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
func (x *Handle) GetHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||||
nhandler :=
|
x.gets = append(x.gets, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn})
|
||||||
HandleFunc{
|
|
||||||
fn: fn,
|
|
||||||
path: path,
|
|
||||||
mode: NORMAL | HTML | HTMLFULL,
|
|
||||||
}
|
|
||||||
|
|
||||||
x.gets = append(x.gets, nhandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Handle) GetJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
func (x *Handle) GetJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||||
nhandler :=
|
x.gets = append(x.gets, HandleFunc{path, JSON, fn})
|
||||||
HandleFunc{
|
|
||||||
fn: fn,
|
|
||||||
path: path,
|
|
||||||
mode: JSON,
|
|
||||||
}
|
|
||||||
|
|
||||||
x.gets = append(x.gets, nhandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Handle) Post(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
func (x *Handle) Post(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||||
nhandler :=
|
x.posts = append(x.posts, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn})
|
||||||
HandleFunc{
|
|
||||||
fn: fn,
|
|
||||||
path: path,
|
|
||||||
mode: NORMAL | HTML | HTMLFULL | JSON,
|
|
||||||
}
|
|
||||||
|
|
||||||
x.posts = append(x.posts, nhandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Handle) PostHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
func (x *Handle) PostHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||||
nhandler :=
|
x.posts = append(x.posts, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn})
|
||||||
HandleFunc{
|
|
||||||
fn: fn,
|
|
||||||
path: path,
|
|
||||||
mode: NORMAL | HTML | HTMLFULL,
|
|
||||||
}
|
|
||||||
|
|
||||||
x.posts = append(x.posts, nhandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Handle) PostJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
func (x *Handle) PostJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||||
nhandler :=
|
x.posts = append(x.posts, HandleFunc{path, JSON, fn})
|
||||||
HandleFunc{
|
}
|
||||||
fn: fn,
|
|
||||||
path: path,
|
|
||||||
mode: JSON,
|
|
||||||
}
|
|
||||||
|
|
||||||
x.posts = append(x.posts, nhandler)
|
func (x *Handle) Delete(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||||
|
x.deletes = append(x.deletes, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Handle) DeleteHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||||
|
x.deletes = append(x.deletes, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Handle) DeleteJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) {
|
||||||
|
x.deletes = append(x.deletes, HandleFunc{path, JSON, fn})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Handle) handleGets(w http.ResponseWriter, r *http.Request, context *Context) {
|
func (x *Handle) handleGets(w http.ResponseWriter, r *http.Request, context *Context) {
|
||||||
@ -243,9 +260,7 @@ func (x *Handle) handleGets(w http.ResponseWriter, r *http.Request, context *Con
|
|||||||
if context.Mode != HTMLFULL {
|
if context.Mode != HTMLFULL {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
LoadBasedOnAnswer(context.Mode, w, "404.html", map[string]interface{}{
|
LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil))
|
||||||
"context": context,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Handle) handlePosts(w http.ResponseWriter, r *http.Request, context *Context) {
|
func (x *Handle) handlePosts(w http.ResponseWriter, r *http.Request, context *Context) {
|
||||||
@ -258,18 +273,42 @@ func (x *Handle) handlePosts(w http.ResponseWriter, r *http.Request, context *Co
|
|||||||
if context.Mode != HTMLFULL {
|
if context.Mode != HTMLFULL {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
LoadBasedOnAnswer(context.Mode, w, "404.html", map[string]interface{}{
|
LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil))
|
||||||
"context": context,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func AnswerTemplate(path string, data AnyMap) func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
func (x *Handle) handleDeletes(w http.ResponseWriter, r *http.Request, context *Context) {
|
||||||
|
for _, s := range x.deletes {
|
||||||
|
if s.path == r.URL.Path && context.Mode&s.mode != 0 {
|
||||||
|
handleError(s.fn(w, r, context), w, context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if context.Mode != HTMLFULL {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAuthLevel(authLevel int, w http.ResponseWriter, r *http.Request, c *Context) bool {
|
||||||
|
if authLevel > 0 {
|
||||||
|
if c.requireAuth(w, r) {
|
||||||
|
logoff(c.Mode, w, r)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.User.user_type < authLevel {
|
||||||
|
notAuth(c.Mode, w, r)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnswerTemplate(path string, data AnyMap, authLevel int) func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||||
return func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
return func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||||
if data == nil {
|
if !checkAuthLevel(authLevel, w, r, c) {
|
||||||
LoadBasedOnAnswer(c.Mode, w, path, c.toMap())
|
return nil
|
||||||
} else {
|
}
|
||||||
LoadBasedOnAnswer(c.Mode, w, path, c.addMap(data))
|
LoadBasedOnAnswer(c.Mode, w, path, c.addMap(data))
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -277,25 +316,24 @@ func AnswerTemplate(path string, data AnyMap) func(w http.ResponseWriter, r *htt
|
|||||||
type Context struct {
|
type Context struct {
|
||||||
Token *string
|
Token *string
|
||||||
User *User
|
User *User
|
||||||
Mode AnswerType
|
Mode AnswerType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Context) addMap(m AnyMap) AnyMap {
|
func (c Context) addMap(m AnyMap) AnyMap {
|
||||||
m["Context"] = c;
|
if m == nil {
|
||||||
return m;
|
return map[string]interface{}{
|
||||||
}
|
"Context": c,
|
||||||
|
}
|
||||||
func (c *Context) toMap() map[string]interface{} {
|
|
||||||
return map[string]interface{}{
|
|
||||||
"Context": c,
|
|
||||||
}
|
}
|
||||||
|
m["Context"] = c
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) requireAuth(w http.ResponseWriter, r *http.Request) bool {
|
func (c *Context) requireAuth(w http.ResponseWriter, r *http.Request) bool {
|
||||||
if c.User == nil {
|
if c.User == nil {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var LogoffError = errors.New("Invalid token!")
|
var LogoffError = errors.New("Invalid token!")
|
||||||
@ -310,10 +348,12 @@ func (x Handle) createContext(mode AnswerType, r *http.Request) (*Context, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO check that the token is still valid
|
||||||
|
|
||||||
if token == nil {
|
if token == nil {
|
||||||
return &Context{
|
return &Context{
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := userFromToken(x.db, *token)
|
user, err := userFromToken(x.db, *token)
|
||||||
@ -324,35 +364,114 @@ func (x Handle) createContext(mode AnswerType, r *http.Request) (*Context, error
|
|||||||
return &Context{token, user, mode}, nil
|
return &Context{token, user, mode}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func logoff(mode AnswerType, w http.ResponseWriter, r *http.Request) {
|
// TODO check if I can use http.Redirect
|
||||||
// Delete cookie
|
func redirect(path string, mode AnswerType, w http.ResponseWriter, r *http.Request) {
|
||||||
cookie := &http.Cookie{
|
w.Header().Set("Location", path)
|
||||||
Name: "auth",
|
if mode == JSON {
|
||||||
Value: "",
|
w.WriteHeader(http.StatusSeeOther)
|
||||||
Expires: time.Unix(0, 0),
|
w.Write([]byte(path))
|
||||||
}
|
return
|
||||||
http.SetCookie(w, cookie)
|
}
|
||||||
|
if mode&(HTMLFULL|HTML) != 0 {
|
||||||
|
w.WriteHeader(http.StatusSeeOther)
|
||||||
|
w.Write([]byte(path))
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Setup response
|
func logoff(mode AnswerType, w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Location", "/login")
|
// Delete cookie
|
||||||
if mode == JSON {
|
cookie := &http.Cookie{
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
Name: "auth",
|
||||||
w.Write([]byte("\"Bye Bye\""));
|
Value: "",
|
||||||
return
|
Expires: time.Unix(0, 0),
|
||||||
}
|
}
|
||||||
if mode & (HTMLFULL | HTML) != 0 {
|
http.SetCookie(w, cookie)
|
||||||
w.WriteHeader(http.StatusUnauthorized);
|
redirect("/login", mode, w, r)
|
||||||
w.Write([]byte("Bye Bye"));
|
}
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusSeeOther);
|
func notAuth(mode AnswerType, w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
if mode == JSON {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
w.Write([]byte("\"You can not access this resource!\""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if mode&(HTMLFULL|HTML) != 0 {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
w.Write([]byte("You can not access this resource!"))
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Handle) staticFiles(pathTest string, fileType string, contentType string) {
|
||||||
|
http.HandleFunc(pathTest, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := r.URL.Path[len(pathTest):]
|
||||||
|
|
||||||
|
if !strings.HasSuffix(path, fileType) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write([]byte("File not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := template.ParseFiles("./views" + pathTest + path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("Failed to load template"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
||||||
|
t.Execute(w, nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorCode(err error, code int, data AnyMap) *Error {
|
||||||
|
// TODO Improve Logging
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Something went wrong returning with: %d\n.Err:\n", code)
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return &Error{code, nil, data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func error500(err error) *Error {
|
||||||
|
return errorCode(err, http.StatusInternalServerError, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Handle) readFiles(pathTest string, baseFilePath string, fileType string, contentType string) {
|
||||||
|
http.HandleFunc(pathTest, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user_path := r.URL.Path[len(pathTest):]
|
||||||
|
|
||||||
|
fmt.Printf("Requested path: %s\n", user_path)
|
||||||
|
|
||||||
|
if !strings.HasSuffix(user_path, fileType) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write([]byte("File not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := os.ReadFile(path.Join(baseFilePath, pathTest, user_path))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("Failed to load file"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
w.Write(bytes)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(db *sql.DB) *Handle {
|
func NewHandler(db *sql.DB) *Handle {
|
||||||
|
|
||||||
var gets []HandleFunc
|
var gets []HandleFunc
|
||||||
var posts []HandleFunc
|
var posts []HandleFunc
|
||||||
x := &Handle{ db, gets, posts, }
|
var deletes []HandleFunc
|
||||||
|
x := &Handle{db, gets, posts, deletes}
|
||||||
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Decide answertype
|
// Decide answertype
|
||||||
@ -380,53 +499,11 @@ func NewHandler(db *sql.DB) *Handle {
|
|||||||
x.handlePosts(w, r, context)
|
x.handlePosts(w, r, context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
panic("TODO handle: " + r.Method)
|
if r.Method == "DELETE" {
|
||||||
})
|
x.handleDeletes(w, r, context)
|
||||||
|
|
||||||
// TODO Handle this in other way
|
|
||||||
http.HandleFunc("/styles/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := r.URL.Path[len("/styles/"):]
|
|
||||||
if !strings.HasSuffix(path, ".css") {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
w.Write([]byte("File not found"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
panic("TODO handle method: " + r.Method)
|
||||||
t, err := template.ParseFiles("./views/styles/" + path)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
w.Write([]byte("Failed to load template"))
|
|
||||||
fmt.Println("Error:")
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
|
||||||
t.Execute(w, nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO Handle this in other way
|
|
||||||
http.HandleFunc("/js/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := r.URL.Path[len("/js/"):]
|
|
||||||
if !strings.HasSuffix(path, ".js") {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
w.Write([]byte("File not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := template.ParseFiles("./views/js/" + path)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
w.Write([]byte("Failed to load template"))
|
|
||||||
fmt.Println("Error:")
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
|
|
||||||
t.Execute(w, nil)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return x
|
return x
|
||||||
|
14
main.go
14
main.go
@ -1,8 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
@ -27,11 +27,21 @@ func main() {
|
|||||||
defer db.Close()
|
defer db.Close()
|
||||||
fmt.Println("Starting server on :8000!")
|
fmt.Println("Starting server on :8000!")
|
||||||
|
|
||||||
|
//TODO check if file structure exists to save data
|
||||||
|
|
||||||
handle := NewHandler(db)
|
handle := NewHandler(db)
|
||||||
|
|
||||||
handle.GetHTML("/", AnswerTemplate("index.html", nil))
|
|
||||||
|
// TODO Handle this in other way
|
||||||
|
handle.staticFiles("/styles/", ".css", "text/css");
|
||||||
|
handle.staticFiles("/js/", ".js", "text/javascript");
|
||||||
|
handle.readFiles("/imgs/", "views", ".png", "image/png;");
|
||||||
|
handle.readFiles("/savedData/", ".", ".png", "image/png;");
|
||||||
|
|
||||||
|
handle.GetHTML("/", AnswerTemplate("index.html", nil, 0))
|
||||||
|
|
||||||
usersEndpints(db, handle)
|
usersEndpints(db, handle)
|
||||||
|
handleModelsEndpoints(handle)
|
||||||
|
|
||||||
handle.Startup()
|
handle.Startup()
|
||||||
}
|
}
|
||||||
|
405
models.go
Normal file
405
models.go
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
_ "image/png"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FAILED_PREPARING = -1
|
||||||
|
|
||||||
|
PREPARING = iota
|
||||||
|
CONFIRM_PRE_TRAINING
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseModel struct {
|
||||||
|
Name string
|
||||||
|
Status int
|
||||||
|
Id string
|
||||||
|
}
|
||||||
|
|
||||||
|
func modelUpdateStatus(handle *Handle, id string, status int) {
|
||||||
|
_, err := handle.db.Exec("update models set status = $1 where id = $2", status, id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to update model status")
|
||||||
|
fmt.Println(err)
|
||||||
|
panic("TODO handle better")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadBaseImage(handle *Handle, id string) {
|
||||||
|
// TODO handle more types than png
|
||||||
|
infile, err := os.Open(path.Join("savedData", id, "baseimage.png"))
|
||||||
|
if err != nil {
|
||||||
|
// TODO better logging
|
||||||
|
fmt.Println(err)
|
||||||
|
fmt.Printf("Failed to read image for model with id %s\n", id)
|
||||||
|
modelUpdateStatus(handle, id, -1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer infile.Close()
|
||||||
|
|
||||||
|
src, format, err := image.Decode(infile)
|
||||||
|
if err != nil {
|
||||||
|
// TODO better logging
|
||||||
|
fmt.Println(err)
|
||||||
|
fmt.Printf("Failed to load image for model with id %s\n", id)
|
||||||
|
modelUpdateStatus(handle, id, -1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if format != "png" {
|
||||||
|
// TODO better logging
|
||||||
|
fmt.Printf("Found unkown format '%s'\n", format)
|
||||||
|
panic("Handle diferent files than .png")
|
||||||
|
}
|
||||||
|
|
||||||
|
var model_color string
|
||||||
|
|
||||||
|
bounds := src.Bounds()
|
||||||
|
width, height := bounds.Max.X, bounds.Max.Y
|
||||||
|
|
||||||
|
switch src.ColorModel() {
|
||||||
|
case color.Gray16Model:
|
||||||
|
fallthrough
|
||||||
|
case color.GrayModel:
|
||||||
|
model_color = "greyscale"
|
||||||
|
default:
|
||||||
|
fmt.Println("Do not know how to handle this color model")
|
||||||
|
|
||||||
|
if src.ColorModel() == color.RGBA64Model {
|
||||||
|
fmt.Println("Color is rgb")
|
||||||
|
} else if src.ColorModel() == color.NRGBAModel {
|
||||||
|
fmt.Println("Color is nrgb")
|
||||||
|
} else if src.ColorModel() == color.YCbCrModel {
|
||||||
|
fmt.Println("Color is ycbcr")
|
||||||
|
} else if src.ColorModel() == color.AlphaModel {
|
||||||
|
fmt.Println("Color is alpha")
|
||||||
|
} else if src.ColorModel() == color.CMYKModel {
|
||||||
|
fmt.Println("Color is cmyk")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Other so assuming color")
|
||||||
|
}
|
||||||
|
|
||||||
|
modelUpdateStatus(handle, id, -1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: this also updates the status to 2
|
||||||
|
_, err = handle.db.Exec("update models set width=$1, height=$2, color_mode=$3, status=$4 where id=$5", width, height, model_color, CONFIRM_PRE_TRAINING, id)
|
||||||
|
if err != nil {
|
||||||
|
// TODO better logging
|
||||||
|
fmt.Println(err)
|
||||||
|
fmt.Printf("Could not update model\n")
|
||||||
|
modelUpdateStatus(handle, id, -1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteModel(handle *Handle, id string, w http.ResponseWriter, c *Context, model BaseModel) {
|
||||||
|
fmt.Printf("Removing model with id: %s\n", id)
|
||||||
|
_, err := handle.db.Exec("delete from models where id=$1;", id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
panic("TODO handle better deleteModel failed delete database query")
|
||||||
|
}
|
||||||
|
|
||||||
|
model_path := path.Join("./savedData", id)
|
||||||
|
fmt.Printf("Removing folder of model with id: %s at %s\n", id, model_path)
|
||||||
|
err = os.RemoveAll(model_path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
panic("TODO handle better deleteModel failed to delete folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Mode == HTML {
|
||||||
|
// TODO move this to a constant so i don't forget
|
||||||
|
w.WriteHeader(309)
|
||||||
|
c.Mode = HTMLFULL
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadBasedOnAnswer(c.Mode, w, "/models/delete.html", c.addMap(AnyMap{
|
||||||
|
"Model": model,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleModelsEndpoints(handle *Handle) {
|
||||||
|
|
||||||
|
// TODO json
|
||||||
|
handle.GetHTML("/models", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||||
|
if c.Mode == JSON {
|
||||||
|
panic("TODO JSON")
|
||||||
|
}
|
||||||
|
if !checkAuthLevel(1, w, r, c) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := handle.db.Query("select id, name from models where user_id=$1;", c.User.id)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
Name string
|
||||||
|
Id string
|
||||||
|
}
|
||||||
|
|
||||||
|
got := []row{}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var r row
|
||||||
|
err = rows.Scan(&r.Id, &r.Name)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
got = append(got, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadBasedOnAnswer(c.Mode, w, "/models/list.html", c.addMap(AnyMap{
|
||||||
|
"List": got,
|
||||||
|
}))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.GetHTML("/models/add", AnswerTemplate("models/add.html", nil, 1))
|
||||||
|
// TODO json
|
||||||
|
handle.Post("/models/add", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||||
|
if c.Mode == JSON {
|
||||||
|
panic("TODO JSON")
|
||||||
|
}
|
||||||
|
if !checkAuthLevel(1, w, r, c) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
read_form, err := r.MultipartReader()
|
||||||
|
if err != nil {
|
||||||
|
LoadBasedOnAnswer(c.Mode, w, "models/add.html", c.addMap(nil))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var name string
|
||||||
|
var file []byte
|
||||||
|
|
||||||
|
for {
|
||||||
|
part, err_part := read_form.NextPart()
|
||||||
|
if err_part == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err_part != nil {
|
||||||
|
return &Error{code: http.StatusBadRequest}
|
||||||
|
}
|
||||||
|
if part.FormName() == "name" {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(part)
|
||||||
|
name = buf.String()
|
||||||
|
}
|
||||||
|
if part.FormName() == "file" {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(part)
|
||||||
|
file = buf.Bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" || len(file) == 0 {
|
||||||
|
LoadBasedOnAnswer(c.Mode, w, "models/add.html", c.addMap(nil))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
row, err := handle.db.Query("select id from models where name=$1 and user_id=$2;", name, c.User.id)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.Next() {
|
||||||
|
LoadBasedOnAnswer(c.Mode, w, "models/add.html", c.addMap(AnyMap{
|
||||||
|
"NameFoundError": true,
|
||||||
|
"Name": name,
|
||||||
|
}))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = handle.db.Exec("insert into models (user_id, name) values ($1, $2)", c.User.id, name)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
row, err = handle.db.Query("select id from models where name=$1 and user_id=$2;", name, c.User.id)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !row.Next() {
|
||||||
|
return &Error{code: http.StatusInternalServerError}
|
||||||
|
}
|
||||||
|
|
||||||
|
var id string
|
||||||
|
err = row.Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO mk this path configurable
|
||||||
|
dir_path := path.Join("savedData", id)
|
||||||
|
|
||||||
|
err = os.Mkdir(dir_path, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
f, err := os.Create(path.Join(dir_path, "baseimage.png"))
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
f.Write(file)
|
||||||
|
|
||||||
|
fmt.Printf("Created model with id %s! Started to proccess image!\n", id)
|
||||||
|
go loadBaseImage(handle, id)
|
||||||
|
|
||||||
|
redirect("/models/edit?id="+id, c.Mode, w, r)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.GetHTML("/models/edit", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||||
|
if !checkAuthLevel(1, w, r, c) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := getIdFromUrl(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return errorCode(nil, http.StatusNotFound, AnyMap{
|
||||||
|
"NotFoundMessage": "Model not found",
|
||||||
|
"GoBackLink": "/models",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO handle admin users
|
||||||
|
rows, err := handle.db.Query("select name, status, width, height, color_mode from models where id=$1 and user_id=$2;", id, c.User.id)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return errorCode(nil, http.StatusNotFound, AnyMap{
|
||||||
|
"NotFoundMessage": "Model not found",
|
||||||
|
"GoBackLink": "/models",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type rowmodel struct {
|
||||||
|
Name string
|
||||||
|
Status int
|
||||||
|
Id string
|
||||||
|
Width *int
|
||||||
|
Height *int
|
||||||
|
Color_mode *string
|
||||||
|
}
|
||||||
|
|
||||||
|
var model rowmodel = rowmodel{}
|
||||||
|
model.Id = id
|
||||||
|
|
||||||
|
err = rows.Scan(&model.Name, &model.Status, &model.Width, &model.Height, &model.Color_mode)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
// All errors will be negative
|
||||||
|
if model.Status < 0 {
|
||||||
|
LoadBasedOnAnswer(c.Mode, w, "/models/edit.html", c.addMap(AnyMap{
|
||||||
|
"Model": model,
|
||||||
|
}))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch model.Status {
|
||||||
|
case PREPARING:
|
||||||
|
LoadBasedOnAnswer(c.Mode, w, "/models/edit.html", c.addMap(AnyMap{
|
||||||
|
"Model": model,
|
||||||
|
}))
|
||||||
|
case CONFIRM_PRE_TRAINING:
|
||||||
|
LoadBasedOnAnswer(c.Mode, w, "/models/edit.html", c.addMap(AnyMap{
|
||||||
|
"Model": model,
|
||||||
|
}))
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Printf("Unkown Status: %d\n", model.Status)
|
||||||
|
return error500(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.Delete("/models/delete", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||||
|
if c.Mode == JSON {
|
||||||
|
panic("TODO handle json on models/delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := MyParseForm(r)
|
||||||
|
if err != nil {
|
||||||
|
return errorCode(err, 400, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !checkId(f, "id") {
|
||||||
|
return errorCode(nil, http.StatusNotFound, AnyMap{
|
||||||
|
"NotFoundMessage": "Model not found",
|
||||||
|
"GoBackLink": "/models",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
id := f.Get("id")
|
||||||
|
|
||||||
|
// TODO handle admin users
|
||||||
|
rows, err := handle.db.Query("select name, status from models where id=$1 and user_id=$2;", id, c.User.id)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return errorCode(nil, http.StatusNotFound, AnyMap{
|
||||||
|
"NotFoundMessage": "Model not found",
|
||||||
|
"GoBackLink": "/models",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var model BaseModel = BaseModel{}
|
||||||
|
model.Id = id
|
||||||
|
|
||||||
|
err = rows.Scan(&model.Name, &model.Status)
|
||||||
|
if err != nil {
|
||||||
|
return error500(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch model.Status {
|
||||||
|
case FAILED_PREPARING:
|
||||||
|
deleteModel(handle, id, w, c, model)
|
||||||
|
return nil
|
||||||
|
case CONFIRM_PRE_TRAINING:
|
||||||
|
|
||||||
|
if checkEmpty(f, "name") {
|
||||||
|
// TODO improve result
|
||||||
|
return errorCode(nil, http.StatusBadRequest, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := f.Get("name")
|
||||||
|
if name != model.Name {
|
||||||
|
LoadError(w, "/models/edit.html", "delete-model-card", c.addMap(AnyMap{
|
||||||
|
"NameDoesNotMatch": true,
|
||||||
|
"Model": model,
|
||||||
|
}))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteModel(handle, id, w, c, model)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
panic("Do not know how to handle model in status:" + strconv.Itoa(model.Status))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -4,10 +4,10 @@ in
|
|||||||
{ pkgs ? import <nixpkgs> {} }:
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
pkgs.mkShell {
|
pkgs.mkShell {
|
||||||
nativeBuildInputs = with unstable; [
|
nativeBuildInputs = with unstable; [
|
||||||
go
|
go
|
||||||
gopls
|
gopls
|
||||||
air
|
air
|
||||||
nodePackages.vscode-css-languageserver-bin
|
nodePackages.vscode-css-languageserver-bin
|
||||||
nodePackages.vscode-html-languageserver-bin
|
nodePackages.vscode-html-languageserver-bin
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
20
sql/models.sql
Normal file
20
sql/models.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- drop table if exists model_defenitions
|
||||||
|
-- drop table if exists models;
|
||||||
|
create table if not exists models (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
user_id uuid references users (id) not null,
|
||||||
|
name varchar (70) not null,
|
||||||
|
-- Status:
|
||||||
|
-- -1: failed preparing
|
||||||
|
-- 1: preparing
|
||||||
|
status integer default 1,
|
||||||
|
|
||||||
|
width integer,
|
||||||
|
height integer,
|
||||||
|
color_mode varchar (20)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- create table model_defenitions (
|
||||||
|
-- id uuid primary key default gen_random_uuid(),
|
||||||
|
-- model_id uuid references models (id) not null,
|
||||||
|
-- )
|
@ -1,8 +1,9 @@
|
|||||||
-- drop table if exists tokens;
|
-- drop table if exists tokens;
|
||||||
|
-- drop table if exists models;
|
||||||
-- drop table if exists users;
|
-- drop table if exists users;
|
||||||
create table if not exists users (
|
create table if not exists users (
|
||||||
id uuid primary key default gen_random_uuid(),
|
id uuid primary key default gen_random_uuid(),
|
||||||
user_type integer default 0,
|
user_type integer default 1,
|
||||||
username varchar (120) not null,
|
username varchar (120) not null,
|
||||||
email varchar (120) not null,
|
email varchar (120) not null,
|
||||||
salt char (8) not null,
|
salt char (8) not null,
|
||||||
|
4
users.go
4
users.go
@ -108,7 +108,7 @@ func generateToken(db *sql.DB, email string, password string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func usersEndpints(db *sql.DB, handle *Handle) {
|
func usersEndpints(db *sql.DB, handle *Handle) {
|
||||||
handle.GetHTML("/login", AnswerTemplate("login.html", nil))
|
handle.GetHTML("/login", AnswerTemplate("login.html", nil, 0))
|
||||||
handle.Post("/login", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
handle.Post("/login", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||||
if c.Mode == JSON {
|
if c.Mode == JSON {
|
||||||
fmt.Println("Handle JSON")
|
fmt.Println("Handle JSON")
|
||||||
@ -148,7 +148,7 @@ func usersEndpints(db *sql.DB, handle *Handle) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
handle.GetHTML("/register", AnswerTemplate("register.html", nil))
|
handle.GetHTML("/register", AnswerTemplate("register.html", nil, 0))
|
||||||
handle.Post("/register", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
handle.Post("/register", func(w http.ResponseWriter, r *http.Request, c *Context) *Error {
|
||||||
if c.Mode == JSON {
|
if c.Mode == JSON {
|
||||||
return &Error{code: http.StatusNotFound}
|
return &Error{code: http.StatusNotFound}
|
||||||
|
147
utils.go
147
utils.go
@ -1,7 +1,150 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "net/url"
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
func checkEmpty(f url.Values, path string) bool {
|
func checkEmpty(f url.Values, path string) bool {
|
||||||
return !f.Has(path) || f.Get(path) == ""
|
return !f.Has(path) || f.Get(path) == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkId(f url.Values, path string) bool {
|
||||||
|
return !checkEmpty(f, path) && isValidUUID(f.Get(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidUUID(u string) bool {
|
||||||
|
_, err := uuid.Parse(u)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIdFromUrl(r *http.Request, target string) (string, error) {
|
||||||
|
if !r.URL.Query().Has(target) {
|
||||||
|
return "", errors.New("Query does not have " + target)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := r.URL.Query().Get("id")
|
||||||
|
if len(id) == 0 {
|
||||||
|
return "", errors.New("Query is empty for " + target)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidUUID(id) {
|
||||||
|
return "", errors.New("Value of query is not a valid uuid for " + target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type maxBytesReader struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
r io.ReadCloser // underlying reader
|
||||||
|
i int64 // max bytes initially, for MaxBytesError
|
||||||
|
n int64 // max bytes remaining
|
||||||
|
err error // sticky error
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaxBytesError struct {
|
||||||
|
Limit int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MaxBytesError) Error() string {
|
||||||
|
// Due to Hyrum's law, this text cannot be changed.
|
||||||
|
return "http: request body too large"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *maxBytesReader) Read(p []byte) (n int, err error) {
|
||||||
|
if l.err != nil {
|
||||||
|
return 0, l.err
|
||||||
|
}
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
// If they asked for a 32KB byte read but only 5 bytes are
|
||||||
|
// remaining, no need to read 32KB. 6 bytes will answer the
|
||||||
|
// question of the whether we hit the limit or go past it.
|
||||||
|
// 0 < len(p) < 2^63
|
||||||
|
if int64(len(p))-1 > l.n {
|
||||||
|
p = p[:l.n+1]
|
||||||
|
}
|
||||||
|
n, err = l.r.Read(p)
|
||||||
|
|
||||||
|
if int64(n) <= l.n {
|
||||||
|
l.n -= int64(n)
|
||||||
|
l.err = err
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n = int(l.n)
|
||||||
|
l.n = 0
|
||||||
|
|
||||||
|
// The server code and client code both use
|
||||||
|
// maxBytesReader. This "requestTooLarge" check is
|
||||||
|
// only used by the server code. To prevent binaries
|
||||||
|
// which only using the HTTP Client code (such as
|
||||||
|
// cmd/go) from also linking in the HTTP server, don't
|
||||||
|
// use a static type assertion to the server
|
||||||
|
// "*response" type. Check this interface instead:
|
||||||
|
type requestTooLarger interface {
|
||||||
|
requestTooLarge()
|
||||||
|
}
|
||||||
|
if res, ok := l.w.(requestTooLarger); ok {
|
||||||
|
res.requestTooLarge()
|
||||||
|
}
|
||||||
|
l.err = &MaxBytesError{l.i}
|
||||||
|
return n, l.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *maxBytesReader) Close() error {
|
||||||
|
return l.r.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func MyParseForm(r *http.Request) (vs url.Values, err error) {
|
||||||
|
if r.Body == nil {
|
||||||
|
err = errors.New("missing form body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ct := r.Header.Get("Content-Type")
|
||||||
|
// RFC 7231, section 3.1.1.5 - empty type
|
||||||
|
// MAY be treated as application/octet-stream
|
||||||
|
if ct == "" {
|
||||||
|
ct = "application/octet-stream"
|
||||||
|
}
|
||||||
|
ct, _, err = mime.ParseMediaType(ct)
|
||||||
|
switch {
|
||||||
|
case ct == "application/x-www-form-urlencoded":
|
||||||
|
var reader io.Reader = r.Body
|
||||||
|
maxFormSize := int64(1<<63 - 1)
|
||||||
|
if _, ok := r.Body.(*maxBytesReader); !ok {
|
||||||
|
maxFormSize = int64(10 << 20) // 10 MB is a lot of text.
|
||||||
|
reader = io.LimitReader(r.Body, maxFormSize+1)
|
||||||
|
}
|
||||||
|
b, e := io.ReadAll(reader)
|
||||||
|
if e != nil {
|
||||||
|
if err == nil {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if int64(len(b)) > maxFormSize {
|
||||||
|
err = errors.New("http: POST too large")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vs, e = url.ParseQuery(string(b))
|
||||||
|
if err == nil {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
case ct == "multipart/form-data":
|
||||||
|
// handled by ParseMultipartForm (which is calling us, or should be)
|
||||||
|
// TODO(bradfitz): there are too many possible
|
||||||
|
// orders to call too many functions here.
|
||||||
|
// Clean this up and write more tests.
|
||||||
|
// request_test.go contains the start of this,
|
||||||
|
// in TestParseMultipartFormOrder and others.
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,25 @@
|
|||||||
<h1>
|
<h1>
|
||||||
404
|
404
|
||||||
</h1>
|
</h1>
|
||||||
<h2>
|
{{ if .NotFoundMessage }}
|
||||||
Page Not found
|
<h2>
|
||||||
</h2>
|
{{ .NotFoundMessage }}
|
||||||
<div class="description">
|
</h2>
|
||||||
The page you were looking for does not exist
|
{{ if .GoBackLink }}
|
||||||
</div>
|
<div class="description">
|
||||||
|
<a hx-get="{{ .GoBackLink }}" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||||
|
👈 Go back
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
<h2>
|
||||||
|
Page Not found
|
||||||
|
</h2>
|
||||||
|
<div class="description">
|
||||||
|
The page you were looking for does not exist
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
BIN
views/imgs/upload-icon.png
Normal file
BIN
views/imgs/upload-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
@ -1,14 +1,49 @@
|
|||||||
function load() {
|
function load() {
|
||||||
for (const elm of document.querySelectorAll("form > button")) {
|
for (const elm of document.querySelectorAll("form > button")) {
|
||||||
elm.addEventListener('click', (e) => {
|
elm.addEventListener('click', (e) => {
|
||||||
e.target.parentElement.classList.add("submitted");
|
e.target.parentElement.classList.add("submitted");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
for (const elm of document.querySelectorAll("button.icon")) {
|
||||||
|
elm.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const input = document.querySelectorAll('form .file-upload input[type="file"]')[0];
|
||||||
|
if (input) {
|
||||||
|
input.click();
|
||||||
|
input.addEventListener('change', (e) => {
|
||||||
|
const file = input.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
elm.setAttribute("disabled", "true");
|
||||||
|
|
||||||
|
const spanToReplace = document.querySelector('.file-upload .icon span');
|
||||||
|
const imgToReplace = document.querySelector('.file-upload .icon img');
|
||||||
|
if (!imgToReplace || !spanToReplace) return;
|
||||||
|
|
||||||
|
if (imgToReplace.getAttribute("replace")) {
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onloadend = () => {
|
||||||
|
imgToReplace.setAttribute("src", fileReader.result)
|
||||||
|
elm.classList.add("adapt");
|
||||||
|
}
|
||||||
|
fileReader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spanToReplace.getAttribute("replace")) {
|
||||||
|
spanToReplace.innerHTML = spanToReplace.getAttribute("replace")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
window.onload = load;
|
window.onload = load;
|
||||||
htmx.on('htmx:afterSwap', load);
|
htmx.on('htmx:afterSwap', load);
|
||||||
htmx.on('htmx:beforeSwap', (env) => {
|
htmx.on('htmx:beforeSwap', (env) => {
|
||||||
if (env.detail.xhr.status === 401) {
|
if (env.detail.xhr.status === 401) {
|
||||||
window.location = "/login"
|
window.location = "/login"
|
||||||
}
|
} else
|
||||||
|
// 309 is the code I selected for html to htmlfull change
|
||||||
|
if (env.detail.xhr.status === 309) {
|
||||||
|
env.detail.target = htmx.find(".app")
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
41
views/models/add.html
Normal file
41
views/models/add.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{{define "title"}}
|
||||||
|
Create New Model : AI Stuff
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "mainbody"}}
|
||||||
|
<main>
|
||||||
|
<h1>
|
||||||
|
Create new Model
|
||||||
|
</h1>
|
||||||
|
<form enctype="multipart/form-data" action="/models/add" method="POST" {{if .Submited}}class="submitted"{{end}} >
|
||||||
|
<fieldset>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input id="name" name="name" required {{if .Name}} value="{{.Name}}" {{end}} />
|
||||||
|
{{if .NameFoundError}}
|
||||||
|
<span class="form-msg error">
|
||||||
|
You already have a model with that name.
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="file-upload" >
|
||||||
|
<label for="file">Base image</label>
|
||||||
|
<div class="form-msg">
|
||||||
|
Please provide a base image.<br/>
|
||||||
|
This image is a sample of the images that you are going to classfy.
|
||||||
|
</div>
|
||||||
|
<div class="icon-holder">
|
||||||
|
<button class="icon">
|
||||||
|
<img replace="true" src="/imgs/upload-icon.png" />
|
||||||
|
<span replace="Image selected">
|
||||||
|
Upload image
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<input id="file" name="file" type="file" required accept="image/png" />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<button>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{{end}}
|
12
views/models/delete.html
Normal file
12
views/models/delete.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{{ define "mainbody" }}
|
||||||
|
<main>
|
||||||
|
<h2 class="text-center">
|
||||||
|
Model {{ .Model.Name }} was deleted!
|
||||||
|
</h2>
|
||||||
|
<div class="description text-center">
|
||||||
|
<a hx-get="/models" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||||
|
👈 Go back
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{{ end }}
|
83
views/models/edit.html
Normal file
83
views/models/edit.html
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{{ define "title" }}
|
||||||
|
Model: {{ .Model.Name }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "base-model-card" }}
|
||||||
|
<div class="card model-card">
|
||||||
|
<h1>
|
||||||
|
{{ .Model.Name }}
|
||||||
|
</h1>
|
||||||
|
<div class="second-line">
|
||||||
|
<img src="/savedData/{{ .Model.Id }}/baseimage.png" />
|
||||||
|
<div class="info">
|
||||||
|
<div>
|
||||||
|
<span class="bold bigger">Image Type:</span> {{ .Model.Color_mode }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="bold bigger">Image Size:</span> {{ .Model.Width }}x{{ .Model.Height }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "delete-model-card" }}
|
||||||
|
<form hx-delete="/models/delete" hx-headers='{"REQUEST-TYPE": "html"}' hx-swap="outerHTML" {{ if .Error }} class="submitted" {{end}} >
|
||||||
|
<fieldset>
|
||||||
|
<span>
|
||||||
|
To delete this model please type "{{ .Model.Name }}":
|
||||||
|
</span>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input name="name" id="name" required />
|
||||||
|
{{ if .NameDoesNotMatch }}
|
||||||
|
<span class="form-msg red">
|
||||||
|
Name does not match "{{ .Model.Name }}"
|
||||||
|
</span>
|
||||||
|
{{ end }}
|
||||||
|
</fieldset>
|
||||||
|
<input type="hidden" name="id" value="{{ .Model.Id }}" />
|
||||||
|
<button class="danger">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "mainbody" }}
|
||||||
|
<main>
|
||||||
|
{{ if (eq .Model.Status 1) }}
|
||||||
|
<div>
|
||||||
|
<h1 class="text-center">
|
||||||
|
{{ .Model.Name }}
|
||||||
|
</h1>
|
||||||
|
<!-- TODO add cool animation -->
|
||||||
|
<h2 class="text-center">
|
||||||
|
Preparing the model
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
{{ else if (eq .Model.Status -1) }}
|
||||||
|
<div>
|
||||||
|
<h1 class="text-center">
|
||||||
|
{{ .Model.Name }}
|
||||||
|
</h1>
|
||||||
|
<!-- TODO improve message -->
|
||||||
|
<h2 class="text-center">
|
||||||
|
Failed to prepare model
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form hx-delete="/models/delete">
|
||||||
|
<input type="hidden" value="{{ .Model.Id }}" />
|
||||||
|
<button class="danger">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{ else if (eq .Model.Status 2) }}
|
||||||
|
{{ template "base-model-card" . }}
|
||||||
|
{{ template "delete-model-card" . }}
|
||||||
|
{{ else }}
|
||||||
|
<h1>
|
||||||
|
Unknown Status of the model.
|
||||||
|
</h1>
|
||||||
|
{{ end }}
|
||||||
|
</main>
|
||||||
|
{{ end }}
|
52
views/models/list.html
Normal file
52
views/models/list.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{{define "title"}}
|
||||||
|
Models : AI Stuff
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "mainbody"}}
|
||||||
|
<main>
|
||||||
|
{{ if (lt 0 (len .List)) }}
|
||||||
|
<div class="list-header">
|
||||||
|
<h2>My Models</h2>
|
||||||
|
<div class="expand"></div>
|
||||||
|
<a class="button" hx-get="/models/add" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||||
|
New
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<!-- Open Button -->
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .List}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{.Name}}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a class="button simple" hx-get="/models/edit?id={{.Id}}" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<h2 class="text-center">
|
||||||
|
You don't have any model
|
||||||
|
</h2>
|
||||||
|
<div class="text-center">
|
||||||
|
<a class="button padded" hx-get="/models/add" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||||
|
Create a new model
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</main>
|
||||||
|
{{end}}
|
@ -5,6 +5,13 @@
|
|||||||
Index
|
Index
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{{ if .Context.User }}
|
||||||
|
<li>
|
||||||
|
<a hx-get="/models" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
|
||||||
|
Models
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
<li class="expand"></li>
|
<li class="expand"></li>
|
||||||
{{ if .Context.User }}
|
{{ if .Context.User }}
|
||||||
<li>
|
<li>
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
* {
|
*{box-sizing: border-box;font-family: 'Roboto', sans-serif;}
|
||||||
box-sizing: border-box;
|
|
||||||
font-family: 'Roboto', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--white: #ffffff;
|
--white: #ffffff;
|
||||||
@ -18,6 +15,10 @@ body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 20px 15vw;
|
||||||
|
}
|
||||||
|
|
||||||
.w100 {
|
.w100 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
@ -32,6 +33,38 @@ body {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigger {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic */
|
||||||
|
.button,
|
||||||
|
button {
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 8px 1px #66666655;
|
||||||
|
background: var(--main);
|
||||||
|
color: var(--black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.padded,
|
||||||
|
button.padded {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.danger,
|
||||||
|
button.danger {
|
||||||
|
background: rgb(var(--red));
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
/* Nav bar */
|
/* Nav bar */
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
@ -49,6 +82,11 @@ nav ul {
|
|||||||
|
|
||||||
nav ul li {
|
nav ul li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul li:first-child {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul .expand {
|
nav ul .expand {
|
||||||
@ -102,6 +140,7 @@ a {
|
|||||||
|
|
||||||
form {
|
form {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
|
margin: 20px 0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 2px 5px 8px 2px #66666655;
|
box-shadow: 2px 5px 8px 2px #66666655;
|
||||||
}
|
}
|
||||||
@ -124,6 +163,7 @@ form input:invalid:focus,
|
|||||||
form.submitted input:invalid {
|
form.submitted input:invalid {
|
||||||
box-shadow: 0 2px 5px 1px rgba(var(--red), 0.2);
|
box-shadow: 0 2px 5px 1px rgba(var(--red), 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
form.submitted input:valid {
|
form.submitted input:valid {
|
||||||
box-shadow: 0 2px 5px 1px rgba(var(--green), 0.2);
|
box-shadow: 0 2px 5px 1px rgba(var(--green), 0.2);
|
||||||
}
|
}
|
||||||
@ -146,16 +186,150 @@ form fieldset .error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form button {
|
form button {
|
||||||
border-radius: 9px 10px;
|
font-size: 1.2rem;
|
||||||
text-align: center;
|
|
||||||
padding: 10px;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
box-shadow: 0 2px 8px 1px #66666655;
|
|
||||||
margin-left: 50%;
|
margin-left: 50%;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
background: var(--main);
|
padding: 10px;
|
||||||
color: var(--black);
|
}
|
||||||
font-size: 1.2rem;
|
|
||||||
|
/* Upload files */
|
||||||
|
|
||||||
|
form fieldset.file-upload input[type="file"] {
|
||||||
|
height: 1px;
|
||||||
|
width: 1px;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
form fieldset.file-upload .icon-holder {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form fieldset.file-upload .icon-holder .icon {
|
||||||
|
height: 150px;
|
||||||
|
width: 150px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: none;
|
||||||
|
transform: none;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
form fieldset.file-upload .icon-holder .icon.adapt {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 80%;
|
||||||
|
max-height: 80%;
|
||||||
|
min-height: 150px;
|
||||||
|
min-width: 150px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form fieldset.file-upload:has(input[type="file"]:invalid:focus) .icon,
|
||||||
|
form.submitted fieldset.file-upload:has(input[type="file"]:invalid:focus) .icon {
|
||||||
|
box-shadow: 0 2px 5px 1px rgba(var(--red), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.submitted fieldset.file-upload:has(input[type="file"]:valid:focus) .icon{
|
||||||
|
box-shadow: 0 2px 5px 1px rgba(var(--green), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
form fieldset.file-upload .icon-holder .icon img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 80%;
|
||||||
|
object-fit: contain;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
form fieldset.file-upload .icon-holder .icon span {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
display: flex;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header .expand {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header .button,
|
||||||
|
.list-header button {
|
||||||
|
padding: 10px 10px;
|
||||||
|
height: calc(100% - 20px);
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 2px 8px 1px #66666622;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead {
|
||||||
|
background: #60606022;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr td,
|
||||||
|
table tr th {
|
||||||
|
border-left: 1px solid #22222244;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr td:first-child,
|
||||||
|
table tr th:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr td button,
|
||||||
|
table tr td .button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
box-shadow: 0 2px 5px 1px #66666655;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 2px 5px 1px #66666655;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Model stuff */
|
||||||
|
.model-card h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-card img {
|
||||||
|
width: 25%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-card .second-line {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user