This commit is contained in:
bronku 2025-03-31 07:32:17 +02:00
parent c32ecbff8f
commit 3136739f4e
53 changed files with 129 additions and 1892 deletions

View file

@ -28,7 +28,7 @@
## Security
- [x] Authentication System
- [x] Persistant logins accross server restarts
- [x] Persistent logins across server restarts
- [x] User login
- [ ] User logout
- [ ] Role-based access (admin, staff)

BIN
foo.db

Binary file not shown.

View file

@ -4,9 +4,13 @@
<main>
<form method="post">
<label>Login</label>
<input type="text" name="login" />
<label>
<input type="text" name="login" />
</label>
<label>Password</label>
<input type="password" name="password" />
<label>
<input type="password" name="password" />
</label>
<button type="submit">login</button>
</form>
</main>

View file

@ -15,7 +15,7 @@ func PasswordHash(password, salt string) string {
func GenerateKey() string {
key := [32]byte{}
if _, err := rand.Read(key[:]); err != nil {
log.Fatal("can't generate a vaild key", err)
log.Fatal("can't generate a valid key", err)
}
return base64.StdEncoding.EncodeToString(key[:])
}

View file

@ -27,7 +27,7 @@ func (h *Server) postCake(r *http.Request) (any, int, error) {
return nil, http.StatusBadRequest, err
}
n.Category = r.FormValue("category")
n.Availability = r.FormValue("availibility")
n.Availability = r.FormValue("availability")
n.ID, err = h.s.SaveCake(n)
fmt.Println(err)
return n, http.StatusAccepted, err

View file

@ -6,20 +6,26 @@ Cake {{.ID}}
<form method="post">
<div>
<h2>Info</h2>
<input hidden="true" name="id" value="{{.ID}}">
<label>
<input hidden="hidden" name="id" value="{{.ID}}">
</label>
<label>name</label>
<input type="text" name="name" value="{{.Name}}">
<label>
<input type="text" name="name" value="{{.Name}}">
</label>
<label>price</label>
<input type="number" name="price" min="0" value="{{.Price}}">
<label>
<input type="number" name="price" min="0" value="{{.Price}}">
</label>
<label>category</label>
<select name="category" id="category" >
<label for="category"></label><select name="category" id="category" >
<option {{ if eq .Category "common" }}selected{{ end }} value="common">common</option>
<option {{ if eq .Category "christmas" }}selected{{ end }} value="christmas">christmas</option>
<option {{ if eq .Category "easter" }}selected{{ end }} value="easter">easter</option>
<option {{ if eq .Category "donuts" }}selected{{ end }} value="donuts">donuts</option>
</select>
<label>availability</label>
<select name="availibility" id="availibility" >
<label for="availability"></label><select name="availability" id="availability" >
<option {{ if eq .Availability "available" }}selected{{ end }} value="available">available</option>
<option {{ if eq .Availability "unavailable" }}selected{{ end }} value="unavailable">unavailable</option>
</select>

View file

@ -1,9 +1,10 @@
{{define "layout"}}
<!doctype html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{{template "title" .}}</title>
<!--suppress HtmlUnknownTarget -->
<script src="/static/htmx2.04.js" defer></script>
</head>
<body>

View file

@ -2,89 +2,105 @@
Order {{.Order.ID}}
{{end}}
{{define "main"}}
<main>
<form method="post">
{{with .Order}}
<div>
<h2>Info</h2>
<input hidden="true" name="id" value="{{.ID}}">
<label>name</label>
<input type="text" name="name" value="{{.Name}}">
<label>surname</label>
<input type="text" name="surname" value="{{.Surname}}">
<label>phone</label>
<input type="tel" name="phone" value="{{.Phone}}">
<label>location</label>
<select name="location">
<option {{ if eq .Location "Kartuzy" }}selected{{ end }} value="Kartuzy">Kartuzy</option>
<option {{ if eq .Location "Somonino" }}selected{{ end }} value="Somonino">Somonino</option>
</select>
<label>date</label>
<input type="date" name="date" value="{{.Date.Format "2006-01-02"}}">
<label>status</label>
<select name="status" id="status" >
<option {{ if eq .Status "accepted" }}selected{{ end }} value="accepted">accepted</option>
<option {{ if eq .Status "done" }}selected{{ end }} value="done">done</option>
</select>
<label>paid</label>
<input type="number" name="paid" min="0" value="{{.Paid}}">
</div>
<div>
<h2>Basket</h2>
<ul id="basket">
</ul>
</div>
{{end}}
{{with .Catalogue}}
<div>
<h2>Catalogue</h2>
<ul>
{{range .}}
<li>
{{.Name}} {{.Price}} <button type=button onClick="addCake({{.ID}},'{{.Name}} {{.Price}}', 1)">add</button>
</li>
{{end}}
</ul>
</div>
{{end}}
<button type="submit">submit</button>
</form>
</main>
<script>
function addCake(id, name, amount){
var cake = document.getElementById("cake["+id+"]")
if (cake != null){
var value = Number(cake.getAttribute("value"))
cake.setAttribute("value", value+1)
return;
<main>
<form method="post">
{{with .Order}}
<div>
<h2>Info</h2>
<label>
<input hidden="hidden" name="id" value="{{.ID}}">
</label>
<label>name</label>
<label>
<input type="text" name="name" value="{{.Name}}">
</label>
<label>surname</label>
<label>
<input type="text" name="surname" value="{{.Surname}}">
</label>
<label>phone</label>
<label>
<input type="tel" name="phone" value="{{.Phone}}">
</label>
<label>location</label>
<label>
<select name="location">
<option {{ if eq .Location "Kartuzy" }}selected{{ end }} value="Kartuzy">Kartuzy</option>
<option {{ if eq .Location "Somonino" }}selected{{ end }} value="Somonino">Somonino</option>
</select>
</label>
<label>date</label>
<label>
<input type="date" name="date" value="{{.Date.Format "2006-01-02"}}">
</label>
<label>status</label>
<label for="status"></label><select name="status" id="status">
<option {{ if eq .Status "accepted" }}selected{{ end }} value="accepted">accepted</option>
<option {{ if eq .Status "done" }}selected{{ end }} value="done">done</option>
</select>
<label>paid</label>
<label>
<input type="number" name="paid" min="0" value="{{.Paid}}">
</label>
</div>
<div>
<h2>Basket</h2>
<ul id="basket">
</ul>
</div>
{{end}}
{{with .Catalogue}}
<div>
<h2>Catalogue</h2>
<ul>
{{range .}}
<li>
{{.Name}} {{.Price}}
<button type=button onClick="addCake({{.ID}},'{{.Name}} {{.Price}}', 1)">add</button>
</li>
{{end}}
</ul>
</div>
{{end}}
<button type="submit">submit</button>
</form>
</main>
<!--suppress JSUnusedGlobalSymbols, JSCheckFunctionSignatures -->
<script>
function addCake(id, name, amount) {
const cake = document.getElementById("cake[" + id + "]");
if (cake != null) {
const value = Number(cake.getAttribute("value"));
cake.setAttribute("value", value + 1)
return;
}
const label = document.createElement("label");
label.innerText = name
const input = document.createElement("input");
input.setAttribute("id", "cake[" + id + "]")
input.setAttribute("type", "number")
input.setAttribute("min", "0")
input.setAttribute("name", "cake[" + id + "]")
input.setAttribute("value", amount)
const button = document.createElement("button");
button.setAttribute("type", "button")
button.setAttribute("onClick", "removeCake(" + id + ")")
button.innerText = "delete"
const li = document.createElement("li");
li.appendChild(label)
li.appendChild(input)
li.appendChild(button)
li.setAttribute("id", "li[" + id + "]")
const ul = document.getElementById("basket");
ul.appendChild(li)
}
var label = document.createElement("label")
label.innerText = name
var input = document.createElement("input")
input.setAttribute("id","cake["+id+"]")
input.setAttribute("type", "number")
input.setAttribute("min", "0")
input.setAttribute("name", "cake["+id+"]")
input.setAttribute("value", amount)
var button = document.createElement("button")
button.setAttribute("type", "button")
button.setAttribute("onClick", "removeCake("+id+")")
button.innerText= "delete"
var li = document.createElement("li")
li.appendChild(label)
li.appendChild(input)
li.appendChild(button)
li.setAttribute("id", "li["+id+"]")
var ul = document.getElementById("basket")
ul.appendChild(li)
}
function removeCake(id){
document.getElementById("li["+id+"]").remove()
}
function removeCake(id) {
document.getElementById("li[" + id + "]").remove()
}
{{range .Order.Cakes}}
{{range .Order.Cakes}}
addCake({{.ID}}, '{{.Name}} {{.Price}}', {{.Amount}})
{{end}}
</script>
{{end}}
</script>
{{end}}

View file

@ -4,13 +4,15 @@
{{end}}
{{define "main"}}
<main>
<form class="search-container" hx-get="/orders/search" hx-target="#results-table", hx-trigger="keyup delay:500ms, change">
<input type="search" name="q" placeholder="Search orders...">
<form class="search-container" hx-get="/orders/search" hx-target="#results-table" hx-trigger="keyup delay:500ms, change">
<label>
<input type="search" name="q" placeholder="Search orders...">
</label>
<label for="from">form</label>
<input type="date" value="{{.Today}}" id="from" name="from">
<label for="from">to</label>
<input type="date" value="{{.Today}}" id="to" name="to">
<label for="to"></label><input type="date" value="{{.Today}}" id="to" name="to">
<div class="htmx-indicator">
Searching...

View file

@ -48,7 +48,6 @@ func (s *Store) getOrdersFromQuery(query string, args ...any) ([]models.Order, e
var out []models.Order
rows, err := s.db.Query(query, args...)
if err != nil {
fmt.Println("gofq", err)
return out, err
}
defer rows.Close()

View file

@ -63,7 +63,7 @@ func TestStore(t *testing.T) {
updatedCake.ID = 10
_, err = s.SaveCake(updatedCake)
if err == nil {
t.Error("did not return an error when attempted to update non existant cake")
t.Error("did not return an error when attempted to update non existent cake")
}
// get cakes
@ -105,7 +105,7 @@ func TestStore(t *testing.T) {
selectedCake, err = s.GetCake(-1)
if err == nil {
t.Error("no error getting a non-existant cake")
t.Error("no error getting a non-existent cake")
}
// create new order
@ -166,7 +166,7 @@ func TestStore(t *testing.T) {
updatedOrder.ID = 10
_, err = s.SaveOrder(updatedOrder)
if err == nil {
t.Error("did not return an error when attempted to update non existant cake")
t.Error("did not return an error when attempted to update non existent cake")
}
// get orders

View file

@ -1,31 +0,0 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
*.DS_Store
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
.idea
tmp
tmp/main
foo.db.bak

View file

@ -1,3 +0,0 @@
# order tracking app for a pastry shop
I don't remember what iroon stood for

View file

@ -1,138 +0,0 @@
# TODO List for Iroon Project
## Critical Features
- [x] Database Implementation
- [x] Init mode to create a new db
- [x] Set up SQLite database
- [x] Create proper schemas
- [x] Implement data persistence
- [x] Add database migrations
- [x] Add database caching
- [ ] Input Validation
- [ ] Phone number format validation
- [ ] Required fields validation
- [ ] Date validation (no past dates)
- [ ] Cake amount limits
- [ ] Form validation feedback
- [ ] Price Management
- [ ] Calculate order totals
- [x] Handle partial payments
- [ ] Add payment status tracking
- [ ] Implement payment validation
## Security
- [x] Authentication System
- [x] Persistant logins accross server restarts
- [x] User login
- [ ] User logout
- [ ] Role-based access (admin, staff)
- [x] Password hashing
- [ ] CSRF Protection
- [ ] Input Sanitization
- [x] Session Management
## UI/UX Improvements
- [ ] Add CSS Styling
- [ ] Responsive design
- [ ] Dark/Light theme
- [ ] Print-friendly styles
- [ ] Improve Navigation
- [ ] Add Loading States
- [x] Confirmation Dialogs
- [ ] Form Autosave
- [x] Success/Error Notifications
## Order Management
- [x] Order Status System
- [ ] Order Search & Filtering
- [ ] By date range
- [ ] By status
- [ ] By customer
- [ ] Order History
- [ ] Order Notes/Comments
- [ ] Bulk Order Operations
## Inventory Management
- [ ] Track Ingredients
- [ ] Low Stock Alerts
- [x] Cake Catalog Management
- [x] Add/Edit/Delete cakes
- [x] Cake categories
- [ ] Multiple categories for each cake
- [ ] Cake images
- [x] Seasonal Menu Items
## Reporting & Analytics
- [ ] Sales Reports
- [ ] Popular Items
- [ ] Customer Analytics
- [ ] Revenue Reports
- [ ] Export Data (CSV/PDF)
## Customer Features
- [ ] Customer Accounts
- [ ] Order History
- [ ] Favorite Items
- [ ] Loyalty Program
- [ ] Reviews & Ratings
## Communication
- [ ] Email Notifications
- [ ] Order confirmation
- [ ] Status updates
- [ ] Payment reminders
- [ ] SMS Notifications
- [ ] Print Order Slips
## Additional Features
- [ ] Calendar View
- [ ] Daily orders
- [ ] Capacity planning
- [ ] Delivery Management
- [ ] Delivery zones
- [ ] Delivery fees
- [ ] Driver assignments
- [ ] Special Requirements
- [ ] Allergies
- [ ] Custom decorations
- [ ] Special messages
- [ ] Multi-language Support
- [ ] Backup System
## Technical Improvements
- [ ] Error Handling
- [ ] Proper error logging
- [ ] User-friendly error messages
- [ ] A better way to handle the error page
- [ ] Performance Optimization
- [ ] API Documentation
- [ ] Unit Tests
- [ ] Integration Tests
- [x] File structure
- [x] Embed templates
- [x] Single init function for handler
## Future Considerations
- [ ] Mobile App
- [ ] Online Payment Integration
- [ ] Integration with POS Systems
- [ ] Customer Feedback System
- [ ] Marketing Tools
- [ ] Promotions
- [ ] Discount codes
- [ ] Gift cards

View file

@ -1,26 +0,0 @@
package iroon
import (
"fmt"
"log"
"net/http"
"github.com/Bronku/iroon/internal/auth"
"github.com/Bronku/iroon/internal/logging"
"github.com/Bronku/iroon/internal/server"
"github.com/Bronku/iroon/internal/store"
)
func Run() {
s := store.OpenStore("./foo.db")
s.AddUser("admin", "secret")
defer s.Close()
h := server.New(s)
defer h.Close()
var handler http.Handler = h
handler = logging.Middleware(handler)
handler = auth.New(s).Middleware(handler)
fmt.Println("starting server")
log.Fatal(http.ListenAndServe(":8080", handler))
}

Binary file not shown.

View file

@ -1,12 +0,0 @@
module github.com/Bronku/iroon
go 1.23.5
require github.com/mattn/go-sqlite3 v1.14.24
require github.com/knaka/go-sqlite3-fts5 v0.0.0-20240729040425-e53b86878d0d
require (
golang.org/x/crypto v0.36.0
golang.org/x/sys v0.31.0 // indirect
)

View file

@ -1,8 +0,0 @@
github.com/knaka/go-sqlite3-fts5 v0.0.0-20240729040425-e53b86878d0d h1:I3lRivq7Zx0fqlKhCJG1KaL2tLG6aiHDj3bvJJqppKw=
github.com/knaka/go-sqlite3-fts5 v0.0.0-20240729040425-e53b86878d0d/go.mod h1:kDHCqub/PNhQnqg8ur7OYO49jpZ+pM0mqBQ768DE3UU=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

View file

@ -1,48 +0,0 @@
package auth
import (
_ "embed"
"fmt"
"log"
"net/http"
"github.com/Bronku/iroon/internal/models"
"github.com/Bronku/iroon/internal/store"
)
type Authenticator struct {
sessions map[string]models.Token
s *store.Store
}
func New(s *store.Store) *Authenticator {
var out Authenticator
var err error
out.s = s
out.sessions, err = s.GetSessions()
//fmt.Println(out.sessions)
if err != nil {
log.Fatal(err)
}
return &out
}
func (a *Authenticator) ensureAuth(in http.Handler) http.Handler {
fmt.Println("ensureAuth called")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := a.getSession(r)
if err != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
in.ServeHTTP(w, r)
})
}
func (a *Authenticator) Middleware(in http.Handler) http.Handler {
handler := http.NewServeMux()
handler.HandleFunc("GET /login", getLogin)
handler.HandleFunc("POST /login", a.login)
handler.Handle("/", a.ensureAuth(in))
return handler
}

View file

@ -1,19 +0,0 @@
package auth
import (
"errors"
"github.com/Bronku/iroon/internal/crypto"
)
func (a *Authenticator) verifyCredentials(login, password string) error {
user, ok := a.s.GetUser(login)
if !ok {
return errors.New("user with this login doesn't exist")
}
hash := crypto.PasswordHash(password, user.Salt)
if hash == user.Password {
return nil
}
return errors.New("wrong credentials")
}

View file

@ -1,17 +0,0 @@
package auth
import (
"fmt"
"net/http"
_ "embed"
)
//go:embed templates/login.html
var loginHTML string
func getLogin(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "text/html")
fmt.Fprint(w, loginHTML)
return
}

View file

@ -1,52 +0,0 @@
package auth
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/Bronku/iroon/internal/models"
)
func (a *Authenticator) getSession(r *http.Request) (models.Token, error) {
c, err := r.Cookie("token")
if err != nil {
return models.Token{}, err
}
session, ok := a.sessions[c.Value]
if !ok {
return models.Token{}, errors.New("session not found")
}
if time.Since(session.Expiration) > 0 {
delete(a.sessions, c.Value)
err := a.s.CleanSessions()
if err != nil {
fmt.Println("error cleaning the sessions", err)
}
return models.Token{}, errors.New("session expired")
}
return session, nil
}
func (a *Authenticator) login(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
login := r.PostFormValue("login")
password := r.PostFormValue("password")
if a.verifyCredentials(login, password) != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
cookie, err := a.newSession(login)
if err != nil {
w.Header().Set("content-type", "text/html")
fmt.Fprint(w, "internal server error")
return
}
http.SetCookie(w, &cookie)
http.Redirect(w, r, "/", http.StatusFound)
}

View file

@ -1,12 +0,0 @@
<header>
<h1>Login</h1>
</header>
<main>
<form method="post">
<label>Login</label>
<input type="text" name="login" />
<label>Password</label>
<input type="password" name="password" />
<button type="submit">login</button>
</form>
</main>

View file

@ -1,30 +0,0 @@
package auth
import (
"net/http"
"time"
"github.com/Bronku/iroon/internal/crypto"
"github.com/Bronku/iroon/internal/models"
)
func (a *Authenticator) newSession(user string) (http.Cookie, error) {
key := crypto.GenerateKey()
var session models.Token
var cookie http.Cookie
session.User = user
session.Expiration = time.Now().Add(time.Hour * 24)
a.sessions[key] = session
err := a.s.AddSession(key, user, session.Expiration)
if err != nil {
return cookie, err
}
cookie.Name = "token"
cookie.Value = key
cookie.HttpOnly = true
cookie.SameSite = http.SameSiteStrictMode
cookie.Path = "/"
//cookie.Secure = true
return cookie, nil
}

View file

@ -1,21 +0,0 @@
package crypto
import (
"crypto/rand"
"encoding/base64"
"log"
"golang.org/x/crypto/argon2"
)
func PasswordHash(password, salt string) string {
return string(argon2.Key([]byte(password), []byte(salt), 3, 32*1024, 4, 32))
}
func GenerateKey() string {
key := [32]byte{}
if _, err := rand.Read(key[:]); err != nil {
log.Fatal("can't generate a vaild key", err)
}
return base64.StdEncoding.EncodeToString(key[:])
}

View file

@ -1,13 +0,0 @@
package logging
import (
"fmt"
"net/http"
)
func ErrorPage(err error, status int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(status)
fmt.Fprint(w, err.Error())
}
}

View file

@ -1,16 +0,0 @@
package logging
import (
"fmt"
"net/http"
"time"
)
func Middleware(in http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
fmt.Println(r.Method, r.URL.String())
in.ServeHTTP(w, r)
fmt.Println(r.Method, "finished in", time.Since(start))
})
}

