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