3 Commits

Author SHA1 Message Date
Adrian Zürcher
5d0496f1db add new handlers for event and responsible tables 2025-10-17 15:41:13 +02:00
Adrian Zürcher
521896510d change module name to repo 2025-10-08 15:06:08 +02:00
Adrian Zürcher
c9577720e6 add README 2025-10-08 15:03:43 +02:00
16 changed files with 1047 additions and 560 deletions

View File

@@ -0,0 +1,50 @@
# 🧩 Member DB (Golang)
A lightweight and efficient **member management system** written in **Go**.
This project provides a RESTful API to perform essential operations like:
✅ **Add and manage members**
📅 **Track member attendance at events**
🗺️ **Record member excursions and outings**
Ideal for small organizations, clubs, or community groups looking for a minimal yet effective backend to handle membership data.
---
## 🚀 Features
- ⚡ Built using **Go** for high performance and simplicity
- 🌐 **RESTful API** endpoints
- 🧱 Modular structure for easy expansion
- 🪶 Lightweight — no unnecessary dependencies
---
## 🎯 Use Cases
- 🏫 Club membership tracking
- 🗓️ Attendance records for events, meetings, or workshops
- 🧭 Excursion or trip logging for community groups
---
## 🛠️ Tech Stack
- **Language:** Go (Golang)
- **Architecture:** RESTful API
- **Storage:** (Add your DB here, e.g., SQLite, PostgreSQL, or in-memory)
- **Deployment:** Runs easily on any Go-supported environment
---
## 📦 Getting Started
### Prerequisites
- Go 1.24.5+ installed
### Installation
```bash
git clone https://gitea.tecamino.com/paadi/memberDB.git
cd memberDB
go mod tidy
go run main.go

View File

@@ -29,9 +29,11 @@ func NewAPI(host string, port int) *API {
apiHandler := NewAPIHandler()
v1 := r.Group("v1")
v1.GET("/members", apiHandler.GetMemberById)
v1.GET("/events", apiHandler.GetEventById)
v1.GET("/events/new", apiHandler.StartNewEvent)
v1.GET("/events/delete", apiHandler.DeleteEvent)
v1.GET("/members", apiHandler.GetMemberById)
v1.GET("/responsible", apiHandler.GetResponsibleById)
v1.POST("/database/open", apiHandler.OpenDatabase)
v1.POST("/members/add", apiHandler.AddNewMember)
@@ -39,6 +41,12 @@ func NewAPI(host string, port int) *API {
v1.POST("/members/delete", apiHandler.DeleteMember)
v1.POST("/members/import/csv", apiHandler.ImportCSV)
v1.POST("/events/attendees/add", apiHandler.AddNewAttendees)
v1.POST("/events/attendees/delete", apiHandler.DeleteAttendee)
v1.POST("/responsible/add", apiHandler.AddNewResponsible)
v1.POST("/responsible/delete", apiHandler.DeleteResponsible)
return &API{host: host,
port: port,
router: r,

View File

@@ -1,10 +1,11 @@
package api
import (
"memberDB/handlers"
"memberDB/models"
"net/http"
"gitea.tecamino.com/paadi/memberDB/handlers"
"gitea.tecamino.com/paadi/memberDB/models"
"github.com/gin-gonic/gin"
)
@@ -49,7 +50,14 @@ func (a *APIHandler) OpenDatabase(c *gin.Context) {
return
}
if err := a.DbHandler.CreateNewAttendanceTable(); err != nil {
if err := a.DbHandler.CreateNewEventTable(); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
if err := a.DbHandler.CreateNewResponsibleTable(); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
@@ -70,6 +78,7 @@ func (a *APIHandler) OpenDatabase(c *gin.Context) {
})
}
// databaseOpened is a helper function to first check wheter database is open for requests
func (a *APIHandler) databaseOpened(c *gin.Context) bool {
if a.DbHandler == nil {
c.JSON(http.StatusBadRequest, gin.H{

View File

@@ -1,63 +0,0 @@
package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
func (a *APIHandler) StartNewEvent(c *gin.Context) {
if !a.databaseOpened(c) {
return
}
name := c.Query("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{
"message": "missing query 'name'",
})
return
}
if err := a.DbHandler.StartNewEvent(name); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "New Event added " + name,
})
}
func (a *APIHandler) GetEventById(c *gin.Context) {
if !a.databaseOpened(c) {
return
}
var i int
var err error
id := c.Query("id")
if id != "" {
i, err = strconv.Atoi(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
}
events, err := a.DbHandler.GetEvent(i)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, events)
}

148
api/eventHandler.go Normal file
View File

@@ -0,0 +1,148 @@
package api
import (
"net/http"
"strconv"
"gitea.tecamino.com/paadi/memberDB/models"
"github.com/gin-gonic/gin"
)
func (a *APIHandler) StartNewEvent(c *gin.Context) {
if !a.databaseOpened(c) {
return
}
name := c.Query("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{
"message": "missing query 'name'",
})
return
}
if err := a.DbHandler.StartNewEvent(name); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "New Event added " + name,
})
}
func (a *APIHandler) GetEventById(c *gin.Context) {
if !a.databaseOpened(c) {
return
}
var i int
var err error
id := c.Query("id")
if id != "" {
i, err = strconv.Atoi(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
}
events, err := a.DbHandler.GetEvent(i)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, events)
}
func (a *APIHandler) DeleteEvent(c *gin.Context) {
if !a.databaseOpened(c) {
return
}
var err error
var request struct {
Ids []int `json:"ids"`
}
err = c.BindJSON(&request)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
err = a.DbHandler.DeleteEvent(request.Ids...)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "event deleted",
})
}
func (a *APIHandler) AddNewAttendees(c *gin.Context) {
if !a.databaseOpened(c) {
return
}
var event models.Event
err := c.BindJSON(&event)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
err = a.DbHandler.AddAttendeesToEvent(event)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "attendee(s) added",
})
}
func (a *APIHandler) DeleteAttendee(c *gin.Context) {
if !a.databaseOpened(c) {
return
}
var err error
var event models.Event
err = c.BindJSON(&event)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
err = a.DbHandler.DeleteAttendeesFromEvent(event)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "attendee(s) deleted",
})
}

View File

@@ -4,10 +4,11 @@ import (
"encoding/csv"
"fmt"
"io"
"memberDB/models"
"net/http"
"strconv"
"gitea.tecamino.com/paadi/memberDB/models"
"github.com/gin-gonic/gin"
"github.com/saintfish/chardet"
"golang.org/x/text/encoding/charmap"
@@ -20,7 +21,13 @@ func (a *APIHandler) AddNewMember(c *gin.Context) {
}
var member models.Member
c.BindJSON(&member)
err := c.BindJSON(&member)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
var text string
if member.FirstName == "" {
@@ -39,7 +46,7 @@ func (a *APIHandler) AddNewMember(c *gin.Context) {
return
}
err := a.DbHandler.AddNewMember(member)
err = a.DbHandler.AddNewMember(member)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
@@ -106,7 +113,13 @@ func (a *APIHandler) EditMember(c *gin.Context) {
}
var member models.Member
c.BindJSON(&member)
err = c.BindJSON(&member)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
err = a.DbHandler.UpdateMember(i, member)
if err != nil {

95
api/responsibleHandler.go Normal file
View File

@@ -0,0 +1,95 @@
package api
import (
"net/http"
"strconv"
"gitea.tecamino.com/paadi/memberDB/models"
"github.com/gin-gonic/gin"
)
func (a *APIHandler) AddNewResponsible(c *gin.Context) {
if !a.databaseOpened(c) {
return
}
var responsible models.Person
err := c.BindJSON(&responsible)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
err = a.DbHandler.AddNewResponsible(responsible)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "responsible added",
})
}
func (a *APIHandler) GetResponsibleById(c *gin.Context) {
if !a.databaseOpened(c) {
return
}
var i int
var err error
id := c.Query("id")
if id != "" {
i, err = strconv.Atoi(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
}
members, err := a.DbHandler.GetResponsible(i)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, members)
}
func (a *APIHandler) DeleteResponsible(c *gin.Context) {
if !a.databaseOpened(c) {
return
}
var err error
var request struct {
Ids []int `json:"ids"`
}
err = c.BindJSON(&request)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
err = a.DbHandler.DeleteResponsible(request.Ids...)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "responsible(s) deleted",
})
}

2
go.mod
View File

@@ -1,4 +1,4 @@
module memberDB
module gitea.tecamino.com/paadi/memberDB
go 1.24.5

View File

@@ -5,16 +5,9 @@ import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"memberDB/crypto"
"memberDB/models"
"memberDB/utils"
"os"
"path/filepath"
"strings"
"time"
_ "modernc.org/sqlite"
)
@@ -58,477 +51,8 @@ func (dh *DatabaseHandler) SetToken(token string) {
dh.token = []byte(token)
}
// createExampleTable creates a new table.
func (dh *DatabaseHandler) CreateNewMemberTable() error {
createTableSQL := `
CREATE TABLE IF NOT EXISTS members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
first_name_hash TEXT NOT NULL,
last_name TEXT NOT NULL,
last_name_hash TEXT NOT NULL,
birthday TEXT NOT NULL,
birthday_hash TEXT NOT NULL,
address TEXT NOT NULL,
zip_code TEXT NOT NULL,
town TEXT NOT NULL,
phone TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
first_visit TEXT NOT NULL,
last_visit TEXT NOT NULL,
member_group TEXT NOT NULL,
responsible_person TEXT NOT NULL
);`
_, err := dh.database.Exec(createTableSQL)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
return nil
}
func (dh *DatabaseHandler) CreateNewAttendanceTable() error {
createTableSQL := `
CREATE TABLE IF NOT EXISTS attendance (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
date TEXT UNIQUE NOT NULL,
attendees TEXT NOT NULL,
count INTEGER NOT NULL
);`
_, err := dh.database.Exec(createTableSQL)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
return nil
}
func (dh *DatabaseHandler) AddNewMember(members ...models.Member) error {
for _, member := range members {
exists, err := dh.memberExists(member)
if err != nil {
return err
} else if exists {
return fmt.Errorf("member %s %s %s exists already", member.FirstName, member.LastName, member.Birthday)
}
encFirstName, err := crypto.Encrypt(member.FirstName, dh.token)
if err != nil {
return err
}
encLastName, err := crypto.Encrypt(member.LastName, dh.token)
if err != nil {
return err
}
//check correct birtday format
if member.Birthday != "" && !utils.IsValidBirthday(member.Birthday) {
return errors.New("incorrect birthday format")
}
encBirthday, err := crypto.Encrypt(member.Birthday, dh.token)
if err != nil {
return err
}
encAddress, err := crypto.Encrypt(member.Address, dh.token)
if err != nil {
return err
}
encZip, err := crypto.Encrypt(member.Zip, dh.token)
if err != nil {
return err
}
encTown, err := crypto.Encrypt(member.Town, dh.token)
if err != nil {
return err
}
encPhone, err := crypto.Encrypt(member.Phone, dh.token)
if err != nil {
return err
}
//check correct email format
if member.Email != "" && !utils.IsValidEmail(member.Email) {
return errors.New("incorrect email format")
}
encEmail, err := crypto.Encrypt(member.Email, dh.token)
if err != nil {
return err
}
now := time.Now().Format("2006-01-02 15:04:05")
encFirstVisit, err := crypto.Encrypt(now, dh.token)
if err != nil {
return err
}
encLastVisit, err := crypto.Encrypt(now, dh.token)
if err != nil {
return err
}
encGroup, err := crypto.Encrypt(member.Group, dh.token)
if err != nil {
return err
}
encResponsiblePerson, err := crypto.Encrypt(member.ResponsiblePerson, dh.token)
if err != nil {
return err
}
_, err = dh.database.Exec("INSERT INTO members (first_name, first_name_hash, last_name, last_name_hash, birthday, birthday_hash, address, zip_code, town, phone, email, first_visit, last_visit, member_group, responsible_person) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", encFirstName, dh.hashField(member.FirstName), encLastName, dh.hashField(member.LastName), encBirthday, dh.hashField(member.Birthday), encAddress, encZip, encTown, encPhone, encEmail, encFirstVisit, encLastVisit, encGroup, encResponsiblePerson)
if err != nil {
return err
}
}
return nil
}
func (dh *DatabaseHandler) DeleteMember(ids ...int) error {
if len(ids) == 0 {
return errors.New("no ids given to be deleted")
}
placeholders := make([]string, len(ids))
args := make([]interface{}, len(ids))
for i, id := range ids {
placeholders[i] = "?"
args[i] = id
}
query := fmt.Sprintf("DELETE FROM members WHERE id IN (%s)", strings.Join(placeholders, ","))
_, err := dh.database.Exec(query, args...)
return err
}
func (dh *DatabaseHandler) GetMember(id int) (members []models.Member, err error) {
query := `SELECT id, first_name, last_name, birthday, address, zip_code, town, phone, email, first_visit, last_visit, member_group, responsible_person FROM members`
var args any
if id > 0 {
query = `
SELECT id, first_name, last_name, birthday, address, zip_code, town, phone, email, first_visit, last_visit, member_group, responsible_person FROM members
WHERE id = ?
`
args = id
}
rows, err := dh.database.Query(query, args)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var id int
var encFirstName, encLastName, encBirthday, encAddress, encZip, encTown, encPhone, encEmail, encFirstVisit, encLastVisit, encGroup, encResponsiblePerson string
if err = rows.Scan(&id, &encFirstName, &encLastName, &encBirthday, &encAddress, &encZip, &encTown, &encPhone, &encEmail, &encFirstVisit, &encLastVisit, &encGroup, &encResponsiblePerson); err != nil {
return
}
firstName, err := crypto.Decrypt(encFirstName, dh.token)
if err != nil {
return members, err
}
lastName, err := crypto.Decrypt(encLastName, dh.token)
if err != nil {
return members, err
}
birthday, err := crypto.Decrypt(encBirthday, dh.token)
if err != nil {
return members, err
}
address, err := crypto.Decrypt(encAddress, dh.token)
if err != nil {
return members, err
}
zip, err := crypto.Decrypt(encZip, dh.token)
if err != nil {
return members, err
}
town, err := crypto.Decrypt(encTown, dh.token)
if err != nil {
return members, err
}
phone, err := crypto.Decrypt(encPhone, dh.token)
if err != nil {
return members, err
}
email, err := crypto.Decrypt(encEmail, dh.token)
if err != nil {
return members, err
}
firstVisit, err := crypto.Decrypt(encFirstVisit, dh.token)
if err != nil {
return members, err
}
lastVisit, err := crypto.Decrypt(encLastVisit, dh.token)
if err != nil {
return members, err
}
group, err := crypto.Decrypt(encGroup, dh.token)
if err != nil {
return members, err
}
responsiblePerson, err := crypto.Decrypt(encResponsiblePerson, dh.token)
if err != nil {
return members, err
}
members = append(members, models.Member{
Id: id,
FirstName: firstName,
LastName: lastName,
Birthday: birthday,
Address: address,
Zip: zip,
Town: town,
Phone: phone,
Email: email,
FirstVisit: firstVisit,
LastVisit: lastVisit,
Group: group,
ResponsiblePerson: responsiblePerson,
})
}
return members, nil
}
func (dh *DatabaseHandler) UpdateMember(id int, member models.Member) (err error) {
var queryParameters []string
var args []any
if member.FirstName != "" {
encFirstName, err := crypto.Encrypt(member.FirstName, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "first_name = ?")
queryParameters = append(queryParameters, "first_name_hash = ?")
args = append(args, encFirstName)
args = append(args, dh.hashField(member.FirstName))
}
if member.LastName != "" {
encLastName, err := crypto.Encrypt(member.LastName, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "last_name = ?")
queryParameters = append(queryParameters, "last_name_hash = ?")
args = append(args, encLastName)
args = append(args, dh.hashField(member.LastName))
}
//check correct birtday format
if member.Birthday != "" && utils.IsValidBirthday(member.Birthday) {
encBirthday, err := crypto.Encrypt(member.Birthday, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "birthday = ?")
queryParameters = append(queryParameters, "birthday_hash = ?")
args = append(args, encBirthday)
args = append(args, dh.hashField(member.Birthday))
} else if member.Birthday != "" {
return errors.New("incorrect birthday format")
}
if member.Address != "" {
encAddress, err := crypto.Encrypt(member.Address, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "address = ?")
args = append(args, encAddress)
}
if member.Zip != "" {
encZip, err := crypto.Encrypt(member.Zip, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "zip_code = ?")
args = append(args, encZip)
}
if member.Town != "" {
encTown, err := crypto.Encrypt(member.Town, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "town = ?")
args = append(args, encTown)
}
if member.Phone != "" {
encPhone, err := crypto.Encrypt(member.Phone, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "phone = ?")
args = append(args, encPhone)
}
//check correct email format
if member.Email != "" && utils.IsValidEmail(member.Email) {
encEmail, err := crypto.Encrypt(member.Email, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "email = ?")
args = append(args, encEmail)
} else if member.Email != "" {
return errors.New("incorrect email format")
}
if member.FirstVisit != "" {
encFirstVisit, err := crypto.Encrypt(member.FirstVisit, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "first_visit = ?")
args = append(args, encFirstVisit)
}
if member.LastVisit != "" {
encLastVisit, err := crypto.Encrypt(member.LastVisit, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "last_visit = ?")
args = append(args, encLastVisit)
}
if member.Group != "" {
encFirstVisit, err := crypto.Encrypt(member.Group, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "member_group = ?")
args = append(args, encFirstVisit)
}
if member.ResponsiblePerson != "" {
encResponsiblePerson, err := crypto.Encrypt(member.ResponsiblePerson, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "responsible_person = ?")
args = append(args, encResponsiblePerson)
}
query := `UPDATE members SET `
query += strings.Join(queryParameters, ", ")
query += ` WHERE id = ?`
args = append(args, id)
_, err = dh.database.Exec(query, args...)
return err
}
func (dh *DatabaseHandler) memberExists(member models.Member) (bool, error) {
query := `
SELECT 1 FROM members
WHERE first_name_hash = ? AND last_name_hash = ? AND birthday_hash = ?
LIMIT 1
`
var exists int
err := dh.database.QueryRow(query, dh.hashField(member.FirstName), dh.hashField(member.LastName), dh.hashField(member.Birthday)).Scan(&exists)
if err == sql.ErrNoRows {
return false, nil // no match
} else if err != nil {
return false, err // db error
}
return true, nil // match found
}
func (dh *DatabaseHandler) hashField(field string) string {
h := hmac.New(sha256.New, dh.token)
h.Write([]byte(field))
return hex.EncodeToString(h.Sum(nil))
}
func (dh *DatabaseHandler) StartNewEvent(name string) error {
_, err := dh.database.Exec("INSERT INTO attendance (name, date, attendees, count) VALUES (?, ?, ?, ?)", name, time.Now().Format("2006-01-02 15:04:05"), "[]", 0)
return err
}
func (dh *DatabaseHandler) GetEvent(id int) (attendees []models.Event, err error) {
query := `SELECT id, name, date, attendees, count FROM attendance`
var args any
if id > 0 {
query = `
SELECT id, name, date, attendees, count FROM attendance
WHERE id = ?
`
args = id
}
rows, err := dh.database.Query(query, args)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var id, count int
var name, date, attendance string
if err = rows.Scan(&id, &name, &date, &attendance, &count); err != nil {
return
}
var a []models.Attendees
if err := json.Unmarshal([]byte(attendance), &a); err != nil {
return attendees, err
}
attendees = append(attendees, models.Event{
Id: id,
Name: name,
Date: date,
Attendees: a,
Count: count,
})
}
return attendees, nil
}

170
handlers/events.go Normal file
View File

@@ -0,0 +1,170 @@
package handlers
import (
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"time"
"gitea.tecamino.com/paadi/memberDB/models"
)
func (dh *DatabaseHandler) CreateNewEventTable() error {
createTableSQL := `
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
date TEXT UNIQUE NOT NULL,
attendees TEXT NOT NULL,
count INTEGER NOT NULL
);`
_, err := dh.database.Exec(createTableSQL)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
return nil
}
func (dh *DatabaseHandler) StartNewEvent(name string) error {
_, err := dh.database.Exec("INSERT INTO events (name, date, attendees, count) VALUES (?, ?, ?, ?)", name, time.Now().Format("2006-01-02 15:04:05"), "[]", 0)
return err
}
func (dh *DatabaseHandler) GetEvent(id int) (attendees []models.Event, err error) {
var args any
query := `SELECT id, name, date, attendees, count FROM events`
if id > 0 {
query += ` WHERE id = ?`
args = id
}
rows, err := dh.database.Query(query, args)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var id, count int
var name, date, attendance string
if err = rows.Scan(&id, &name, &date, &attendance, &count); err != nil {
return
}
var a []models.Person
if err := json.Unmarshal([]byte(attendance), &a); err != nil {
return attendees, err
}
attendees = append(attendees, models.Event{
Id: id,
Name: name,
Date: date,
Attendees: a,
Count: count,
})
}
return attendees, nil
}
func (dh *DatabaseHandler) DeleteEvent(ids ...int) error {
if len(ids) == 0 {
return errors.New("no ids given to be deleted")
}
placeholders := make([]string, len(ids))
args := make([]any, len(ids))
for i, id := range ids {
placeholders[i] = "?"
args[i] = id
}
query := fmt.Sprintf("DELETE FROM events WHERE id IN (%s)", strings.Join(placeholders, ","))
_, err := dh.database.Exec(query, args...)
return err
}
func (dh *DatabaseHandler) AddAttendeesToEvent(event models.Event) error {
//get event from database by id
row := dh.database.QueryRow(`SELECT attendees FROM events WHERE id = ?`, event.Id)
var attendeesString string
if err := row.Scan(&attendeesString); err != nil {
return err
}
fmt.Println(attendeesString)
var attendees []models.Person
err := json.Unmarshal([]byte(attendeesString), &attendees)
if err != nil {
return err
}
next:
for _, newAttendee := range event.Attendees {
for _, attendee := range attendees {
if attendee.FirstName == newAttendee.FirstName && attendee.LastName == newAttendee.LastName {
continue next
}
}
attendees = append(attendees, newAttendee)
}
attendeesByte, err := json.Marshal(attendees)
if err != nil {
return err
}
count := len(attendees)
_, err = dh.database.Exec("UPDATE events SET attendees= ? count= ? WHERE id= ?", string(attendeesByte), event.Id, count)
if err != nil {
return err
}
return nil
}
func (dh *DatabaseHandler) DeleteAttendeesFromEvent(event models.Event) error {
//get event from database by id
row := dh.database.QueryRow(`SELECT attendees FROM events WHERE id = ?`, event.Id)
var attendeesString string
if err := row.Scan(&attendeesString); err != nil {
return err
}
fmt.Println(attendeesString)
var attendees []models.Person
err := json.Unmarshal([]byte(attendeesString), &attendees)
if err != nil {
return err
}
for _, newAttendee := range event.Attendees {
for i, attendee := range attendees {
if attendee.FirstName == newAttendee.FirstName && attendee.LastName == newAttendee.LastName {
attendees = slices.Delete(attendees, i, i+1)
}
}
}
attendeesByte, err := json.Marshal(attendees)
if err != nil {
return err
}
count := len(attendees)
_, err = dh.database.Exec("UPDATE events SET attendees= ? count= ? WHERE id= ?", string(attendeesByte), event.Id, count)
if err != nil {
return err
}
return nil
}

421
handlers/members.go Normal file
View File

@@ -0,0 +1,421 @@
package handlers
import (
"database/sql"
"errors"
"fmt"
"strings"
"time"
"gitea.tecamino.com/paadi/memberDB/crypto"
"gitea.tecamino.com/paadi/memberDB/models"
"gitea.tecamino.com/paadi/memberDB/utils"
)
// CreateNewMemberTable creates a new member table.
func (dh *DatabaseHandler) CreateNewMemberTable() error {
createTableSQL := `
CREATE TABLE IF NOT EXISTS members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
first_name_hash TEXT NOT NULL,
last_name TEXT NOT NULL,
last_name_hash TEXT NOT NULL,
birthday TEXT NOT NULL,
birthday_hash TEXT NOT NULL,
address TEXT NOT NULL,
zip_code TEXT NOT NULL,
town TEXT NOT NULL,
phone TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
first_visit TEXT NOT NULL,
last_visit TEXT NOT NULL,
member_group TEXT NOT NULL,
responsible_person TEXT NOT NULL
);`
_, err := dh.database.Exec(createTableSQL)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
return nil
}
// AddNewMember adds a new member to memeber table at least fist, las name and birthday has to be entered
func (dh *DatabaseHandler) AddNewMember(members ...models.Member) error {
for _, member := range members {
exists, err := dh.memberExists(member)
if err != nil {
return err
} else if exists {
return fmt.Errorf("member %s %s %s exists already", member.FirstName, member.LastName, member.Birthday)
}
encFirstName, err := crypto.Encrypt(member.FirstName, dh.token)
if err != nil {
return err
}
encLastName, err := crypto.Encrypt(member.LastName, dh.token)
if err != nil {
return err
}
//check correct birtday format
if member.Birthday != "" && !utils.IsValidBirthday(member.Birthday) {
return errors.New("incorrect birthday format")
}
encBirthday, err := crypto.Encrypt(member.Birthday, dh.token)
if err != nil {
return err
}
encAddress, err := crypto.Encrypt(member.Address, dh.token)
if err != nil {
return err
}
encZip, err := crypto.Encrypt(member.Zip, dh.token)
if err != nil {
return err
}
encTown, err := crypto.Encrypt(member.Town, dh.token)
if err != nil {
return err
}
encPhone, err := crypto.Encrypt(member.Phone, dh.token)
if err != nil {
return err
}
//check correct email format
if member.Email != "" && !utils.IsValidEmail(member.Email) {
return errors.New("incorrect email format")
}
encEmail, err := crypto.Encrypt(member.Email, dh.token)
if err != nil {
return err
}
now := time.Now().Format("2006-01-02 15:04:05")
encFirstVisit, err := crypto.Encrypt(now, dh.token)
if err != nil {
return err
}
encLastVisit, err := crypto.Encrypt(now, dh.token)
if err != nil {
return err
}
encGroup, err := crypto.Encrypt(member.Group, dh.token)
if err != nil {
return err
}
encResponsiblePerson, err := crypto.Encrypt(member.ResponsiblePerson, dh.token)
if err != nil {
return err
}
_, err = dh.database.Exec("INSERT INTO members (first_name, first_name_hash, last_name, last_name_hash, birthday, birthday_hash, address, zip_code, town, phone, email, first_visit, last_visit, member_group, responsible_person) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", encFirstName, dh.hashField(member.FirstName), encLastName, dh.hashField(member.LastName), encBirthday, dh.hashField(member.Birthday), encAddress, encZip, encTown, encPhone, encEmail, encFirstVisit, encLastVisit, encGroup, encResponsiblePerson)
if err != nil {
return err
}
}
return nil
}
// DeleteMember removes members by given ids
func (dh *DatabaseHandler) DeleteMember(ids ...int) error {
if len(ids) == 0 {
return errors.New("no ids given to be deleted")
}
placeholders := make([]string, len(ids))
args := make([]any, len(ids))
for i, id := range ids {
placeholders[i] = "?"
args[i] = id
}
query := fmt.Sprintf("DELETE FROM members WHERE id IN (%s)", strings.Join(placeholders, ","))
_, err := dh.database.Exec(query, args...)
return err
}
// GetMember returns one member by given id
func (dh *DatabaseHandler) GetMember(id int) (members []models.Member, err error) {
var args any
query := `SELECT id, first_name, last_name, birthday, address, zip_code, town, phone, email, first_visit, last_visit, member_group, responsible_person FROM members`
if id > 0 {
query = ` WHERE id = ?`
args = id
}
rows, err := dh.database.Query(query, args)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var id int
var encFirstName, encLastName, encBirthday, encAddress, encZip, encTown, encPhone, encEmail, encFirstVisit, encLastVisit, encGroup, encResponsiblePerson string
if err = rows.Scan(&id, &encFirstName, &encLastName, &encBirthday, &encAddress, &encZip, &encTown, &encPhone, &encEmail, &encFirstVisit, &encLastVisit, &encGroup, &encResponsiblePerson); err != nil {
return
}
firstName, err := crypto.Decrypt(encFirstName, dh.token)
if err != nil {
return members, err
}
lastName, err := crypto.Decrypt(encLastName, dh.token)
if err != nil {
return members, err
}
birthday, err := crypto.Decrypt(encBirthday, dh.token)
if err != nil {
return members, err
}
address, err := crypto.Decrypt(encAddress, dh.token)
if err != nil {
return members, err
}
zip, err := crypto.Decrypt(encZip, dh.token)
if err != nil {
return members, err
}
town, err := crypto.Decrypt(encTown, dh.token)
if err != nil {
return members, err
}
phone, err := crypto.Decrypt(encPhone, dh.token)
if err != nil {
return members, err
}
email, err := crypto.Decrypt(encEmail, dh.token)
if err != nil {
return members, err
}
firstVisit, err := crypto.Decrypt(encFirstVisit, dh.token)
if err != nil {
return members, err
}
lastVisit, err := crypto.Decrypt(encLastVisit, dh.token)
if err != nil {
return members, err
}
group, err := crypto.Decrypt(encGroup, dh.token)
if err != nil {
return members, err
}
responsiblePerson, err := crypto.Decrypt(encResponsiblePerson, dh.token)
if err != nil {
return members, err
}
members = append(members, models.Member{
Id: id,
FirstName: firstName,
LastName: lastName,
Birthday: birthday,
Address: address,
Zip: zip,
Town: town,
Phone: phone,
Email: email,
FirstVisit: firstVisit,
LastVisit: lastVisit,
Group: group,
ResponsiblePerson: responsiblePerson,
})
}
return members, nil
}
// UpdateMember updates/overrides all information given meber id
func (dh *DatabaseHandler) UpdateMember(id int, member models.Member) (err error) {
var queryParameters []string
var args []any
if member.FirstName != "" {
encFirstName, err := crypto.Encrypt(member.FirstName, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "first_name = ?")
queryParameters = append(queryParameters, "first_name_hash = ?")
args = append(args, encFirstName)
args = append(args, dh.hashField(member.FirstName))
}
if member.LastName != "" {
encLastName, err := crypto.Encrypt(member.LastName, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "last_name = ?")
queryParameters = append(queryParameters, "last_name_hash = ?")
args = append(args, encLastName)
args = append(args, dh.hashField(member.LastName))
}
//check correct birtday format
if member.Birthday != "" && utils.IsValidBirthday(member.Birthday) {
encBirthday, err := crypto.Encrypt(member.Birthday, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "birthday = ?")
queryParameters = append(queryParameters, "birthday_hash = ?")
args = append(args, encBirthday)
args = append(args, dh.hashField(member.Birthday))
} else if member.Birthday != "" {
return errors.New("incorrect birthday format")
}
if member.Address != "" {
encAddress, err := crypto.Encrypt(member.Address, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "address = ?")
args = append(args, encAddress)
}
if member.Zip != "" {
encZip, err := crypto.Encrypt(member.Zip, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "zip_code = ?")
args = append(args, encZip)
}
if member.Town != "" {
encTown, err := crypto.Encrypt(member.Town, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "town = ?")
args = append(args, encTown)
}
if member.Phone != "" {
encPhone, err := crypto.Encrypt(member.Phone, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "phone = ?")
args = append(args, encPhone)
}
//check correct email format
if member.Email != "" && utils.IsValidEmail(member.Email) {
encEmail, err := crypto.Encrypt(member.Email, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "email = ?")
args = append(args, encEmail)
} else if member.Email != "" {
return errors.New("incorrect email format")
}
if member.FirstVisit != "" {
encFirstVisit, err := crypto.Encrypt(member.FirstVisit, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "first_visit = ?")
args = append(args, encFirstVisit)
}
if member.LastVisit != "" {
encLastVisit, err := crypto.Encrypt(member.LastVisit, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "last_visit = ?")
args = append(args, encLastVisit)
}
if member.Group != "" {
encFirstVisit, err := crypto.Encrypt(member.Group, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "member_group = ?")
args = append(args, encFirstVisit)
}
if member.ResponsiblePerson != "" {
encResponsiblePerson, err := crypto.Encrypt(member.ResponsiblePerson, dh.token)
if err != nil {
return err
}
queryParameters = append(queryParameters, "responsible_person = ?")
args = append(args, encResponsiblePerson)
}
query := `UPDATE members SET `
query += strings.Join(queryParameters, ", ")
query += ` WHERE id = ?`
args = append(args, id)
_, err = dh.database.Exec(query, args...)
return err
}
// memberExists helper to check wheter member already exists
func (dh *DatabaseHandler) memberExists(member models.Member) (bool, error) {
query := `
SELECT 1 FROM members
WHERE first_name_hash = ? AND last_name_hash = ? AND birthday_hash = ?
LIMIT 1
`
var exists int
err := dh.database.QueryRow(query, dh.hashField(member.FirstName), dh.hashField(member.LastName), dh.hashField(member.Birthday)).Scan(&exists)
if err == sql.ErrNoRows {
return false, nil // no match
} else if err != nil {
return false, err // db error
}
return true, nil // match found
}

110
handlers/responsible.go Normal file
View File

@@ -0,0 +1,110 @@
package handlers
import (
"database/sql"
"errors"
"fmt"
"strings"
"gitea.tecamino.com/paadi/memberDB/models"
)
func (dh *DatabaseHandler) CreateNewResponsibleTable() error {
createTableSQL := `
CREATE TABLE IF NOT EXISTS responsible (
id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT UNIQUE NOT NULL,
);`
_, err := dh.database.Exec(createTableSQL)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
return nil
}
func (dh *DatabaseHandler) AddNewResponsible(responsibles ...models.Person) error {
for _, responsible := range responsibles {
exists, err := dh.responsibleExists(responsible)
if err != nil {
return err
} else if exists {
continue
}
_, err = dh.database.Exec("INSERT INTO responsible (first_name, last_name) VALUES (?, ?)", responsible.FirstName, responsible.LastName)
if err != nil {
return err
}
}
return nil
}
func (dh *DatabaseHandler) GetResponsible(id int) (persons []models.Person, err error) {
var args any
query := `SELECT id, first_name, last_name FROM responsible`
if id > 0 {
query = ` WHERE id = ?`
args = id
}
rows, err := dh.database.Query(query, args)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var id int
var firstName, lastName string
if err = rows.Scan(&id, &firstName, &lastName); err != nil {
return
}
persons = append(persons, models.Person{
Id: id,
FirstName: firstName,
LastName: lastName,
})
}
return persons, nil
}
func (dh *DatabaseHandler) DeleteResponsible(ids ...int) error {
if len(ids) == 0 {
return errors.New("no ids given to be deleted")
}
placeholders := make([]string, len(ids))
args := make([]any, len(ids))
for i, id := range ids {
placeholders[i] = "?"
args[i] = id
}
query := fmt.Sprintf("DELETE FROM responsible WHERE id IN (%s)", strings.Join(placeholders, ","))
_, err := dh.database.Exec(query, args...)
return err
}
func (dh *DatabaseHandler) responsibleExists(responsible models.Person) (bool, error) {
query := `
SELECT 1 FROM responsible
WHERE first_name = ? AND last_name = ?
LIMIT 1
`
var exists int
err := dh.database.QueryRow(query, responsible.FirstName, responsible.LastName).Scan(&exists)
if err == sql.ErrNoRows {
return false, nil // no match
} else if err != nil {
return false, err // db error
}
return true, nil // match found
}

View File

@@ -3,7 +3,8 @@ package main
import (
"flag"
"log"
"memberDB/api"
"gitea.tecamino.com/paadi/memberDB/api"
)
func main() {

View File

@@ -1,6 +0,0 @@
package models
type Attendees struct {
FirstName int `json:"firstName"`
LastName string `json:"lastName"`
}

View File

@@ -4,6 +4,6 @@ type Event struct {
Id int `json:"id"`
Name string `json:"name"`
Date string `json:"date"`
Attendees []Attendees `json:"attendees"`
Attendees []Person `json:"attendees"`
Count int `json:"count"`
}

7
models/person.go Normal file
View File

@@ -0,0 +1,7 @@
package models
type Person struct {
Id int `json:"id,omitEmpty"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
}