From 75ea1362a814c79d88b48c93c86cf3f526cd1d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Z=C3=BCrcher?= Date: Wed, 8 Oct 2025 15:02:07 +0200 Subject: [PATCH] first commit --- README.MD | 0 api/api.go | 50 ++++ api/apiHandler.go | 81 ++++++ api/attendanceHandler.go | 63 +++++ api/membersHandler.go | 296 ++++++++++++++++++++++ crypto/crypto.go | 62 +++++ data/mydatabase.db | Bin 0 -> 32768 bytes go.mod | 52 ++++ go.sum | 131 ++++++++++ handlers/database.go | 534 +++++++++++++++++++++++++++++++++++++++ main.go | 18 ++ models/attendees.go | 6 + models/database.go | 15 ++ models/event.go | 9 + models/member.go | 17 ++ utils/utils.go | 27 ++ 16 files changed, 1361 insertions(+) create mode 100644 README.MD create mode 100644 api/api.go create mode 100644 api/apiHandler.go create mode 100644 api/attendanceHandler.go create mode 100644 api/membersHandler.go create mode 100644 crypto/crypto.go create mode 100644 data/mydatabase.db create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handlers/database.go create mode 100644 main.go create mode 100644 models/attendees.go create mode 100644 models/database.go create mode 100644 models/event.go create mode 100644 models/member.go create mode 100644 utils/utils.go diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..e69de29 diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..5ad0590 --- /dev/null +++ b/api/api.go @@ -0,0 +1,50 @@ +package api + +import ( + "fmt" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +type API struct { + host string + port int + router *gin.Engine +} + +func NewAPI(host string, port int) *API { + r := gin.Default() + r.Use(cors.New(cors.Config{ + //AllowOrigins: []string{"http://localhost:9000"}, // frontend origin + AllowOrigins: []string{"*"}, // frontend origin + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + + apiHandler := NewAPIHandler() + + v1 := r.Group("v1") + v1.GET("/members", apiHandler.GetMemberById) + v1.GET("/events", apiHandler.GetEventById) + v1.GET("/events/new", apiHandler.StartNewEvent) + + v1.POST("/database/open", apiHandler.OpenDatabase) + v1.POST("/members/add", apiHandler.AddNewMember) + v1.POST("/members/edit", apiHandler.EditMember) + v1.POST("/members/delete", apiHandler.DeleteMember) + v1.POST("/members/import/csv", apiHandler.ImportCSV) + + return &API{host: host, + port: port, + router: r, + } +} + +func (api *API) Run() error { + return api.router.Run(fmt.Sprintf("%s:%d", api.host, api.port)) +} diff --git a/api/apiHandler.go b/api/apiHandler.go new file mode 100644 index 0000000..a69f5cf --- /dev/null +++ b/api/apiHandler.go @@ -0,0 +1,81 @@ +package api + +import ( + "memberDB/handlers" + "memberDB/models" + "net/http" + + "github.com/gin-gonic/gin" +) + +type APIHandler struct { + DbHandler *handlers.DatabaseHandler +} + +func NewAPIHandler() *APIHandler { + return &APIHandler{} +} + +func (a *APIHandler) OpenDatabase(c *gin.Context) { + var database models.Database + var err error + if err := c.BindJSON(&database); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + + if database.Path == "" { + database.SetDefaultPath() + } + + if database.Token == "" { + database.SetDefaultToken() + } + + a.DbHandler, err = handlers.NewDatabaseHandler(database.Path, database.Create) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + + if err := a.DbHandler.CreateNewMemberTable(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + + if err := a.DbHandler.CreateNewAttendanceTable(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + + a.DbHandler.SetToken(database.Token) + + _, err = a.DbHandler.GetMember(0) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "message": "database opened", + }) +} + +func (a *APIHandler) databaseOpened(c *gin.Context) bool { + if a.DbHandler == nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "no database opened", + }) + return false + } + return true +} diff --git a/api/attendanceHandler.go b/api/attendanceHandler.go new file mode 100644 index 0000000..a6a48df --- /dev/null +++ b/api/attendanceHandler.go @@ -0,0 +1,63 @@ +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) +} diff --git a/api/membersHandler.go b/api/membersHandler.go new file mode 100644 index 0000000..d2a36e6 --- /dev/null +++ b/api/membersHandler.go @@ -0,0 +1,296 @@ +package api + +import ( + "encoding/csv" + "fmt" + "io" + "memberDB/models" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/saintfish/chardet" + "golang.org/x/text/encoding/charmap" + "golang.org/x/text/transform" +) + +func (a *APIHandler) AddNewMember(c *gin.Context) { + if !a.databaseOpened(c) { + return + } + + var member models.Member + c.BindJSON(&member) + + var text string + if member.FirstName == "" { + text = "firstName " + } + if member.LastName == "" { + text += "lastName " + } + if member.Birthday == "" { + text += "birthday " + } + if text != "" { + c.JSON(http.StatusBadRequest, gin.H{ + "message": text + "can not be empty", + }) + return + } + + err := a.DbHandler.AddNewMember(member) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "member added", + }) +} + +func (a *APIHandler) GetMemberById(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.GetMember(i) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, members) +} + +func (a *APIHandler) EditMember(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 + } + } else { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "query parameter 'id' missing", + }) + return + } + + var member models.Member + c.BindJSON(&member) + + err = a.DbHandler.UpdateMember(i, member) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "message": "member updated", + }) +} + +func (a *APIHandler) DeleteMember(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.DeleteMember(request.Ids...) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "message": "member deleted", + }) +} + +func (a *APIHandler) ImportCSV(c *gin.Context) { + if !a.databaseOpened(c) { + return + } + var err error + + fileHeader, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + rowIndex := c.PostForm("rowIndex") + comma := c.PostForm("seperator") + firstName := c.PostForm("firstName") + lastName := c.PostForm("lastName") + birthday := c.PostForm("birthday") + address := c.PostForm("address") + town := c.PostForm("town") + zip := c.PostForm("zip") + phone := c.PostForm("phone") + email := c.PostForm("email") + + rowIndexI, err := strconv.Atoi(rowIndex) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": err.Error(), + }) + return + } + + file, err := fileHeader.Open() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open uploaded file"}) + return + } + defer file.Close() + + // Read first N bytes to detect encoding + const sniffSize = 1024 * 10 // 10KB for detection + buf := make([]byte, sniffSize) + n, err := file.Read(buf) + if err != nil && err != io.EOF { + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + + // Detect encoding + detector := chardet.NewTextDetector() + result, err := detector.DetectBest(buf[:n]) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to detect encoding"}) + return + } + + _, err = file.Seek(0, io.SeekStart) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to rewind file"}) + return + } + + // Wrap reader based on encoding + var reader io.Reader + switch result.Charset { + case "UTF-8": + reader = file + case "windows-1252": + reader = transform.NewReader(file, charmap.Windows1252.NewDecoder()) + case "ISO-8859-1": + reader = transform.NewReader(file, charmap.ISO8859_1.NewDecoder()) + default: + c.JSON(http.StatusBadRequest, gin.H{"message": "Unsupported encoding: " + result.Charset}) + return + } + + csvReader := csv.NewReader(reader) + csvReader.Comma = rune(comma[0]) + + records, err := csvReader.ReadAll() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse CSV"}) + return + } + + var firstNameIndex, lastNameIndex, birthdayIndex, addressIndex, townIndex, zipIndex, phoneIndex, emailIndex int + + var message string + for i, row := range records { + if i < rowIndexI { + continue + } else if i == rowIndexI { + for c, column := range row { + switch column { + case firstName: + firstNameIndex = c + case lastName: + lastNameIndex = c + case birthday: + birthdayIndex = c + case address: + addressIndex = c + case town: + townIndex = c + case zip: + zipIndex = c + case phone: + phoneIndex = c + case email: + emailIndex = c + } + } + continue + } + + if row[firstNameIndex] == "" && row[lastNameIndex] == "" { + continue + } + + err := a.DbHandler.AddNewMember(models.Member{ + FirstName: row[firstNameIndex], + LastName: row[lastNameIndex], + Birthday: row[birthdayIndex], + Address: row[addressIndex], + Town: row[townIndex], + Zip: row[zipIndex], + Phone: row[phoneIndex], + Email: row[emailIndex], + }) + + if err != nil { + message += "Row: " + fmt.Sprint(i) + " " + row[firstNameIndex] + " " + row[lastNameIndex] + " error: " + err.Error() + "\n" + } + } + + if message != "" { + c.JSON(http.StatusBadRequest, gin.H{ + "message": message, + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "message": "CSV imported", + }) +} diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 0000000..e35c851 --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,62 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "io" +) + +func Encrypt(text string, key []byte) (string, error) { + plaintext := []byte(text) + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, aesGCM.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + + ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +func Decrypt(encrypted string, key []byte) (string, error) { + data, err := base64.StdEncoding.DecodeString(encrypted) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonceSize := aesGCM.NonceSize() + if len(data) < nonceSize { + return "", fmt.Errorf("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", err + } + + return string(plaintext), nil +} diff --git a/data/mydatabase.db b/data/mydatabase.db new file mode 100644 index 0000000000000000000000000000000000000000..6859bab700b379830b088c19af1d19068b18cb7d GIT binary patch literal 32768 zcmeI*OK;jp8~|`=(xizRQE9cxA*w`gt!NiN#xJdq)(}H@gaEM#mS|05fZJ^%4|ejXxC;64?mXC*gXN9)+3Bo1JhQeJ{T*yAk>B*KhV>e@C{T%8|X_Vt>Yd zk2!nFQ+e;%x+-uS1V8`;KmY_l00ck)1VG@66y3r+i5D*vZuK8YW zJ5y6+Rf(%|x}?N!BjPW2cXn(X&zDstr_|z=TK-tBHRDC48J9KnBwr3I9xG+_br|RB zJrY-yh8i!QgsWC6T^+@GIC)tf)yny*rhHI%-4Y@G-Fjw!g1*f^sXX4=Pb8wvrNF9^PrMNc89v(G`oQ9&!k1T_ zd>z(&^;0#vy?<~J{bja1XOHv@GW1uQM<1MHb?{$dTK3QnF77A2ExTyze*3OnS6^HA zUD2AVhCK}0SfAZJ=}pO+iaN$a;@@*4=l0;j?BjceM$n(QciS1X`|dr#q^H~6>*#w1 z(lyE0_U&M8%(Cn2L&Bl;W^-#_K3F%jR=4qjU1c7>+4z)jW);5q=Fz8w)2r}zbNjvU z2TL^kxY|Mj0T2KI5C8!X009sH0T2Lzhb-{(W%S!fZ?P9Hqs89hF(H?ii|32yvq9I> zPEM*7mhiQsj&~@f3p3Ym7K(F8GSEn&squn;m(*fh_H9EAj$p$W$^#JuX#Rl|Zo3?%WA#Y&>d2#D3KikhLE zq|*-shZ39mI6dXh)68&MM2YOFj|Xix*`nLVVQpq=obH_E9dFR?8d;PHG>sW{*-D#x)k;YY^_wNCI`c4@b+91S59_TIuI1fPMWHHvoTLwtbFL=jY^`?e1tY8I zWyj1ZrPgblpsM+lqU0wdJ29{HI>)V<)^R0su9m3bNHbb3zL91)y;WIb!F_;`=WW4C?pFloOOl#Vbf@cM_nmXXid@?Pd3b~ zb3Q3qd~xigD(0D%v)k=V&+YStMxSPrY`2@w`lVctrD_9BsdwwD#W$?Z>2Z(O@mvpP z>Y&;lsJ*&d@1Bq z`UVj67z9871V8`;KmY_l00ck)1VG?_A+VhPf3D~Mmw*2s`zsPIkU#(gKmY_l00ck) z1V8`;KmY_l00bV8z;1NoU}yCrz~y`Y;9dL== 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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ff73aae --- /dev/null +++ b/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "flag" + "log" + "memberDB/api" +) + +func main() { + port := flag.Int("port", 4040, "server port") + url := flag.String("url", "127.0.0.1", "server url") + flag.Parse() + + a := api.NewAPI(*url, *port) + if err := a.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/models/attendees.go b/models/attendees.go new file mode 100644 index 0000000..9c968b0 --- /dev/null +++ b/models/attendees.go @@ -0,0 +1,6 @@ +package models + +type Attendees struct { + FirstName int `json:"firstName"` + LastName string `json:"lastName"` +} diff --git a/models/database.go b/models/database.go new file mode 100644 index 0000000..305c171 --- /dev/null +++ b/models/database.go @@ -0,0 +1,15 @@ +package models + +type Database struct { + Path string `json:"dbPath,omitempty"` + Token string `json:"token,omitempty"` + Create bool `json:"create,omitempty"` +} + +func (d *Database) SetDefaultPath() { + d.Path = "data/mydatabase.db" +} + +func (d *Database) SetDefaultToken() { + d.Token = "6576889abd578frdj,ouzt6909-gtrfb" +} diff --git a/models/event.go b/models/event.go new file mode 100644 index 0000000..d820126 --- /dev/null +++ b/models/event.go @@ -0,0 +1,9 @@ +package models + +type Event struct { + Id int `json:"id"` + Name string `json:"name"` + Date string `json:"date"` + Attendees []Attendees `json:"attendees"` + Count int `json:"count"` +} diff --git a/models/member.go b/models/member.go new file mode 100644 index 0000000..9739b33 --- /dev/null +++ b/models/member.go @@ -0,0 +1,17 @@ +package models + +type Member struct { + Id int `json:"id,omitempty"` + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + Birthday string `json:"birthday,omitempty"` + Address string `json:"address,omitempty"` + Zip string `json:"zip,omitempty"` + Town string `json:"town,omitempty"` + Phone string `json:"phone,omitempty"` + Email string `json:"email,omitempty"` + FirstVisit string `json:"firstVisit,omitempty"` + LastVisit string `json:"lastVisit,omitempty"` + Group string `json:"group,omitempty"` + ResponsiblePerson string `json:"responsiblePerson,omitempty"` +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..4622b40 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,27 @@ +package utils + +import ( + "net/mail" + "time" +) + +func IsValidEmail(email string) bool { + _, err := mail.ParseAddress(email) + return err == nil +} + +// Try multiple accepted date formats +var birthdayFormats = []string{ + "2006-01-02", // ISO: 1999-12-12 + "02.01.2006", // D.M.Y: 12.12.1999 + "2.1.2006", // D.M.Y without leading zeros: 1.2.1999 +} + +func IsValidBirthday(birthday string) bool { + for _, layout := range birthdayFormats { + if _, err := time.Parse(layout, birthday); err == nil { + return true + } + } + return false +}