View file

@ -1,8 +0,0 @@
package models
import "time"
type Token struct {
User string
Expiration time.Time
}

View file

@ -1,33 +0,0 @@
package models
import "time"
type Cake struct {
Name string
ID int
Price int
Amount int
Category string
Availability string
}
type Order struct {
ID int
Name string
Surname string
Phone string
Location string
Date time.Time
Accepted time.Time
Status string
Paid int // increments of 0.01
Cakes []Cake
}
func (o *Order) Total() int {
out := o.Paid * (-1)
for _, e := range o.Cakes {
out += e.Price * e.Amount
}
return out
}

View file

@ -1,6 +0,0 @@
package models
type User struct {
Password string
Salt string
}

View file

@ -1,5 +0,0 @@
package server
import "net/http"
type fetcher func(r *http.Request) (any, int, error)

View file

@ -10,7 +10,7 @@ import (
"github.com/Bronku/iroon/internal/models"
)
func (h *Server) orders(r *http.Request) (any, int, error) {
func (h *Server) orders(_ *http.Request) (any, int, error) {
//r.URL.Query().Get()
orders, err := h.s.GetTopOrders(time.Now(), time.Now().Add(time.Hour*24))
data := struct {
@ -35,7 +35,7 @@ func (h *Server) ordersSearch(r *http.Request) (any, int, error) {
return data, http.StatusOK, err
}
func (h *Server) cakes(r *http.Request) (any, int, error) {
func (h *Server) cakes(_ *http.Request) (any, int, error) {
data, err := h.s.GetCakes()
return data, http.StatusOK, err
}

View file

@ -1,9 +0,0 @@
package server
import "net/http"
func (s *Server) redirect(path string, code int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, path, code)
}
}

View file

@ -1,81 +0,0 @@
package server
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/Bronku/iroon/internal/models"
)
func (h *Server) postCake(r *http.Request) (any, int, error) {
err := r.ParseForm()
if err != nil {
return nil, http.StatusInternalServerError, err
}
var n models.Cake
n.ID, err = strconv.Atoi(r.FormValue("id"))
if err != nil {
return nil, http.StatusBadRequest, err
}
n.Name = r.FormValue("name")
n.Price, err = strconv.Atoi(r.FormValue("price"))
if err != nil {
return nil, http.StatusBadRequest, err
}
n.Category = r.FormValue("category")
n.Availability = r.FormValue("availibility")
n.ID, err = h.s.SaveCake(n)
fmt.Println(err)
return n, http.StatusAccepted, err
}
func (h *Server) postOrder(r *http.Request) (any, int, error) {
cakes, err := h.s.GetCakes()
if err != nil {
return nil, http.StatusInternalServerError, err
}
err = r.ParseForm()
fmt.Println(r.PostForm)
if err != nil {
return nil, http.StatusBadRequest, err
}
var n models.Order
n.ID, err = strconv.Atoi(r.FormValue("id"))
if err != nil {
return nil, http.StatusBadRequest, err
}
n.Paid, err = strconv.Atoi(r.FormValue("paid"))
if err != nil {
return nil, http.StatusBadRequest, err
}
n.Date, err = time.Parse("2006-01-02", r.FormValue("date"))
if err != nil {
return nil, http.StatusBadRequest, err
}
n.Cakes = make([]models.Cake, 0)
for _, e := range cakes {
e.Amount, err = strconv.Atoi(r.FormValue(fmt.Sprintf("cake[%d]", e.ID)))
if err != nil {
continue
}
n.Cakes = append(n.Cakes, e)
}
n.Accepted = time.Now()
n.Name = strings.TrimSpace(r.FormValue("name"))
n.Surname = strings.TrimSpace(r.FormValue("surname"))
n.Phone = strings.TrimSpace(r.FormValue("phone"))
n.Location = strings.TrimSpace(r.FormValue("location"))
n.Status = strings.TrimSpace(r.FormValue("status"))
n.ID, err = h.s.SaveOrder(n)
fmt.Println(n)
return n, http.StatusAccepted, err
}

View file

@ -1,63 +0,0 @@
package server
import (
"embed"
"html/template"
"net/http"
"github.com/Bronku/iroon/internal/store"
)
type Server struct {
tmpl map[string]*template.Template
s *store.Store
routes map[string]route
http.Handler
}
type route struct {
function fetcher
template string
templateEntry string
}
func (h *Server) Close() {
}
//go:embed static/*
var static embed.FS
func (h *Server) loadHandler() {
mux := http.NewServeMux()
for i, e := range h.routes {
mux.HandleFunc(i, h.render(e.function, e.template, e.templateEntry))
}
mux.HandleFunc("GET /", h.redirect("/orders", http.StatusSeeOther))
fs := http.FileServerFS(static)
mux.Handle("GET /static/", fs)
h.Handler = mux
}
func New(store *store.Store) *Server {
var server Server
server.routes = map[string]route{
"GET /order/": {server.order, "order", "layout"},
"GET /orders": {server.orders, "orders", "layout"},
"GET /orders/search/": {server.ordersSearch, "orders", "orders-table"},
"GET /cake/": {server.cake, "cake", "layout"},
"GET /cakes": {server.cakes, "cakes", "layout"},
"POST /order/": {server.postOrder, "confirmation", "layout"},
"POST /cake/": {server.postCake, "confirmation", "layout"},
}
server.loadTemplates()
server.s = store
server.loadHandler()
return &server
}

File diff suppressed because one or more lines are too long

View file

@ -1,43 +0,0 @@
package server
import (
"embed"
"html/template"
"log"
"net/http"
"github.com/Bronku/iroon/internal/logging"
)
//go:embed templates/*
var templates embed.FS
func (h *Server) loadTemplates() {
h.tmpl = make(map[string]*template.Template)
for _, page := range h.routes {
tmpl, err := template.ParseFS(templates,
"templates/layout/*.html",
"templates/"+page.template+".html",
)
if err != nil {
log.Fatal(err)
}
h.tmpl[page.template] = tmpl
}
}
func (s *Server) render(fetch fetcher, templateFile string, templateEntry string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, code, err := fetch(r)
if err != nil {
logging.ErrorPage(err, code).ServeHTTP(w, r)
return
}
err = s.tmpl[templateFile].ExecuteTemplate(w, templateEntry, data)
if err != nil {
logging.ErrorPage(err, http.StatusInternalServerError).ServeHTTP(w, r)
return
}
w.Header().Set("content-type", "text/html")
}
}

