diff --git a/.air.toml b/.air.toml
index e58a42a..d883ef7 100644
--- a/.air.toml
+++ b/.air.toml
@@ -4,16 +4,16 @@ tmp_dir = "tmp"
[build]
args_bin = []
-bin = "./tmp/main"
-cmd = "go build -o ./tmp/main ."
+bin = "tmp/server"
+cmd = "make"
delay = 1000
exclude_dir = [
"assets",
"tmp",
"vendor",
"testdata",
+ "web/frontend/dist",
"web/frontend/node_modules",
- "web/static",
]
exclude_file = []
exclude_regex = ["_test.go"]
diff --git a/.env b/.env
new file mode 100644
index 0000000..5c7b238
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+SESSION_KEY="67d829bf61dc5f87a73fd814e2c9f629"
diff --git a/.gitignore b/.gitignore
index d9dbc43..25e7016 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,28 @@ go.work
tmp
web/static/css/.DS_Store
+#
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/Makefile b/Makefile
index e5f387b..a41ffcb 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ SVELTE_COMPONENTS = status
SVELTE_TARGETS = $(addprefix $(FRONTEND)/public/build/,$(addsuffix .ts, $(SVELTE_COMPONENTS)))
-.PHONY: $(TARGET)
+.PHONY: $(TARGET) clean
.NOTPARALLEL:
$(TARGET): $(SVELTE_TARGETS)
@@ -15,3 +15,6 @@ $(TARGET): $(SVELTE_TARGETS)
$(SVELTE_TARGETS): $(SVELTE_SRC)
$(info ===> BUILD frontend)
cd $(FRONTEND) && npm install && npm run build
+
+clean:
+ @rm -rf tmp
diff --git a/internal/api/news.go b/internal/api/news.go
new file mode 100644
index 0000000..09edf9b
--- /dev/null
+++ b/internal/api/news.go
@@ -0,0 +1,100 @@
+package api
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "net/http"
+ "strconv"
+
+ "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
+)
+
+func createNewsItem(rw http.ResponseWriter, r *http.Request) {
+ req := repository.CreateNewsEntryParams{}
+ if err := decode(r.Body, &req); err != nil {
+ handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
+ return
+ }
+
+ repo := repository.GetRepository()
+ err := repo.CreateNewsEntry(r.Context(), req)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+}
+
+func readNewsItems(rw http.ResponseWriter, r *http.Request) {
+ repo := repository.GetRepository()
+ items, err := repo.ListNews(r.Context())
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+
+ slog.Debug("/api/news returned", "newscount", len(items))
+ rw.Header().Add("Content-Type", "application/json")
+ bw := bufio.NewWriter(rw)
+ defer bw.Flush()
+
+ if err := json.NewEncoder(bw).Encode(items); err != nil {
+ handleError(err, http.StatusInternalServerError, rw)
+ return
+ }
+}
+
+func readNewsItem(rw http.ResponseWriter, r *http.Request) {
+ repo := repository.GetRepository()
+ id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+
+ item, err := repo.GetNewsEntry(r.Context(), id)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+
+ rw.Header().Add("Content-Type", "application/json")
+ bw := bufio.NewWriter(rw)
+ defer bw.Flush()
+
+ if err := json.NewEncoder(bw).Encode(item); err != nil {
+ handleError(err, http.StatusInternalServerError, rw)
+ return
+ }
+}
+
+func updateNewsItem(rw http.ResponseWriter, r *http.Request) {
+ req := repository.UpdateNewsEntryParams{}
+ if err := decode(r.Body, &req); err != nil {
+ handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
+ return
+ }
+
+ repo := repository.GetRepository()
+ err := repo.UpdateNewsEntry(r.Context(), req)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+}
+
+func deleteNewsItem(rw http.ResponseWriter, r *http.Request) {
+ repo := repository.GetRepository()
+ id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+
+ err = repo.DeleteNewsEntry(r.Context(), id)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+}
diff --git a/internal/api/rest.go b/internal/api/rest.go
index aa71d3d..a5b22b7 100644
--- a/internal/api/rest.go
+++ b/internal/api/rest.go
@@ -1,15 +1,10 @@
package api
import (
- "bufio"
"encoding/json"
- "fmt"
"io"
"log/slog"
"net/http"
- "strconv"
-
- "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
)
type ErrorResponse struct {
@@ -24,6 +19,12 @@ func MountApiEndpoints(r *http.ServeMux) {
r.HandleFunc("GET /api/news/{id}", readNewsItem)
r.HandleFunc("PATCH /api/news/", updateNewsItem)
r.HandleFunc("DELETE /api/news/{id}", deleteNewsItem)
+
+ r.HandleFunc("POST /api/retailers/", createRetailerItem)
+ r.HandleFunc("GET /api/retailers/", readRetailerItems)
+ r.HandleFunc("GET /api/retailers/{id}", readRetailerItem)
+ r.HandleFunc("PATCH /api/retailers/", updateRetailerItem)
+ r.HandleFunc("DELETE /api/retailers/{id}", deleteRetailerItem)
}
func handleError(err error, statusCode int, rw http.ResponseWriter) {
@@ -41,91 +42,3 @@ func decode(r io.Reader, val any) error {
dec.DisallowUnknownFields()
return dec.Decode(val)
}
-
-func createNewsItem(rw http.ResponseWriter, r *http.Request) {
- req := repository.CreateNewsEntryParams{}
- if err := decode(r.Body, &req); err != nil {
- handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
- return
- }
-
- repo := repository.GetRepository()
- err := repo.CreateNewsEntry(r.Context(), req)
- if err != nil {
- handleError(err, http.StatusBadRequest, rw)
- return
- }
-}
-
-func readNewsItems(rw http.ResponseWriter, r *http.Request) {
- repo := repository.GetRepository()
- items, err := repo.ListNews(r.Context())
- if err != nil {
- handleError(err, http.StatusBadRequest, rw)
- return
- }
-
- slog.Debug("/api/news returned", "newscount", len(items))
- rw.Header().Add("Content-Type", "application/json")
- bw := bufio.NewWriter(rw)
- defer bw.Flush()
-
- if err := json.NewEncoder(bw).Encode(items); err != nil {
- handleError(err, http.StatusInternalServerError, rw)
- return
- }
-}
-
-func readNewsItem(rw http.ResponseWriter, r *http.Request) {
- repo := repository.GetRepository()
- id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
- if err != nil {
- handleError(err, http.StatusBadRequest, rw)
- return
- }
-
- item, err := repo.GetNewsEntry(r.Context(), id)
- if err != nil {
- handleError(err, http.StatusBadRequest, rw)
- return
- }
-
- rw.Header().Add("Content-Type", "application/json")
- bw := bufio.NewWriter(rw)
- defer bw.Flush()
-
- if err := json.NewEncoder(bw).Encode(item); err != nil {
- handleError(err, http.StatusInternalServerError, rw)
- return
- }
-}
-
-func updateNewsItem(rw http.ResponseWriter, r *http.Request) {
- req := repository.UpdateNewsEntryParams{}
- if err := decode(r.Body, &req); err != nil {
- handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
- return
- }
-
- repo := repository.GetRepository()
- err := repo.UpdateNewsEntry(r.Context(), req)
- if err != nil {
- handleError(err, http.StatusBadRequest, rw)
- return
- }
-}
-
-func deleteNewsItem(rw http.ResponseWriter, r *http.Request) {
- repo := repository.GetRepository()
- id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
- if err != nil {
- handleError(err, http.StatusBadRequest, rw)
- return
- }
-
- err = repo.DeleteNewsEntry(r.Context(), id)
- if err != nil {
- handleError(err, http.StatusBadRequest, rw)
- return
- }
-}
diff --git a/internal/api/retailer.go b/internal/api/retailer.go
new file mode 100644
index 0000000..5acd264
--- /dev/null
+++ b/internal/api/retailer.go
@@ -0,0 +1,100 @@
+package api
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "net/http"
+ "strconv"
+
+ "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
+)
+
+func createRetailerItem(rw http.ResponseWriter, r *http.Request) {
+ req := repository.CreateNewsEntryParams{}
+ if err := decode(r.Body, &req); err != nil {
+ handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
+ return
+ }
+
+ repo := repository.GetRepository()
+ err := repo.CreateNewsEntry(r.Context(), req)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+}
+
+func readRetailerItems(rw http.ResponseWriter, r *http.Request) {
+ repo := repository.GetRepository()
+ items, err := repo.ListRetailers(r.Context())
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+
+ slog.Debug("/api/retailers returned", "count", len(items))
+ rw.Header().Add("Content-Type", "application/json")
+ bw := bufio.NewWriter(rw)
+ defer bw.Flush()
+
+ if err := json.NewEncoder(bw).Encode(items); err != nil {
+ handleError(err, http.StatusInternalServerError, rw)
+ return
+ }
+}
+
+func readRetailerItem(rw http.ResponseWriter, r *http.Request) {
+ repo := repository.GetRepository()
+ id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+
+ item, err := repo.GetNewsEntry(r.Context(), id)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+
+ rw.Header().Add("Content-Type", "application/json")
+ bw := bufio.NewWriter(rw)
+ defer bw.Flush()
+
+ if err := json.NewEncoder(bw).Encode(item); err != nil {
+ handleError(err, http.StatusInternalServerError, rw)
+ return
+ }
+}
+
+func updateRetailerItem(rw http.ResponseWriter, r *http.Request) {
+ req := repository.UpdateNewsEntryParams{}
+ if err := decode(r.Body, &req); err != nil {
+ handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
+ return
+ }
+
+ repo := repository.GetRepository()
+ err := repo.UpdateNewsEntry(r.Context(), req)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+}
+
+func deleteRetailerItem(rw http.ResponseWriter, r *http.Request) {
+ repo := repository.GetRepository()
+ id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+
+ err = repo.DeleteNewsEntry(r.Context(), id)
+ if err != nil {
+ handleError(err, http.StatusBadRequest, rw)
+ return
+ }
+}
diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go
index c2b9d12..57cd45a 100644
--- a/internal/handlers/admin.go
+++ b/internal/handlers/admin.go
@@ -1,6 +1,7 @@
package handlers
import (
+ "fmt"
"html/template"
"log/slog"
"net/http"
@@ -13,11 +14,12 @@ func AdminHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tpl := template.Must(template.ParseFS(web.Templates, "templates/admin.html", "templates/base.html"))
viteFragment, err := vite.HTMLFragment(vite.Config{
- FS: web.StaticAssets,
+ FS: web.DistFS(),
IsDev: false,
})
if err != nil {
- http.Error(w, "Error instantiating vite fragment", http.StatusInternalServerError)
+ msg := fmt.Errorf("error instantiating vite fragment: %v", err)
+ http.Error(w, msg.Error(), http.StatusInternalServerError)
return
}
diff --git a/main.go b/main.go
index 7262715..ff66ea2 100644
--- a/main.go
+++ b/main.go
@@ -11,12 +11,14 @@ import (
"strings"
"time"
+ "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/api"
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/auth"
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/handlers"
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/middleware"
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
"git.clustercockpit.org/moebiusband/go-http-skeleton/web"
"github.com/joho/godotenv"
+ "golang.org/x/crypto/bcrypt"
_ "modernc.org/sqlite"
)
@@ -116,8 +118,14 @@ func main() {
ctx := context.Background()
q := repository.GetRepository()
+ hash, err := bcrypt.GenerateFromPassword([]byte(parts[1]), bcrypt.DefaultCost)
+ password := string(hash)
+ if err != nil {
+ slog.Error("Error while encrypting new user password")
+ os.Exit(1)
+ }
if err := q.CreateUser(ctx, repository.CreateUserParams{
- UserName: parts[0], UserPass: &parts[1],
+ UserName: parts[0], UserPass: &password,
}); err != nil {
slog.Error("Add User: Could not add new user authentication", "username", parts[0], "error", err.Error())
os.Exit(1)
@@ -215,6 +223,7 @@ func main() {
})
})))
+ api.MountApiEndpoints(mux)
mux.HandleFunc("GET /", handlers.RootHandler())
securedMux := http.NewServeMux()
diff --git a/web/frontend/.gitignore b/web/frontend/.gitignore
deleted file mode 100644
index a547bf3..0000000
--- a/web/frontend/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/web/frontend/src/App.svelte b/web/frontend/src/App.svelte
index 9e85973..dd23da9 100644
--- a/web/frontend/src/App.svelte
+++ b/web/frontend/src/App.svelte
@@ -1,45 +1,139 @@
+
+
+
+
+
+
+
+ {#each columns as col}
+
+
+
+
+ {/each}
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+ {#if activeTab === "invoice"}
+
Invoice list
+ {/if}
+ {#if activeTab === "retailers"}
+
+
+ {/if}
+ {#if activeTab === "news"}
+
news
+ {/if}
-
Vite + Svelte
-
-
-
-
-
-
- Check out SvelteKit, the official Svelte app framework powered by Vite!
-
-
-
Click on the Vite and Svelte logos to learn more
-
-
diff --git a/web/frontend/src/lib/GenericForm.svelte b/web/frontend/src/lib/GenericForm.svelte
new file mode 100644
index 0000000..3d08f0e
--- /dev/null
+++ b/web/frontend/src/lib/GenericForm.svelte
@@ -0,0 +1,14 @@
+
+
+
diff --git a/web/frontend/src/lib/Table.svelte b/web/frontend/src/lib/Table.svelte
new file mode 100644
index 0000000..f89c564
--- /dev/null
+++ b/web/frontend/src/lib/Table.svelte
@@ -0,0 +1,48 @@
+
+
+
+
+
+ {#each columns as col}
+ {col} |
+ {/each}
+ edit |
+ remove |
+
+
+
+ {#each records as row}
+
+ {#each columns as col}
+ {row[col]} |
+ {/each}
+
+
+ |
+
+
+ |
+
+ {/each}
+
+
diff --git a/web/frontend/vite.config.ts b/web/frontend/vite.config.ts
index d32eba1..f7cd9fd 100644
--- a/web/frontend/vite.config.ts
+++ b/web/frontend/vite.config.ts
@@ -4,4 +4,7 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
+ build: {
+ manifest: true
+ }
})
diff --git a/web/templates.go b/web/templates.go
index 11dfec9..b0bf0a9 100644
--- a/web/templates.go
+++ b/web/templates.go
@@ -2,6 +2,8 @@ package web
import (
"embed"
+ "fmt"
+ "io/fs"
"net/http"
"text/template"
@@ -19,9 +21,17 @@ type PageData struct {
//go:embed templates
var Templates embed.FS
-//go:embed frontend/dist
+//go:embed all:frontend/dist
var StaticAssets embed.FS
+func DistFS() fs.FS {
+ efs, err := fs.Sub(StaticAssets, "frontend/dist")
+ if err != nil {
+ panic(fmt.Sprintf("unable to serve frontend: %v", err))
+ }
+ return efs
+}
+
func RenderTemplate(w http.ResponseWriter, name string, data PageData) error {
tpl := template.Must(template.ParseFS(Templates, "templates/"+name+".html", "templates/base.html"))
return tpl.ExecuteTemplate(w, "base", data)
diff --git a/web/templates/admin.html b/web/templates/admin.html
index 3f9a328..0146df6 100644
--- a/web/templates/admin.html
+++ b/web/templates/admin.html
@@ -1,3 +1,3 @@
{{define "vite"}} {{ .Vite.Tags }} {{end}} {{define "content"}}
-
+
{{end}}