first commit
This commit is contained in:
parent
76ae38e837
commit
ca95749ec9
22
.github/workflows/Deploy-docker.yml
vendored
Normal file
22
.github/workflows/Deploy-docker.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: build and deploy kleinTodo
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build the Docker image
|
||||
run: docker compose build
|
||||
- name: Docker login
|
||||
run: docker login gitea.kleinsense.nl -p ${{secrets.docker_password}} -u ${{secrets.docker_username}}
|
||||
- name: Docker push
|
||||
run: docker push gitea.kleinsense.nl/dariusklein/kleinTodo:latest
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
# Use an official Golang runtime as a parent image
|
||||
FROM golang:latest as build
|
||||
|
||||
# Set the working directory to /app
|
||||
WORKDIR /app
|
||||
# Copy the current directory contents into the container at /app
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
# Download and install any required dependencies
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
# Build the Go app
|
||||
RUN go build .
|
||||
|
||||
FROM gcr.io/distroless/base-debian12
|
||||
|
||||
COPY --from=build /app/kleinTodo .
|
||||
|
||||
# Define the command to run the app when the container starts
|
||||
CMD ["./kleinTodo"]
|
||||
159
common/bolt.go
Normal file
159
common/bolt.go
Normal file
@ -0,0 +1,159 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type DataStore interface {
|
||||
SaveValueToBucket(bucket, key, value string) error
|
||||
CreateBucket(bucket string) error
|
||||
GetFromBucketByKey(bucket, key string) (string, error)
|
||||
GetAllBuckets() ([]string, error)
|
||||
GetAllFromBucket(bucket string) (map[string]string, error)
|
||||
GetAllKeysFromBucket(bucket string) ([]string, error)
|
||||
EmptyBucket(bucket string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type BoltStore struct {
|
||||
DB *bolt.DB
|
||||
}
|
||||
|
||||
func NewBoltStore(path string) (*BoltStore, error) {
|
||||
db, err := bolt.Open(path, 0600, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open db: %w", err)
|
||||
}
|
||||
return &BoltStore{DB: db}, nil
|
||||
}
|
||||
|
||||
func (s *BoltStore) Close() error {
|
||||
return s.DB.Close()
|
||||
}
|
||||
|
||||
var (
|
||||
dataStore *BoltStore
|
||||
once sync.Once
|
||||
err error
|
||||
)
|
||||
|
||||
func GetTodoDataStore() (*BoltStore, error) {
|
||||
once.Do(func() {
|
||||
// We assign to the outer 'dataStore' and 'err' variables.
|
||||
dataStore, err = NewBoltStore("todo.db")
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dataStore, nil
|
||||
}
|
||||
|
||||
// SaveValueToBucket Save data to bucket
|
||||
func (s *BoltStore) SaveValueToBucket(bucket, key, value string) error {
|
||||
return s.DB.Update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(key), []byte(value))
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveValueFromBucket remove value
|
||||
func (s *BoltStore) RemoveValueFromBucket(bucket, key string) error {
|
||||
return s.DB.Update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Delete([]byte(key))
|
||||
})
|
||||
}
|
||||
|
||||
// CreateBucket Create bucket if not exists
|
||||
func (s *BoltStore) CreateBucket(bucket string) error {
|
||||
return s.DB.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(bucket))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// GetFromBucketByKey Returns value from bucket by key or empty string
|
||||
func (s *BoltStore) GetFromBucketByKey(bucket, key string) string {
|
||||
var value string
|
||||
s.DB.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket))
|
||||
if b == nil {
|
||||
return nil // Bucket doesn't exist, nothing to do
|
||||
}
|
||||
// Get returns nil if the key doesn't exist
|
||||
valBytes := b.Get([]byte(key))
|
||||
if valBytes != nil {
|
||||
value = string(valBytes)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
// GetAllBuckets Returns map of all buckets
|
||||
func (s *BoltStore) GetAllBuckets() []string {
|
||||
var a = make([]string, 0)
|
||||
s.DB.View(func(tx *bolt.Tx) error {
|
||||
return tx.ForEach(func(name []byte, b *bolt.Bucket) error {
|
||||
a = append(a, string(name))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return a
|
||||
}
|
||||
|
||||
// GetAllFromBucket Returns map of all from bucket
|
||||
func (s *BoltStore) GetAllFromBucket(bucket string) map[string]string {
|
||||
results := make(map[string]string)
|
||||
s.DB.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
// Using a cursor is the recommended way to iterate
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
results[string(k)] = string(v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
// GetAllKeysFromBucket Returns keys from bucket
|
||||
func (s *BoltStore) GetAllKeysFromBucket(bucket string) []string {
|
||||
var keys []string
|
||||
s.DB.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(bucket))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.ForEach(func(k, v []byte) error {
|
||||
keys = append(keys, string(k))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
// EmptyBucket Returns value from bucket by key or empty string
|
||||
func (s *BoltStore) EmptyBucket(bucket string) error {
|
||||
return s.DB.Update(func(tx *bolt.Tx) error {
|
||||
if err := tx.DeleteBucket([]byte(bucket)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.CreateBucket([]byte(bucket)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
6
common/const.go
Normal file
6
common/const.go
Normal file
@ -0,0 +1,6 @@
|
||||
package common
|
||||
|
||||
const UserBucket = "users"
|
||||
const TodoBucket = "todo"
|
||||
|
||||
const AuthHeader = "Authorization"
|
||||
10
common/filter.go
Normal file
10
common/filter.go
Normal file
@ -0,0 +1,10 @@
|
||||
package common
|
||||
|
||||
func Filter[T any](ss []T, test func(T) bool) (ret []T) {
|
||||
for _, s := range ss {
|
||||
if test(s) {
|
||||
ret = append(ret, s)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
24
common/hash.go
Normal file
24
common/hash.go
Normal file
@ -0,0 +1,24 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func (credentials *Credentials) ComparePasswords(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(password), []byte(credentials.Password))
|
||||
if err != nil {
|
||||
slog.Error(err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (credentials *Credentials) HashedPassword() (string, error) {
|
||||
return HashPassword(credentials.Password)
|
||||
}
|
||||
30
common/jwt/create.go
Normal file
30
common/jwt/create.go
Normal file
@ -0,0 +1,30 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CreateUserJWT(name string) string {
|
||||
|
||||
//create claims for jwt
|
||||
claims := jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "todo.dariusklein.nl",
|
||||
Subject: name,
|
||||
}
|
||||
return SignJWT(claims)
|
||||
}
|
||||
|
||||
func SignJWT(claims jwt.Claims) string {
|
||||
//Build jwt with claims
|
||||
t := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||
//get jwt secret from environment
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
//sign jwt token with secret
|
||||
token, _ := t.SignedString([]byte(secret))
|
||||
return token
|
||||
}
|
||||
33
common/jwt/verify.go
Normal file
33
common/jwt/verify.go
Normal file
@ -0,0 +1,33 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
_ "context"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetVerifiedUser(r *http.Request) (string, error) {
|
||||
verifyJWT, err := VerifyJWT(strings.TrimPrefix(r.Header.Get(common.AuthHeader), "Bearer \t"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return verifyJWT, nil
|
||||
}
|
||||
|
||||
// VerifyJWT verify JWT token and returns user object
|
||||
func VerifyJWT(authToken string) (string, error) {
|
||||
//get jwt secret from environment
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
//parse jwt token
|
||||
token, err := jwt.ParseWithClaims(authToken, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
subject := token.Claims.(*jwt.RegisteredClaims).Subject
|
||||
return subject, err
|
||||
}
|
||||
25
common/scheduler.go
Normal file
25
common/scheduler.go
Normal file
@ -0,0 +1,25 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func Schedule(seconds time.Duration, task func()) (stop func()) {
|
||||
ticker := time.NewTicker(seconds)
|
||||
quit := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
task()
|
||||
case <-quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
close(quit)
|
||||
}
|
||||
}
|
||||
7
common/time.go
Normal file
7
common/time.go
Normal file
@ -0,0 +1,7 @@
|
||||
package common
|
||||
|
||||
import "time"
|
||||
|
||||
func CompareTime(t1, t2 time.Time) time.Duration {
|
||||
return t2.Sub(t1)
|
||||
}
|
||||
40
common/todo.go
Normal file
40
common/todo.go
Normal file
@ -0,0 +1,40 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (todo Todo) Store(store *BoltStore, user string) error {
|
||||
if todo.Owner != user {
|
||||
return fmt.Errorf("unauthorized user")
|
||||
}
|
||||
todoJson, err := json.Marshal(todo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return store.SaveValueToBucket(user, todo.Name, string(todoJson))
|
||||
}
|
||||
|
||||
func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error {
|
||||
todo := Todo{
|
||||
Name: todoRequest.Name,
|
||||
Description: todoRequest.Description,
|
||||
Status: todoRequest.Status,
|
||||
Owner: user,
|
||||
}
|
||||
todoJson, err := json.Marshal(todo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return store.SaveValueToBucket(user, todo.Name, string(todoJson))
|
||||
}
|
||||
|
||||
func (todos TodoList) FindByName(name string) (Todo, bool) {
|
||||
for _, todo := range todos.Todos {
|
||||
if todo.Name == name {
|
||||
return todo, true
|
||||
}
|
||||
}
|
||||
return Todo{}, false
|
||||
}
|
||||
32
common/types.go
Normal file
32
common/types.go
Normal file
@ -0,0 +1,32 @@
|
||||
package common
|
||||
|
||||
type Credentials struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type Todo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Owner string `json:"owner"`
|
||||
}
|
||||
|
||||
type StoreTodoRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type TodoList struct {
|
||||
Todos []Todo `json:"todos"`
|
||||
}
|
||||
|
||||
type MisMatchingTodo struct {
|
||||
ServerTodo Todo `json:"server_todo"`
|
||||
LocalTodo Todo `json:"local_todo"`
|
||||
}
|
||||
type SyncResponse struct {
|
||||
SyncedTodos []Todo `json:"synced_todos"`
|
||||
MisMatchingTodos []MisMatchingTodo `json:"mismatching_todos"`
|
||||
}
|
||||
11
go.mod
Normal file
11
go.mod
Normal file
@ -0,0 +1,11 @@
|
||||
module gitea.kleinsense.nl/DariusKlein/kleinTodo
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||
go.etcd.io/bbolt v1.4.2
|
||||
golang.org/x/crypto v0.40.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.34.0 // indirect
|
||||
62
handler/errorHandlers.go
Normal file
62
handler/errorHandlers.go
Normal file
@ -0,0 +1,62 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func InternalServerErrorHandler(w http.ResponseWriter, err error) {
|
||||
setError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
func NotFoundHandler(w http.ResponseWriter) {
|
||||
setError(w, http.StatusNotFound, "404 Not Found")
|
||||
}
|
||||
|
||||
func BadRequestHandler(w http.ResponseWriter) {
|
||||
setError(w, http.StatusBadRequest, "404 Not Found")
|
||||
}
|
||||
|
||||
func UnprocessableEntityHandler(w http.ResponseWriter, err error) {
|
||||
setError(w, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
|
||||
func UnauthorizedHandler(w http.ResponseWriter) {
|
||||
log.Println("unauthorized")
|
||||
setError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
func NotImplementedHandler(w http.ResponseWriter) {
|
||||
setError(w, http.StatusNotImplemented, "WORK IN PROGRESS")
|
||||
}
|
||||
|
||||
func setError(w http.ResponseWriter, httpStatus int, errorMessage string) {
|
||||
w.WriteHeader(httpStatus)
|
||||
if _, err := w.Write([]byte(errorMessage)); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func handleError(w http.ResponseWriter, status int, err error) bool {
|
||||
if err != nil {
|
||||
switch status {
|
||||
case http.StatusInternalServerError:
|
||||
InternalServerErrorHandler(w, err)
|
||||
case http.StatusNotFound:
|
||||
NotFoundHandler(w)
|
||||
case http.StatusBadRequest:
|
||||
BadRequestHandler(w)
|
||||
case http.StatusUnauthorized:
|
||||
UnauthorizedHandler(w)
|
||||
case http.StatusNotImplemented:
|
||||
NotImplementedHandler(w)
|
||||
case http.StatusUnprocessableEntity:
|
||||
UnprocessableEntityHandler(w, err)
|
||||
default:
|
||||
InternalServerErrorHandler(w, err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
34
handler/loginHandler.go
Normal file
34
handler/loginHandler.go
Normal file
@ -0,0 +1,34 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common/jwt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var user common.Credentials
|
||||
// Decode input
|
||||
err := json.NewDecoder(r.Body).Decode(&user)
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
// Get data store
|
||||
db, err := common.GetTodoDataStore()
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
password := db.GetFromBucketByKey(common.UserBucket, user.Username)
|
||||
if user.ComparePasswords(password) {
|
||||
w.Header().Set(common.AuthHeader, jwt.CreateUserJWT(user.Username))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
handleError(w, http.StatusUnauthorized, errors.New("username or password is incorrect"))
|
||||
return
|
||||
}
|
||||
}
|
||||
37
handler/registerHandler.go
Normal file
37
handler/registerHandler.go
Normal file
@ -0,0 +1,37 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var user common.Credentials
|
||||
// Decode input
|
||||
err := json.NewDecoder(r.Body).Decode(&user)
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
// Get data store
|
||||
db, err := common.GetTodoDataStore()
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
// Check if user exists
|
||||
if len(db.GetFromBucketByKey(common.UserBucket, user.Username)) > 0 {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
w.Write([]byte(`{"error":"user already exists"}`))
|
||||
return
|
||||
}
|
||||
// Hash password
|
||||
password, err := user.HashedPassword()
|
||||
if handleError(w, http.StatusBadRequest, err) {
|
||||
return
|
||||
}
|
||||
// Store user
|
||||
err = db.SaveValueToBucket(common.UserBucket, user.Username, password)
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
32
handler/storeHandler.go
Normal file
32
handler/storeHandler.go
Normal file
@ -0,0 +1,32 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common/jwt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func StoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := jwt.GetVerifiedUser(r)
|
||||
if handleError(w, http.StatusUnauthorized, err) {
|
||||
return
|
||||
}
|
||||
|
||||
var todo common.StoreTodoRequest
|
||||
// Decode input
|
||||
err = json.NewDecoder(r.Body).Decode(&todo)
|
||||
if handleError(w, http.StatusBadRequest, err) {
|
||||
return
|
||||
}
|
||||
|
||||
store, err := common.GetTodoDataStore()
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
|
||||
err = todo.Store(store, user)
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
71
handler/syncHandler.go
Normal file
71
handler/syncHandler.go
Normal file
@ -0,0 +1,71 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common/jwt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func SyncHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := jwt.GetVerifiedUser(r)
|
||||
if handleError(w, http.StatusUnauthorized, err) {
|
||||
return
|
||||
}
|
||||
|
||||
var todoList common.TodoList
|
||||
err = json.NewDecoder(r.Body).Decode(&todoList)
|
||||
if handleError(w, http.StatusBadRequest, err) {
|
||||
return
|
||||
}
|
||||
|
||||
store, err := common.GetTodoDataStore()
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
|
||||
storedTodoJsons := store.GetAllFromBucket(user)
|
||||
|
||||
serverTodos := make(map[string]common.Todo)
|
||||
for key, val := range storedTodoJsons {
|
||||
var todo common.Todo
|
||||
if json.Unmarshal([]byte(val), &todo) == nil {
|
||||
serverTodos[key] = todo
|
||||
}
|
||||
}
|
||||
|
||||
var response = common.SyncResponse{
|
||||
SyncedTodos: []common.Todo{},
|
||||
MisMatchingTodos: []common.MisMatchingTodo{},
|
||||
}
|
||||
|
||||
for _, clientTodo := range todoList.Todos {
|
||||
serverTodo, exists := serverTodos[clientTodo.Name]
|
||||
|
||||
if !exists {
|
||||
err = clientTodo.Store(store, user)
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
serverTodos[clientTodo.Name] = clientTodo
|
||||
} else {
|
||||
if !reflect.DeepEqual(serverTodo, clientTodo) {
|
||||
response.MisMatchingTodos = append(response.MisMatchingTodos, common.MisMatchingTodo{
|
||||
ServerTodo: serverTodo,
|
||||
LocalTodo: clientTodo,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, todo := range serverTodos {
|
||||
response.SyncedTodos = append(response.SyncedTodos, todo)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(response)
|
||||
if handleError(w, http.StatusInternalServerError, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
58
main.go
Normal file
58
main.go
Normal file
@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/handler"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := common.GetTodoDataStore()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Create a new ServeMux to route requests.
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Register handler for each endpoint.
|
||||
mux.HandleFunc("POST /register", handler.RegisterHandler)
|
||||
mux.HandleFunc("POST /login", handler.LoginHandler)
|
||||
mux.HandleFunc("POST /store", handler.StoreHandler)
|
||||
mux.HandleFunc("GET /sync", handler.SyncHandler)
|
||||
|
||||
// A simple root handler to confirm the server is running.
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, "API Server is running. Use the /register, /login, /store, or /sync endpoints.")
|
||||
})
|
||||
|
||||
port := os.Getenv("SERVER_PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
// Configure the server.
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: mux,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("Server starting on port %s...", port)
|
||||
|
||||
// Start the server.
|
||||
// log.Fatal will exit the application if the server fails to start.
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user