View file

@ -1,30 +0,0 @@
{{define "title"}}
Cake {{.ID}}
{{end}}
{{define "main"}}
<main>
<form method="post">
<div>
<h2>Info</h2>
<input hidden="true" name="id" value="{{.ID}}">
<label>name</label>
<input type="text" name="name" value="{{.Name}}">
<label>price</label>
<input type="number" name="price" min="0" value="{{.Price}}">
<label>category</label>
<select name="category" id="category" >
<option {{ if eq .Category "common" }}selected{{ end }} value="common">common</option>
<option {{ if eq .Category "christmas" }}selected{{ end }} value="christmas">christmas</option>
<option {{ if eq .Category "easter" }}selected{{ end }} value="easter">easter</option>
<option {{ if eq .Category "donuts" }}selected{{ end }} value="donuts">donuts</option>
</select>
<label>availability</label>
<select name="availibility" id="availibility" >
<option {{ if eq .Availability "available" }}selected{{ end }} value="available">available</option>
<option {{ if eq .Availability "unavailable" }}selected{{ end }} value="unavailable">unavailable</option>
</select>
</div>
<button type="submit">submit</button>
</form>
</main>
{{end}}

View file

@ -1,25 +0,0 @@
{{define "title"}}
Cakes
{{end}}
{{define "main"}}
<main>
<table>
<tr>
<th></th>
<th>Name</th>
<th>Price</th>
<th>Category</th>
<th>Availability</th>
</tr>
{{range .}}
<tr>
<th><a href="/cake/{{.ID}}">edit</a></th>
<th>{{.Name}}</th>
<th>{{.Price}}</th>
<th>{{.Category}}</th>
<th>{{.Availability}}</th>
</tr>
{{end}}
</table>
</main>
{{end}}

