feat: initial project commit

This commit is contained in:
Patryk Hegenberg 2025-11-04 22:20:09 +01:00
commit 2c4fc7869a
10 changed files with 1632 additions and 0 deletions

178
backend/database.go Normal file
View file

@ -0,0 +1,178 @@
package main
import (
"database/sql"
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
_ "modernc.org/sqlite"
)
func InitDB(filepath string) *sql.DB {
db, err := sql.Open("sqlite", filepath)
if err != nil {
log.Fatal(err)
}
if err = db.Ping(); err != nil {
log.Fatal(err)
}
createTables(db)
return db
}
func createTables(db *sql.DB) {
queries := []string{
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
is_admin BOOLEAN NOT NULL DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS schedules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
day_of_week INTEGER NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
type TEXT NOT NULL,
title TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS time_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
schedule_id INTEGER NOT NULL,
date TEXT NOT NULL,
type TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (schedule_id) REFERENCES schedules(id)
)`,
}
for _, query := range queries {
if _, err := db.Exec(query); err != nil {
log.Fatal(err)
}
}
// Create default admin user (password: admin123)
// Hash generated with bcrypt
hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
fmt.Println(string(hash))
_, err := db.Exec(`
INSERT OR IGNORE INTO users (id, username, password, is_admin)
VALUES (?, ?, ?, ?)`,
1, "admin", string(hash), true,
)
if err != nil {
log.Fatal(err)
}
}
func GetUserByUsername(db *sql.DB, username string) (*User, error) {
user := &User{}
err := db.QueryRow("SELECT id, username, password, is_admin FROM users WHERE username = ?", username).
Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin)
if err != nil {
return nil, err
}
return user, nil
}
func CreateUser(db *sql.DB, username, hashedPassword string, isAdmin bool) error {
_, err := db.Exec("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
username, hashedPassword, isAdmin)
return err
}
func GetAllUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, username, is_admin FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Username, &u.IsAdmin); err != nil {
continue
}
users = append(users, u)
}
return users, nil
}
func CreateSchedule(db *sql.DB, schedule *Schedule) error {
_, err := db.Exec("INSERT INTO schedules (day_of_week, start_time, end_time, type, title) VALUES (?, ?, ?, ?, ?)",
schedule.DayOfWeek, schedule.StartTime, schedule.EndTime, schedule.Type, schedule.Title)
return err
}
func GetAllSchedules(db *sql.DB) ([]Schedule, error) {
rows, err := db.Query("SELECT id, day_of_week, start_time, end_time, type, title FROM schedules ORDER BY day_of_week, start_time")
if err != nil {
return nil, err
}
defer rows.Close()
var schedules []Schedule
for rows.Next() {
var s Schedule
if err := rows.Scan(&s.ID, &s.DayOfWeek, &s.StartTime, &s.EndTime, &s.Type, &s.Title); err != nil {
continue
}
schedules = append(schedules, s)
}
return schedules, nil
}
func DeleteSchedule(db *sql.DB, id int) error {
_, err := db.Exec("DELETE FROM schedules WHERE id = ?", id)
return err
}
func CreateTimeEntry(db *sql.DB, entry *TimeEntry) error {
_, err := db.Exec("INSERT INTO time_entries (user_id, schedule_id, date, type) VALUES (?, ?, ?, ?)",
entry.UserID, entry.ScheduleID, entry.Date, entry.Type)
return err
}
func GetTimeEntriesByUser(db *sql.DB, userID int) ([]TimeEntry, error) {
rows, err := db.Query("SELECT id, user_id, schedule_id, date, type, created_at FROM time_entries WHERE user_id = ? ORDER BY date DESC, created_at DESC",
userID)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []TimeEntry
for rows.Next() {
var e TimeEntry
if err := rows.Scan(&e.ID, &e.UserID, &e.ScheduleID, &e.Date, &e.Type, &e.CreatedAt); err != nil {
continue
}
entries = append(entries, e)
}
return entries, nil
}
func GetAllTimeEntries(db *sql.DB) ([]TimeEntry, error) {
rows, err := db.Query("SELECT id, user_id, schedule_id, date, type, created_at FROM time_entries ORDER BY date DESC, created_at DESC")
if err != nil {
return nil, err
}
defer rows.Close()
var entries []TimeEntry
for rows.Next() {
var e TimeEntry
if err := rows.Scan(&e.ID, &e.UserID, &e.ScheduleID, &e.Date, &e.Type, &e.CreatedAt); err != nil {
continue
}
entries = append(entries, e)
}
return entries, nil
}

21
backend/go.mod Normal file
View file

@ -0,0 +1,21 @@
module school-timetracker
go 1.25.3
require (
golang.org/x/crypto v0.43.0
modernc.org/sqlite v1.40.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/sys v0.37.0 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

51
backend/go.sum Normal file
View file

@ -0,0 +1,51 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ=
modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

172
backend/handlers.go Normal file
View file

@ -0,0 +1,172 @@
package main
import (
"database/sql"
"encoding/json"
"net/http"
"strconv"
"golang.org/x/crypto/bcrypt"
)
type App struct {
DB *sql.DB
}
func (app *App) LoginHandler(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
user, err := GetUserByUsername(app.DB, req.Username)
if err != nil {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
token, err := createToken(user.ID, user.Username, user.IsAdmin)
if err != nil {
http.Error(w, "Error creating token", http.StatusInternalServerError)
return
}
response := LoginResponse{
Token: token,
Username: user.Username,
IsAdmin: user.IsAdmin,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func (app *App) GetSchedulesHandler(w http.ResponseWriter, r *http.Request) {
schedules, err := GetAllSchedules(app.DB)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(schedules)
}
func (app *App) CreateScheduleHandler(w http.ResponseWriter, r *http.Request) {
var schedule Schedule
if err := json.NewDecoder(r.Body).Decode(&schedule); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := CreateSchedule(app.DB, &schedule); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
func (app *App) DeleteScheduleHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
if err := DeleteSchedule(app.DB, id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (app *App) CreateUserHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
IsAdmin bool `json:"is_admin"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, "Error hashing password", http.StatusInternalServerError)
return
}
if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
func (app *App) GetUsersHandler(w http.ResponseWriter, r *http.Request) {
users, err := GetAllUsers(app.DB)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func (app *App) CreateTimeEntryHandler(w http.ResponseWriter, r *http.Request) {
userIDStr := r.Header.Get("X-User-ID")
userID, _ := strconv.Atoi(userIDStr)
var entry TimeEntry
if err := json.NewDecoder(r.Body).Decode(&entry); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
entry.UserID = userID
if err := CreateTimeEntry(app.DB, &entry); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
func (app *App) GetMyTimeEntriesHandler(w http.ResponseWriter, r *http.Request) {
userIDStr := r.Header.Get("X-User-ID")
userID, _ := strconv.Atoi(userIDStr)
entries, err := GetTimeEntriesByUser(app.DB, userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(entries)
}
func (app *App) GetAllTimeEntriesHandler(w http.ResponseWriter, r *http.Request) {
entries, err := GetAllTimeEntries(app.DB)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(entries)
}

46
backend/main.go Normal file
View file

@ -0,0 +1,46 @@
package main
import (
"log"
"net/http"
"os"
)
func main() {
dbPath := os.Getenv("DB_PATH")
if dbPath == "" {
dbPath = "./timetracking.db"
}
db := InitDB(dbPath)
defer db.Close()
app := &App{DB: db}
// Public routes
http.HandleFunc("/api/login", CORS(app.LoginHandler))
// Protected routes
http.HandleFunc("/api/schedules", CORS(AuthMiddleware(app.GetSchedulesHandler)))
http.HandleFunc("/api/time-entries", CORS(AuthMiddleware(app.CreateTimeEntryHandler)))
http.HandleFunc("/api/my-time-entries", CORS(AuthMiddleware(app.GetMyTimeEntriesHandler)))
// Admin routes
http.HandleFunc("/api/admin/schedules", CORS(AdminMiddleware(app.CreateScheduleHandler)))
http.HandleFunc("/api/admin/schedules/delete", CORS(AdminMiddleware(app.DeleteScheduleHandler)))
http.HandleFunc("/api/admin/users", CORS(AdminMiddleware(app.CreateUserHandler)))
http.HandleFunc("/api/admin/users/list", CORS(AdminMiddleware(app.GetUsersHandler)))
http.HandleFunc("/api/admin/time-entries", CORS(AdminMiddleware(app.GetAllTimeEntriesHandler)))
// Serve frontend
fs := http.FileServer(http.Dir("./static"))
http.Handle("/", fs)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Server starting on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}

125
backend/middleware.go Normal file
View file

@ -0,0 +1,125 @@
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
var jwtSecret = []byte("your-secret-key-change-in-production")
type Claims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
IsAdmin bool `json:"is_admin"`
Exp int64 `json:"exp"`
}
func createToken(userID int, username string, isAdmin bool) (string, error) {
claims := Claims{
UserID: userID,
Username: username,
IsAdmin: isAdmin,
Exp: time.Now().Add(24 * time.Hour).Unix(),
}
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`))
payload, _ := json.Marshal(claims)
payloadEncoded := base64.RawURLEncoding.EncodeToString(payload)
message := header + "." + payloadEncoded
h := hmac.New(sha256.New, jwtSecret)
h.Write([]byte(message))
signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
return message + "." + signature, nil
}
func verifyToken(tokenString string) (*Claims, error) {
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token format")
}
message := parts[0] + "." + parts[1]
h := hmac.New(sha256.New, jwtSecret)
h.Write([]byte(message))
expectedSignature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
if parts[2] != expectedSignature {
return nil, fmt.Errorf("invalid signature")
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, err
}
var claims Claims
if err := json.Unmarshal(payload, &claims); err != nil {
return nil, err
}
if time.Now().Unix() > claims.Exp {
return nil, fmt.Errorf("token expired")
}
return &claims, nil
}
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := verifyToken(tokenString)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
r.Header.Set("X-User-ID", fmt.Sprintf("%d", claims.UserID))
r.Header.Set("X-Username", claims.Username)
r.Header.Set("X-Is-Admin", fmt.Sprintf("%t", claims.IsAdmin))
next(w, r)
}
}
func AdminMiddleware(next http.HandlerFunc) http.HandlerFunc {
return AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
isAdmin := r.Header.Get("X-Is-Admin") == "true"
if !isAdmin {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next(w, r)
})
}
func CORS(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next(w, r)
}
}

41
backend/models.go Normal file
View file

@ -0,0 +1,41 @@
package main
import (
"time"
)
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"-"`
IsAdmin bool `json:"is_admin"`
}
type Schedule struct {
ID int `json:"id"`
DayOfWeek int `json:"day_of_week"` // 0=Monday, 4=Friday
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Type string `json:"type"` // "lesson" or "break"
Title string `json:"title"`
}
type TimeEntry struct {
ID int `json:"id"`
UserID int `json:"user_id"`
ScheduleID int `json:"schedule_id"`
Date string `json:"date"`
Type string `json:"type"` // "lesson" or "break"
CreatedAt time.Time `json:"created_at"`
}
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse struct {
Token string `json:"token"`
Username string `json:"username"`
IsAdmin bool `json:"is_admin"`
}

23
backend/static/index.html Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Schulzeit Erfassung</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<style>
html, body {
height: 100%;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="/elm.js"></script>
<script>
var app = Elm.Main.init({
node: document.getElementById('app')
});
</script>
</body>
</html>