View file

@ -1,8 +0,0 @@
{{define "title"}}
Confirmed
{{end}}
{{define "main"}}
<main>
<h2>{{.ID}} confirmed</h2>
</main>
{{end}}

View file

@ -1,17 +0,0 @@
{{define "layout"}}
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>{{template "title" .}}</title>
<script src="/static/htmx2.04.js" defer></script>
</head>
<body>
<header>
<h1>{{template "title" .}}</h1>
</header>
<nav>{{template "nav" .}}</nav>
{{template "main" .}}
</body>
</html>
{{end}}

View file

@ -1,6 +0,0 @@
{{define "nav"}}
<a href="/orders">All orders</a>
<a href="/order">New order</a>
<a href="/cakes">All Cakes</a>
<a href="/cake">New Cake</a>
{{end}}

View file

@ -1,90 +0,0 @@
{{define "title"}}
Order {{.Order.ID}}
{{end}}
{{define "main"}}
<main>
<form method="post">
{{with .Order}}
<div>
<h2>Info</h2>
<input hidden="true" name="id" value="{{.ID}}">
<label>name</label>
<input type="text" name="name" value="{{.Name}}">
<label>surname</label>
<input type="text" name="surname" value="{{.Surname}}">
<label>phone</label>
<input type="tel" name="phone" value="{{.Phone}}">
<label>location</label>
<select name="location">
<option {{ if eq .Location "Kartuzy" }}selected{{ end }} value="Kartuzy">Kartuzy</option>
<option {{ if eq .Location "Somonino" }}selected{{ end }} value="Somonino">Somonino</option>
</select>
<label>date</label>
<input type="date" name="date" value="{{.Date.Format "2006-01-02"}}">
<label>status</label>
<select name="status" id="status" >
<option {{ if eq .Status "accepted" }}selected{{ end }} value="accepted">accepted</option>
<option {{ if eq .Status "done" }}selected{{ end }} value="done">done</option>
</select>
<label>paid</label>
<input type="number" name="paid" min="0" value="{{.Paid}}">
</div>
<div>
<h2>Basket</h2>
<ul id="basket">
</ul>
</div>
{{end}}
{{with .Catalogue}}
<div>
<h2>Catalogue</h2>
<ul>
{{range .}}
<li>
{{.Name}} {{.Price}} <button type=button onClick="addCake({{.ID}},'{{.Name}} {{.Price}}', 1)">add</button>
</li>
{{end}}
</ul>
</div>
{{end}}
<button type="submit">submit</button>
</form>
</main>
<script>
function addCake(id, name, amount){
var cake = document.getElementById("cake["+id+"]")
if (cake != null){
var value = Number(cake.getAttribute("value"))
cake.setAttribute("value", value+1)
return;
}
var label = document.createElement("label")
label.innerText = name
var input = document.createElement("input")
input.setAttribute("id","cake["+id+"]")
input.setAttribute("type", "number")
input.setAttribute("min", "0")
input.setAttribute("name", "cake["+id+"]")
input.setAttribute("value", amount)
var button = document.createElement("button")
button.setAttribute("type", "button")
button.setAttribute("onClick", "removeCake("+id+")")
button.innerText= "delete"
var li = document.createElement("li")
li.appendChild(label)
li.appendChild(input)
li.appendChild(button)
li.setAttribute("id", "li["+id+"]")
var ul = document.getElementById("basket")
ul.appendChild(li)
}
function removeCake(id){
document.getElementById("li["+id+"]").remove()
}
{{range .Order.Cakes}}
addCake({{.ID}}, '{{.Name}} {{.Price}}', {{.Amount}})
{{end}}
</script>
{{end}}

View file

@ -1,51 +0,0 @@
{{define "title"}}
Orders
{{end}}
{{define "main"}}
<main>
<form class="search-container" hx-get="/orders/search" hx-target="#results-table", hx-trigger="keyup delay:500ms, change">
<input type="search" name="q" placeholder="Search orders...">
<label for="from">form</label>
<input type="date" value="{{.Today}}" id="from" name="from">
<label for="from">to</label>
<input type="date" value="{{.Today}}" id="to" name="to">
<div class="htmx-indicator">
Searching...
</div>
<div id="results-table">
{{template "orders-table" .Orders}}
</div>
</form>
</main>
{{end}}
{{define "orders-table"}}
<table id="orders_table">
<tr>
<th></th>
<th>Name</th>
<th>Surname</th>
<th>Phone</th>
<th>Location</th>
<th>Date</th>
<th>Status</th>
<th>Total</th>
</tr>
{{range .}}
<tr>
<th><a href="/order/{{.ID}}">edit</a></th>
<th>{{.Name}}</th>
<th>{{.Surname}}</th>
<th>{{.Phone}}</th>
<th>{{.Location}}</th>
<th>{{.Date.Format "2006-01-02"}}</th>
<th>{{.Status}}</th>
<th>{{.Total}}</th>
</tr>
{{end}}
</table>
{{end}}

View file

@ -1,112 +0,0 @@
package store
import (
"errors"
"fmt"
"github.com/Bronku/iroon/internal/models"
)
func (s *Store) cakeCount() int {
out := 0
rows, err := s.db.Query("select count(*) from cake;")
if err != nil {
return out
}
defer rows.Close()
_ = rows.Next()
_ = rows.Scan(&out)
return out
}
func (s *Store) loadCakes() ([]models.Cake, error) {
out := make([]models.Cake, 0, s.cakeCount())
rows, err := s.db.Query("select id, name, price, category, availability from cake order by id asc;")
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var cake models.Cake
err = rows.Scan(&cake.ID, &cake.Name, &cake.Price, &cake.Category, &cake.Availability)
if err != nil {
continue
}
out = append(out, cake)
}
return out, nil
}
func (s *Store) searchCakes(id int) (int, error) {
for i, e := range s.cakes {
if e.ID != id {
continue
}
return i, nil
}
return 0, errors.New("cake not found")
}
func (s *Store) GetCake(id int) (models.Cake, error) {
if id <= 0 {
return models.Cake{}, errors.New("Invalid cake id")
}
i, err := s.searchCakes(id)
if err != nil {
return models.Cake{}, err
}
return s.cakes[i], err
}
func (s *Store) GetCakes() ([]models.Cake, error) {
result := make([]models.Cake, len(s.cakes))
copy(result, s.cakes)
return result, nil
}
func (s *Store) updateCake(newCake models.Cake) error {
query := "update cake set name = ? , price = ?, category = ?, availability = ? where id = ?"
_, err := s.db.Exec(query, newCake.Name, newCake.Price, newCake.Category, newCake.Availability, newCake.ID)
if err != nil {
return err
}
i, err := s.searchCakes(newCake.ID)
if err != nil {
return err
}
s.cakes[i] = newCake
return nil
}
// #todo: implement
func (s *Store) SyncCakes() error {
return nil
}
func (s *Store) SaveCake(newCake models.Cake) (int, error) {
if newCake.ID != 0 {
return newCake.ID, s.updateCake(newCake)
}
fmt.Println("adding a new cake", newCake)
query := "insert into cake(name, price, category, availability) values (?, ?, ?, ?) returning id;"
rows, err := s.db.Query(query, newCake.Name, newCake.Price, newCake.Category, newCake.Availability)
if err != nil {
return 0, err
}
defer rows.Close()
if !rows.Next() {
return 0, errors.New("query didn't return the cake id")
}
err = rows.Scan(&newCake.ID)
if err != nil {
return 0, err
}
s.cakes = append(s.cakes, newCake)
return newCake.ID, nil
}

View file

@ -1,49 +0,0 @@
package store
import (
"embed"
_ "embed"
"log"
"strconv"
"strings"
)
//go:embed migrations/*.sql
var migrations embed.FS
func (s *Store) loadFile(file string) {
filename := strings.Split(file, ".")
version, err := strconv.Atoi(filename[0])
if err != nil {
log.Fatal("only allowed files in migrations directory are <version>.txt")
}
if version <= s.version() {
return
}
query, _ := migrations.ReadFile("migrations/" + file)
_, err = s.db.Exec(string(query))
if err != nil {
log.Fatal("error executing migration: ", file, err)
}
}
func (s *Store) loadMigrations() {
migration_files, _ := migrations.ReadDir("migrations")
for _, e := range migration_files {
s.loadFile(e.Name())
}
}
func (s *Store) version() int {
out := -1
row, err := s.db.Query("PRAGMA user_version;")
if err != nil {
return out
}
defer row.Close()
row.Next()
_ = row.Scan(&out)
return out
}

View file

@ -1,112 +0,0 @@
CREATE TABLE cake
(
id integer primary key autoincrement,
name text not null unique,
price integer not null,
category text,
availability text
);
CREATE TABLE customer_order
(
id integer primary key autoincrement,
name text,
surname text,
phone text,
location text,
order_date string not null,
delivery_date string not null,
status text,
paid integer
);
CREATE TABLE ordered_cake
(
customer_order integer references customer_order (id) not null,
cake integer references cake (id) not null,
amount integer not null,
primary key (customer_order, cake)
);
CREATE TABLE user
(
login text primary key,
password text,
role text,
salt text
);
CREATE TABLE session
(
token text primary key,
user text,
expiration text
);
create
virtual table order_fts using fts5 (
id,
name,
surname,
phone,
location,
order_date,
delivery_date,
status,
paid,
content = 'customer_order'
);
create trigger order_fts_insert
after insert
on customer_order
begin
insert into order_fts (id,
name,
surname,
phone,
location,
order_date,
delivery_date,
status,
paid)
values (new.id,
new.name,
new.surname,
new.phone,
new.location,
new.order_date,
new.delivery_date,
new.status,
new.paid);
end;
create trigger order_fts_delete
after delete
on customer_order
begin
delete
from order_fts
where id = old.id;
end;
create trigger order_fts_update
after
update
on customer_order
begin
update customer_order
set id = new.id,
name = new.name,
surname = new.surname,
phone = new.phone,
location = new.location,
order_date = new.order_date,
delivery_date = new.delivery_date,
status = new.status,
paid = new.paid
where id = old.id;
end;
pragma
user_version = 1;

View file

@ -1,153 +0,0 @@
package store
import (
"database/sql"
"errors"
"fmt"
"strconv"
"time"
"github.com/Bronku/iroon/internal/models"
)
func (s *Store) parseOrderRow(row *sql.Rows) (models.Order, error) {
var out models.Order
var order_date, delivery_date string
err := row.Scan(&out.ID, &out.Name, &out.Surname, &out.Phone, &out.Location, &order_date, &delivery_date, &out.Status, &out.Paid)
if err != nil {
return out, err
}
out.Accepted, _ = time.Parse("2006-01-02 15:04", order_date)
out.Date, _ = time.Parse("2006-01-02 15:04", delivery_date)
out.Cakes = make([]models.Cake, 0)
rows, err := s.db.Query("select cake, amount from ordered_cake where customer_order = ?;", out.ID)
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var newCake models.Cake
err = rows.Scan(&newCake.ID, &newCake.Amount)
if err != nil {
return out, err
}
cakeData, err := s.GetCake(newCake.ID)
if err != nil {
continue
}
newCake.Name = cakeData.Name
newCake.Price = cakeData.Price
out.Cakes = append(out.Cakes, newCake)
}
return out, nil
}
func (s *Store) getOrdersFromQuery(query string, args ...any) ([]models.Order, error) {
var out []models.Order
rows, err := s.db.Query(query, args...)
if err != nil {
fmt.Println("gofq", err)
return out, err
}
defer rows.Close()
for rows.Next() {
order, err := s.parseOrderRow(rows)
if err != nil {
fmt.Println(err)
continue
}
out = append(out, order)
}
return out, nil
}
func (s *Store) GetOrder(id int) (models.Order, error) {
query := "select * from customer_order where id = ?;"
out, err := s.getOrdersFromQuery(query, id)
if err != nil {
return models.Order{}, err
}
if len(out) < 1 {
return models.Order{}, errors.New("no output order")
}
return out[0], nil
}
func (s *Store) GetFilteredOrder(filter string, from, to time.Time) ([]models.Order, error) {
if filter == "" {
return s.GetTopOrders(from, to)
}
start := from.Format("2006-01-02") + " 00:00"
end := to.Format("2006-01-02") + " 99:99"
if to.IsZero() {
end = "9999-99-99 99:99"
}
query := "select id, name, surname, phone, location, order_date, delivery_date, status, paid from order_fts(?) where delivery_date >= ? and delivery_date <= ? order by rank;"
return s.getOrdersFromQuery(query, filter, start, end)
}
func (s *Store) GetTopOrders(from, to time.Time) ([]models.Order, error) {
start := from.Format("2006-01-02") + " 00:00"
end := to.Format("2006-01-02") + " 99:99"
if to.IsZero() {
end = "9999-99-99 99:99"
}
query := "select * from customer_order where status != 'done' and delivery_date >= ? and delivery_date <= ? ;"
return s.getOrdersFromQuery(query, start, end)
}
func (s *Store) SaveOrder(newOrder models.Order) (int, error) {
query := "insert into customer_order(name, surname, phone, location, order_date, delivery_date, status, paid) values (?, ?, ?, ?, ?, ?, ?, ?) returning id;"
if newOrder.ID != 0 {
query = "update customer_order set name = ?, surname = ?, phone = ?, location = ?, order_date = ?, delivery_date = ?, status = ?, paid = ? where id = "
query += strconv.Itoa(newOrder.ID) + " returning id;"
}
tx, err := s.db.Begin()
if err != nil {
return 0, err
}
accepted := newOrder.Accepted.Format("2006-01-02 15:04")
date := newOrder.Date.Format("2006-01-02 15:04")
row, err := tx.Query(query, newOrder.Name, newOrder.Surname, newOrder.Phone, newOrder.Location, accepted, date, newOrder.Status, newOrder.Paid)
if err != nil {
_ = tx.Rollback()
return 0, err
}
defer row.Close()
if !row.Next() {
_ = tx.Rollback()
return 0, errors.New("The database didn't respond with an id")
}
err = row.Scan(&newOrder.ID)
if err != nil {
_ = tx.Rollback()
return newOrder.ID, err
}
// remove all ordered_cakes associated with this order before inserting
query = "delete from ordered_cake where customer_order = ?;"
_, err = tx.Exec(query, newOrder.ID)
if err != nil {
_ = tx.Rollback()
return newOrder.ID, err
}
// add all ordered_cakes for this order
query = "insert into ordered_cake(customer_order, cake, amount) values (?,?,?);"
for _, e := range newOrder.Cakes {
_, err := tx.Exec(query, newOrder.ID, e.ID, e.Amount)
if err != nil {
_ = tx.Rollback()
return newOrder.ID, err
}
}
err = tx.Commit()
return newOrder.ID, err
}

View file

@ -1,52 +0,0 @@
package store
import (
"time"
"github.com/Bronku/iroon/internal/models"
)
func (s *Store) AddSession(token, userName string, expiration time.Time) error {
query := "insert into session (token, user, expiration) values(?, ?, ?)"
_, err := s.db.Exec(query, token, userName, expiration.Format("2006-01-02 15:04"))
return err
}
func (s *Store) CleanSessions() error {
now := time.Now().Format("2006-01-02 15:04")
query := "delete from session where expiration < ?;"
_, err := s.db.Exec(query, now)
return err
}
func (s *Store) GetSessions() (map[string]models.Token, error) {
out := make(map[string]models.Token)
err := s.CleanSessions()
if err != nil {
return out, err
}
query := "select token, user, expiration from session;"
rows, err := s.db.Query(query)
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var current models.Token
var token string
var expiration string
err = rows.Scan(&token, &current.User, &expiration)
if err != nil {
continue
}
current.Expiration, err = time.Parse("2006-01-02 15:04", expiration)
if err != nil {
continue
}
if token == "" {
continue
}
out[token] = current
}
return out, nil
}

View file

@ -1,43 +0,0 @@
package store
import (
"database/sql"
"log"
"github.com/Bronku/iroon/internal/models"
_ "github.com/knaka/go-sqlite3-fts5"
_ "github.com/mattn/go-sqlite3"
)
type Store struct {
db *sql.DB
cakes []models.Cake
users map[string]models.User
}
func OpenStore(filename string) *Store {
var out Store
var err error
out.db, err = sql.Open("sqlite3", filename)
if err != nil {
log.Fatal("Can't open the database", filename, err)
}
out.loadMigrations()
out.cakes, err = out.loadCakes()
if err != nil {
log.Fatal(err)
}
out.users, err = out.loadUsers()
if err != nil {
log.Fatal(err)
}
return &out
}
func (s *Store) Close() {
if s.db != nil {
s.db.Close()
}
}

View file

@ -1,293 +0,0 @@
package store
import (
"testing"
"time"
"github.com/Bronku/iroon/internal/models"
)
func TestStore(t *testing.T) {
s := OpenStore("file:memdb1?mode=memory&cache=shared")
defer s.Close()
// ensure db is empty
cakes, err := s.GetCakes()
if err != nil {
t.Errorf("Error getting cakes: %v", err)
}
if len(cakes) != 0 {
t.Errorf("Expected empty cakes list, got: %v", cakes)
}
orders, err := s.GetTopOrders(time.Now(), time.Now().Add(time.Hour*24))
if err != nil {
t.Errorf("Error getting orders: %v", err)
}
if len(orders) != 0 {
t.Errorf("Expected empty orders list, got: %v", orders)
}
// create new cake
newCake := models.Cake{Name: "Chocolate Cake", Price: 2500}
newCake.ID, err = s.SaveCake(newCake)
if err != nil {
t.Fatalf("Failed to save new cake: %v", err)
}
if newCake.ID <= 0 {
t.Errorf("Expected positive cake ID, got: %d", newCake.ID)
}
// create another cake
anotherCake := models.Cake{Name: "Another Cake", Price: 3000}
anotherCake.ID, err = s.SaveCake(anotherCake)
if err != nil {
t.Fatalf("Failed to save new cake: %v", err)
}
if anotherCake.ID <= 0 {
t.Errorf("Expected positive cake ID, got: %d", anotherCake.ID)
}
// Update Existing Cake
newCake.Name = "Updated Cake"
newCake.Price = 100
newID, err := s.SaveCake(newCake)
if newID != newCake.ID {
t.Error("wrong id")
}
if err != nil {
t.Error("error updating cake", err)
}
// Update Non-existing Cake
var updatedCake models.Cake
updatedCake.ID = 10
_, err = s.SaveCake(updatedCake)
if err == nil {
t.Error("did not return an error when attempted to update non existant cake")
}
// get cakes
cakes, err = s.GetCakes()
if err != nil {
t.Fatalf("Failed to get all cakes after creation: %v", err)
}
if len(cakes) != 2 {
t.Error("Expected two cakes returned, got ", len(cakes))
}
for i, c := range cakes {
if c.ID == newCake.ID {
newCake.Amount = i
}
if c.ID == anotherCake.ID {
anotherCake.Amount = i
}
}
if newCake.Amount == -1 {
t.Error("not found newCake")
}
if cakes[newCake.Amount].ID != newCake.ID || cakes[newCake.Amount].Name != newCake.Name || cakes[newCake.Amount].Price != newCake.Price {
t.Errorf("Want %v\nGot %v", newCake, cakes[newCake.Amount])
}
if anotherCake.Amount == -1 {
t.Error("not found anotherCake")
}
if cakes[anotherCake.Amount].ID != anotherCake.ID || cakes[anotherCake.Amount].Name != anotherCake.Name || cakes[anotherCake.Amount].Price != anotherCake.Price {
t.Errorf("Want %v\nGot %v", anotherCake, cakes)
}
selectedCake, err := s.GetCake(newCake.ID)
if err != nil {
t.Error("error getting a cake", err)
}
if selectedCake.ID != newCake.ID || selectedCake.Name != newCake.Name || selectedCake.Price != newCake.Price {
t.Errorf("Want %v\nGot %v", newCake, selectedCake)
}
selectedCake, err = s.GetCake(-1)
if err == nil {
t.Error("no error getting a non-existant cake")
}
// create new order
now := time.Now()
newOrder := models.Order{
Name: "John",
Surname: "Doe",
Phone: "123-456-7890",
Location: "Some Location",
Accepted: now,
Date: now.Add(time.Hour * 24),
Status: "Pending",
Paid: 1000,
Cakes: []models.Cake{{ID: newCake.ID, Amount: 2}, {ID: anotherCake.ID, Amount: 10}},
}
newOrder.ID, err = s.SaveOrder(newOrder)
if err != nil {
t.Fatalf("Failed to save new order: %v", err)
}
if newOrder.ID <= 0 {
t.Errorf("Expected positive order ID, got: %d", newOrder.ID)
}
// create another order
anotherOrder := models.Order{
Name: "Jane",
Surname: "Doe",
Phone: "123-456-7890",
Location: "New Location",
Accepted: now,
Date: now.Add(time.Hour * 192),
Status: "Accepted",
Paid: 1500,
Cakes: []models.Cake{{ID: newCake.ID, Amount: 100}},
}
anotherOrder.ID, err = s.SaveOrder(anotherOrder)
if err != nil {
t.Fatalf("Failed to save new order: %v", err)
}
if anotherOrder.ID <= 0 {
t.Errorf("Expected positive order ID, got: %d", anotherOrder.ID)
}
// update existing order
newOrder.Name = "James"
newOrder.Status = "Done"
newOrder.Cakes = newOrder.Cakes[1:]
newID, err = s.SaveOrder(newOrder)
if newID != newOrder.ID {
t.Error("wrong id")
}
if err != nil {
t.Error("error updating order", err)
}
// update non existing order
var updatedOrder models.Order
updatedOrder.ID = 10
_, err = s.SaveOrder(updatedOrder)
if err == nil {
t.Error("did not return an error when attempted to update non existant cake")
}
// get orders
orders, err = s.GetTopOrders(time.Time{}, time.Time{})
if err != nil {
t.Fatalf("Failed to get all orders after creation: %v", err)
}
if len(orders) != 2 {
t.Error("Expected two orders returned, got ", len(orders))
}
newOrderPos := -1
anotherOrderPos := -1
for i, o := range orders {
if o.ID == newOrder.ID {
newOrderPos = i
}
if o.ID == anotherOrder.ID {
anotherOrderPos = i
}
}
if newOrderPos == -1 {
t.Error("not found newOrder")
}
if newOrder.Name != orders[newOrderPos].Name ||
newOrder.Surname != orders[newOrderPos].Surname ||
newOrder.Phone != orders[newOrderPos].Phone ||
newOrder.Status != orders[newOrderPos].Status ||
newOrder.Location != orders[newOrderPos].Location ||
newOrder.Accepted.Format("2006-01-02 15:04") != orders[newOrderPos].Accepted.Format("2006-01-02 15:04") ||
newOrder.Date.Format("2006-01-02 15:04") != orders[newOrderPos].Date.Format("2006-01-02 15:04") {
t.Errorf("Want %v\nGot %v", newOrder, orders[newOrderPos])
}
if !areCakeSlicesEqual(newOrder.Cakes, orders[newOrderPos].Cakes) {
t.Errorf("Want %v\nGot %v", newOrder.Cakes, orders[newOrderPos].Cakes)
}
if anotherOrderPos == -1 {
t.Error("not found anotherOrder")
}
if anotherOrder.Name != orders[anotherOrderPos].Name ||
anotherOrder.Surname != orders[anotherOrderPos].Surname ||
anotherOrder.Phone != orders[anotherOrderPos].Phone ||
anotherOrder.Status != orders[anotherOrderPos].Status ||
anotherOrder.Location != orders[anotherOrderPos].Location ||
anotherOrder.Accepted.Format("2006-01-02 15:04") != orders[anotherOrderPos].Accepted.Format("2006-01-02 15:04") ||
anotherOrder.Date.Format("2006-01-02 15:04") != orders[anotherOrderPos].Date.Format("2006-01-02 15:04") {
t.Errorf("Want %v\nGot %v", anotherOrder, orders[anotherOrderPos])
}
if !areCakeSlicesEqual(anotherOrder.Cakes, orders[anotherOrderPos].Cakes) {
t.Errorf("Want %v\nGot %v", anotherOrder.Cakes, orders[anotherOrderPos].Cakes)
}
selectedOrder, err := s.GetOrder(newOrder.ID)
if err != nil {
t.Error("error getting an order by id ", err)
}
if newOrder.Name != selectedOrder.Name ||
newOrder.Surname != selectedOrder.Surname ||
newOrder.Phone != selectedOrder.Phone ||
newOrder.Status != selectedOrder.Status ||
newOrder.Location != selectedOrder.Location ||
newOrder.Accepted.Format("2006-01-02 15:04") != selectedOrder.Accepted.Format("2006-01-02 15:04") ||
newOrder.Date.Format("2006-01-02 15:04") != selectedOrder.Date.Format("2006-01-02 15:04") {
t.Errorf("Want %v\nGot %v", newOrder, selectedOrder)
}
if !areCakeSlicesEqual(newOrder.Cakes, selectedOrder.Cakes) {
t.Errorf("Want %v\nGot %v", newOrder.Cakes, selectedOrder.Cakes)
}
_, err = s.GetOrder(-1)
if err == nil {
t.Error("no error getting invalid order")
}
}
func TestSliceComparison(t *testing.T) {
a := []models.Cake{
{Name: "ok", Price: 100, ID: 12, Amount: 10},
{Name: "ok", Price: 11, ID: 13, Amount: 1},
{Name: "ok", Price: 100, ID: 10, Amount: 11},
}
b := []models.Cake{
{Name: "okas", Price: 110, ID: 12, Amount: 10},
{Name: "oksa", Price: 11, ID: 13, Amount: 1},
{Name: "oasdk", Price: 101, ID: 10, Amount: 11},
}
if !areCakeSlicesEqual(a, b) {
t.Error("aren't equal")
}
}
func areCakesEqual(a, b models.Cake) bool {
return a.ID == b.ID && a.Amount == b.Amount
}
func areCakeSlicesEqual(a, b []models.Cake) bool {
if len(a) != len(b) {
return false
}
matched := make([]bool, len(a))
mainLoop:
for _, e1 := range a {
for i, e2 := range b {
if matched[i] {
continue
}
if areCakesEqual(e1, e2) {
matched[i] = true
continue mainLoop
}
}
}
for _, e := range matched {
if e == false {
return false
}
}
return true
}

View file

@ -1,48 +0,0 @@
package store
import (
"errors"
"github.com/Bronku/iroon/internal/crypto"
"github.com/Bronku/iroon/internal/models"
)
func (s *Store) AddUser(login, password string) error {
_, exists := s.GetUser(login)
if exists {
return errors.New("the user already exists")
}
query := "insert into user (login, password, salt) values(?, ?, ?)"
salt := crypto.GenerateKey()
hash := crypto.PasswordHash(password, salt)
_, err := s.db.Exec(query, login, hash, salt)
if err == nil {
s.users[login] = models.User{Password: hash, Salt: salt}
}
return err
}
func (s *Store) loadUsers() (map[string]models.User, error) {
out := make(map[string]models.User)
query := "select login, password, salt from user;"
rows, err := s.db.Query(query)
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var current models.User
var login string
err = rows.Scan(&login, &current.Password, &current.Salt)
if err != nil {
continue
}
out[login] = current
}
return out, nil
}
func (s *Store) GetUser(login string) (models.User, bool) {
value, ok := s.users[login]
return value, ok
}

View file

@ -1,7 +0,0 @@
package main
import "github.com/Bronku/iroon/cmd/iroon"
func main() {
iroon.Run()
}