diff --git a/.gitignore b/.gitignore
index 69c1c38..c339de6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,10 @@ ml/data/test_images
node_modules
ui/out
__debug_bin*
-api/__debug_bin*
\ No newline at end of file
+api/.env.local
+api/__debug_bin*
+ui/yarn.lock
+api/.env
+ui/.env.local
+ui/.env.local
+*.local
diff --git a/README.md b/README.md
index 281ebce..e798a6b 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,9 @@ The front-end makes use of the [Next.js](https://nextjs.org/) framework. [Storyb
The back-end or API is written in Golang, and defines specific methods to persist data and call the face recognition model. It has various API methods to create new users, assign permissions and access control, and (re)train and interfere with the facial recognition model.
+ - Swagger API is hosted at http://localhost:80/swagger/index.html
+ - Postman Collection [can be found here](https://kerberosio.postman.co/workspace/UUG.AI~52218588-948a-49a0-8046-34f96285bd19/collection/5538769-9f1fc117-a30b-4acf-a7f4-e7ca1ebe5d6f?action=share&creator=5538769)
+
## ML (face recognition)
To be documented
diff --git a/api/.env b/api/.env
deleted file mode 100644
index dc5a84f..0000000
--- a/api/.env
+++ /dev/null
@@ -1,2 +0,0 @@
-EMAIL = "root@email.com"
-PASSWORD = "password"
\ No newline at end of file
diff --git a/api/.vscode/launch.json b/api/.vscode/launch.json
index 4e87f85..ca5ab0e 100644
--- a/api/.vscode/launch.json
+++ b/api/.vscode/launch.json
@@ -4,8 +4,6 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
-
-
{
"name": "Launch Package",
"type": "go",
diff --git a/api/controllers/email.go b/api/controllers/email.go
new file mode 100644
index 0000000..512feea
--- /dev/null
+++ b/api/controllers/email.go
@@ -0,0 +1,85 @@
+package controllers
+
+import (
+ "log"
+ "os"
+
+ "github.com/uug-ai/facial-access-control/api/models"
+ "github.com/uug-ai/facial-access-control/api/notifications"
+)
+
+type Email struct {
+ Type string
+ Mailgun notifications.Mail
+ SMTP notifications.SMTP
+}
+
+func (email Email) Send(m notifications.Message) {
+ if email.Type == "mailgun" {
+ err := email.Mailgun.Send(m) // TODO add error handler
+ if err != nil {
+ log.Println(err)
+ }
+ } else if email.Type == "smtp" {
+ err := email.SMTP.Send(m) // TODO add error handler
+ if err != nil {
+ log.Println(err)
+ }
+ }
+}
+
+func Mail(mailto string, template string) *Email {
+ emailProvider := os.Getenv("MAIL_PROVIDER")
+ if emailProvider == "" {
+ emailProvider = "mailgun"
+ }
+ var email Email
+ if emailProvider == "mailgun" {
+ mailgun := notifications.Mail{
+ Domain: os.Getenv("MAILGUN_DOMAIN"),
+ ApiKey: os.Getenv("MAILGUN_API_KEY"),
+ EmailFrom: os.Getenv("EMAIL_FROM"),
+ EmailTo: mailto,
+ TemplateId: template,
+ }
+ email.Type = "mailgun"
+ email.Mailgun = mailgun
+ } else if emailProvider == "smtp" {
+ smtp := notifications.SMTP{
+ Server: os.Getenv("SMTP_SERVER"),
+ Port: os.Getenv("SMTP_PORT"),
+ Username: os.Getenv("SMTP_USERNAME"),
+ Password: os.Getenv("SMTP_PASSWORD"),
+ EmailFrom: os.Getenv("EMAIL_FROM"),
+ EmailTo: mailto,
+ TemplateId: template,
+ }
+ email.Type = "smtp"
+ email.SMTP = smtp
+ }
+ return &email
+}
+
+func SendWelcomeEmail(user models.User, m notifications.Message) {
+ m.Title = os.Getenv("WELCOME_TITLE")
+ email := Mail(user.Email, os.Getenv("WELCOME_TEMPLATE"))
+ email.Send(m)
+}
+
+func SendShareEmail(user models.User, m notifications.Message) {
+ m.Title = os.Getenv("SHARE_TITLE")
+ email := Mail(user.Email, os.Getenv("SHARE_TEMPLATE"))
+ email.Send(m) // TODO add error handler
+}
+
+func SendActivationEmail(user models.User, m notifications.Message) {
+ m.Title = os.Getenv("ACTIVATE_TITLE")
+ email := Mail(user.Email, os.Getenv("ACTIVATE_TEMPLATE"))
+ email.Send(m) // TODO add error handler
+}
+
+func SendForgotEmail(user models.User, m notifications.Message) {
+ m.Title = os.Getenv("FORGOT_TITLE")
+ email := Mail(user.Email, os.Getenv("FORGOT_TEMPLATE"))
+ email.Send(m) // TODO add error handler
+}
diff --git a/api/controllers/users.go b/api/controllers/users.go
index de2334f..f3230e0 100644
--- a/api/controllers/users.go
+++ b/api/controllers/users.go
@@ -1,11 +1,19 @@
package controllers
import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
"strconv"
+ "time"
"github.com/gin-gonic/gin"
"github.com/uug-ai/facial-access-control/api/database"
+ "github.com/uug-ai/facial-access-control/api/encryption"
"github.com/uug-ai/facial-access-control/api/models"
+ "github.com/uug-ai/facial-access-control/api/notifications"
+ "github.com/uug-ai/facial-access-control/api/utils"
)
// user godoc
@@ -80,6 +88,7 @@ func GetUserByEmail(c *gin.Context) models.User {
})
return user
}
+
// @Router /api/users [post]
// @Security Bearer
// @securityDefinitions.apikey Bearer
@@ -93,9 +102,9 @@ func GetUserByEmail(c *gin.Context) models.User {
// @Produce json
// @Param user body models.User true "User data"
// @Success 201 {object} models.User
-// @Failure 400 {object} gin.H{"error": "Invalid user data"}
-// @Failure 409 {object} gin.H{"error": "User already exists"}
-// @Failure 500 {object} gin.H{"error": "Failed to add user"}
+// @Failure 400
+// @Failure 409
+// @Failure 500
func AddUser(c *gin.Context) {
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
@@ -105,7 +114,7 @@ func AddUser(c *gin.Context) {
return
}
- err := database.AddUser(user)
+ _, err := database.AddUser(user)
if err != nil {
switch err {
case database.ErrUserAlreadyExists:
@@ -126,7 +135,6 @@ func AddUser(c *gin.Context) {
})
}
-
// user godoc
// @Router /api/users/{id} [delete]
// @Security Bearer
@@ -162,3 +170,234 @@ func DeleteUser(c *gin.Context) error {
})
return nil
}
+
+// InviteUser godoc
+// @Router /api/users/invite [post]
+// @Security Bearer
+// @securityDefinitions.apikey Bearer
+// @in header
+// @name Authorization
+// @ID inviteUser
+// @Tags users
+// @Summary Invite
+// @Description Invite user
+// @Success 200
+func InviteUser(c *gin.Context) {
+ var user models.User
+ if err := c.ShouldBindJSON(&user); err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid user data",
+ })
+ return
+ }
+
+ // Add user to the database
+ addedUser, errUser := database.AddUser(user)
+ if errUser != nil {
+ switch errUser {
+ case database.ErrUserAlreadyExists:
+ c.JSON(409, gin.H{
+ "error": "User already exists",
+ })
+ default:
+ c.JSON(500, gin.H{
+ "error": "Failed to add user",
+ })
+ }
+ return
+ }
+
+ // Create fingerprint
+ now := time.Now()
+ fmt.Printf("ID: %v\n", addedUser.Id)
+ fingerprint := models.UserFingerprint{
+ Email: user.Email,
+ FirstName: user.FirstName,
+ LastName: user.LastName,
+ Id: addedUser.Id,
+ Expiration: now.Add(time.Hour * 24 * 7).Unix(), // 1 week (7 days)
+ Creation: now.Unix(),
+ }
+
+ // Serialize fingerprint
+ bufferBytes, err := json.Marshal(fingerprint)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error while encoding fingerprint",
+ })
+ return
+ }
+
+ // Encrypt the fingerprint using the ENV variable PRIVATE_KEY
+ encryptionKey := os.Getenv("PRIVATE_KEY")
+ if encryptionKey == "" {
+ c.JSON(500, gin.H{
+ "error": "No encryption key found",
+ })
+ return
+ }
+
+ encryptedFingerprint, err := encryption.AesEncrypt(bufferBytes, encryptionKey)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error while encrypting fingerprint",
+ })
+ return
+ }
+
+ base64Fingerprint := utils.Base64Encode(string(encryptedFingerprint))
+ fprint := utils.EncodeURL(base64Fingerprint)
+
+ mail := notifications.SMTP{
+ Server: os.Getenv("SMTP_SERVER"),
+ Port: os.Getenv("SMTP_PORT"),
+ Username: os.Getenv("SMTP_USERNAME"),
+ Password: os.Getenv("SMTP_PASSWORD"),
+ EmailFrom: os.Getenv("EMAIL_FROM"),
+ EmailTo: user.Email,
+ TemplateId: "invite",
+ }
+
+ // Get base url
+ baseUrl := os.Getenv("BASE_URL")
+ if baseUrl != "" {
+ baseUrl = utils.RemoveTrailingSlash(baseUrl)
+ }
+
+ message := notifications.Message{
+ Title: "Invitation",
+ Body: "You have been invited to join the Facial Access Control",
+ User: user.Email,
+ Data: map[string]string{
+ "link": baseUrl + "/onboarding?token=" + fprint,
+ "firstname": user.FirstName,
+ },
+ }
+
+ if err := mail.Send(message); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Failed to send invite to user",
+ })
+ return
+ }
+
+ c.JSON(201, gin.H{
+ "message": "User successfully invited",
+ })
+}
+
+// UpdateUser godoc
+// @Router /api/users/{id} [patch]
+// @Security Bearer
+// @securityDefinitions.apikey Bearer
+// @in header
+// @name Authorization
+// @ID updateUser
+// @Tags users
+// @Summary Update user
+// @Description Update user
+// @Param id path int true "User ID"
+// @Accept json
+// @Produce json
+// @Param user body models.User true "User data"
+// @Success 200
+// @Failure 400
+// @Failure 500
+func UpdateUser(c *gin.Context) {
+ id := c.Param("id")
+
+ userID, err := strconv.Atoi(id)
+ if err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid user ID",
+ })
+ return
+ }
+
+ var user models.User
+ if err := c.ShouldBindJSON(&user); err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid user data",
+ })
+ return
+ }
+
+ user.Id = userID
+ if err := database.UpdateUser(user); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Failed to update user",
+ })
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "message": "User updated successfully",
+ })
+}
+
+// OnboardUser godoc
+// @Router /api/users/onboard [post]
+// @Security Bearer
+// @securityDefinitions.apikey Bearer
+// @in header
+// @name Authorization
+// @ID onboardUser
+// @Tags users
+// @Summary Onboard user
+// @Description Onboard user
+// @Accept multipart/form-data
+// @Produce json
+// @Param firstName formData string true "First name"
+func OnboardUser(c *gin.Context) {
+ var user models.User
+
+ // Parse form data
+ if err := c.Request.ParseMultipartForm(10 << 20); err != nil {
+ c.JSON(400, gin.H{"error": "Failed to parse multipart form data"})
+ return
+ }
+
+ // Get fields from form data
+ user.FirstName = c.Request.FormValue("firstName")
+ user.LastName = c.Request.FormValue("lastName")
+ user.Email = c.Request.FormValue("email")
+ user.PhoneNumber = c.Request.FormValue("phoneNumber")
+ user.DateOfBirth = c.Request.FormValue("dateOfBirth")
+ idStr := c.Request.FormValue("id")
+ id, err := strconv.Atoi(idStr)
+ if err != nil {
+ c.JSON(400, gin.H{"error": "Invalid ID"})
+ return
+ }
+ user.Id = id
+
+ // Get video file from form data
+ file, _, err := c.Request.FormFile("video")
+ if err != nil {
+ c.JSON(400, gin.H{"error": "Video file is required"})
+ return
+ }
+ defer file.Close()
+
+ // Read video file into byte slice
+ videoBytes, err := io.ReadAll(file)
+ if err != nil {
+ c.JSON(500, gin.H{"error": "Failed to read video file"})
+ return
+ }
+ user.Video = videoBytes
+
+ // Save user to database (replace with your actual database logic)
+ err = database.OnboardUser(user)
+ if err != nil {
+ switch err {
+ case database.ErrUserAlreadyExists:
+ c.JSON(409, gin.H{"error": "User already exists"})
+ default:
+ c.JSON(500, gin.H{"error": "Failed to add user"})
+ }
+ return
+ }
+
+ c.JSON(201, gin.H{"message": "User onboarded successfully", "user": user})
+}
diff --git a/api/data/main.go b/api/data/main.go
index baa44f8..04ffae9 100644
--- a/api/data/main.go
+++ b/api/data/main.go
@@ -8,34 +8,33 @@ import (
)
var Users = []models.User{
- {Id: 0, FirstName: "admin", LastName: "admin", Email: "admin@example.com", Password: "admin", Role: "admin", Language: "en"},
- {Id: 1, FirstName: "user", LastName: "user", Email: "user@example.com", Password: "user", Role: "user", Language: "en"},
- {Id: 2, FirstName: "Kilian", LastName: "Smith", Email: "kilian@example.com", Password: "Kilian", Role: "admin", Language: "en"},
- {Id: 3, FirstName: "Cedric", LastName: "Johnson", Email: "cedric@example.com", Password: "Cedric", Role: "admin", Language: "en"},
- {Id: 4, FirstName: "Johann", LastName: "Brown", Email: "johann@example.com", Password: "Johann", Role: "admin", Language: "en"},
- {Id: 5, FirstName: "Romain", LastName: "Davis", Email: "romain@example.com", Password: "Romain", Role: "admin", Language: "en"},
- {Id: 6, FirstName: "Alex", LastName: "Wilson", Email: "alex@example.com", Password: "Alex", Role: "admin", Language: "en"},
- {Id: 7, FirstName: "Mickael", LastName: "Taylor", Email: "mickael@example.com", Password: "Mickael", Role: "admin", Language: "en"},
- {Id: 8, FirstName: "Mickael", LastName: "Thomas", Email: "mickael1@example.com", Password: "Mickael", Role: "admin", Language: "en"},
- {Id: 9, FirstName: "Mickael", LastName: "Robinson", Email: "mickael2@example.com", Password: "Mickael", Role: "admin", Language: "en"},
- {Id: 10, FirstName: "Mickael", LastName: "Clark", Email: "mickael3@example.com", Password: "Mickael", Role: "admin", Language: "en"},
+ {Id: 0, FirstName: "admin", LastName: "admin", Email: "admin@example.com", Password: "admin", Role: "admin", Language: "en", Status: "onboarded"},
+ {Id: 1, FirstName: "user", LastName: "user", Email: "user@example.com", Password: "user", Role: "user", Language: "en", Status: "onboarded"},
+ {Id: 2, FirstName: "Kilian", LastName: "Smith", Email: "kilian@example.com", Password: "Kilian", Role: "admin", Language: "en", Status: "onboarded"},
+ {Id: 3, FirstName: "Cedric", LastName: "Johnson", Email: "cedric@example.com", Password: "Cedric", Role: "admin", Language: "en", Status: "onboarded"},
+ {Id: 4, FirstName: "Johann", LastName: "Brown", Email: "johann@example.com", Password: "Johann", Role: "admin", Language: "en", Status: "onboarded"},
+ {Id: 5, FirstName: "Romain", LastName: "Davis", Email: "romain@example.com", Password: "Romain", Role: "admin", Language: "en", Status: "onboarded"},
+ {Id: 6, FirstName: "Alex", LastName: "Wilson", Email: "alex@example.com", Password: "Alex", Role: "admin", Language: "en", Status: "onboarded"},
+ {Id: 7, FirstName: "Mickael", LastName: "Taylor", Email: "mickael@example.com", Password: "Mickael", Role: "admin", Language: "en", Status: "invited"},
+ {Id: 8, FirstName: "Luis", LastName: "Thomas", Email: "mickael1@example.com", Password: "Mickael", Role: "admin", Language: "en", Status: "invited"},
+ {Id: 9, FirstName: "Glenn", LastName: "Robinson", Email: "mickael2@example.com", Password: "Mickael", Role: "admin", Language: "en", Status: "invited"},
+ {Id: 10, FirstName: "Mickael", LastName: "Clark", Email: "mickael3@example.com", Password: "Mickael", Role: "admin", Language: "en", Status: "invited"},
}
var Locations = []models.Location{
- {Id: 1, Name: "Location 1", Address: "Address 1", Lat: 1.0, Lng: 1.0},
- {Id: 2, Name: "Location 2", Address: "Address 2", Lat: 2.0, Lng: 2.0},
- {Id: 3, Name: "Location 3", Address: "Address 3", Lat: 3.0, Lng: 3.0},
- {Id: 4, Name: "Location 4", Address: "Address 4", Lat: 4.0, Lng: 4.0},
+ {Id: 1, Name: "Location 1", Address: "Address 1", Lat: 1.0, Lng: 1.0},
+ {Id: 2, Name: "Location 2", Address: "Address 2", Lat: 2.0, Lng: 2.0},
+ {Id: 3, Name: "Location 3", Address: "Address 3", Lat: 3.0, Lng: 3.0},
+ {Id: 4, Name: "Location 4", Address: "Address 4", Lat: 4.0, Lng: 4.0},
}
// Initialize function to hash passwords
func Initialize() {
- for i, user := range Users {
- hashedPassword, err := utils.Hash(user.Password)
- if err != nil {
- log.Fatalf("Error hashing password for user %s: %v", user.Email, err)
- }
- Users[i].Password = hashedPassword
- }
+ for i, user := range Users {
+ hashedPassword, err := utils.Hash(user.Password)
+ if err != nil {
+ log.Fatalf("Error hashing password for user %s: %v", user.Email, err)
+ }
+ Users[i].Password = hashedPassword
+ }
}
-
diff --git a/api/database/file.go b/api/database/file.go
index d9455db..6d44dd8 100644
--- a/api/database/file.go
+++ b/api/database/file.go
@@ -9,73 +9,94 @@ import (
)
var ErrUserAlreadyExists = errors.New("user already exists")
-
+var ErrInvalidUserData = errors.New("invalid user data")
func GetUsersFromFile() []models.User {
- // Directly return users from data without re-hashing passwords
- return data.Users
+ // Directly return users from data without re-hashing passwords
+ return data.Users
}
func GetUserByIdFromFile(id int) models.User {
- users := GetUsersFromFile()
- for _, user := range users {
- if user.Id == id {
- return user
- }
- }
- return models.User{}
+ users := GetUsersFromFile()
+ for _, user := range users {
+ if user.Id == id {
+ return user
+ }
+ }
+ return models.User{}
}
func GetUserByEmailFromFile(email string) models.User {
- users := GetUsersFromFile()
- for _, user := range users {
- if user.Email == email {
- return user
- }
- }
- return models.User{}
+ users := GetUsersFromFile()
+ for _, user := range users {
+ if user.Email == email {
+ return user
+ }
+ }
+ return models.User{}
}
+func AddUserToFile(user models.User) (models.User, error) {
+ users := GetUsersFromFile()
-func AddUserToFile(user models.User) error {
- users := GetUsersFromFile()
-
- // Find the maximum ID in the current user list
- maxID := 0
- for _, u := range users {
+ // Find the maximum ID in the current user list
+ maxID := 0
+ for _, u := range users {
if u.Email == user.Email {
- return ErrUserAlreadyExists
+ return models.User{}, ErrUserAlreadyExists
+ }
+ if u.Id > maxID {
+ maxID = u.Id
}
- if u.Id > maxID {
- maxID = u.Id
- }
- }
-
- // Assign the new user an ID that is one greater than the current maximum
- user.Id = maxID + 1
-
- // Hash the user's password before saving
- hashedPassword, err := utils.Hash(user.Password)
- if err != nil {
- return err
- }
- user.Password = hashedPassword
-
- data.Users = append(data.Users, user)
- return nil
+ }
+
+ // Assign the new user an ID that is one greater than the current maximum
+ user.Id = maxID + 1
+
+ // Hash the user's password before saving
+ hashedPassword, err := utils.Hash(user.Password)
+ if err != nil {
+ return models.User{}, err
+ }
+ user.Password = hashedPassword
+
+ data.Users = append(data.Users, user)
+ return user, nil
}
func DeleteUserFromFile(id int) error {
- users := GetUsersFromFile()
- for i, user := range users {
- if user.Id == id {
- data.Users = append(users[:i], users[i+1:]...)
- return nil
- }
- }
- return errors.New("user not found")
+ users := GetUsersFromFile()
+ for i, user := range users {
+ if user.Id == id {
+ data.Users = append(users[:i], users[i+1:]...)
+ return nil
+ }
+ }
+ return errors.New("user not found")
}
+func UpdateUserFromFile(user models.User) error {
+ users := GetUsersFromFile()
+ for i, u := range users {
+ if u.Id == user.Id {
+ users[i] = user
+ return nil
+ }
+ }
+ return errors.New("user not found")
+}
+
+func OnboardUserToFile(user models.User) error {
+ users := GetUsersFromFile()
+ for i, u := range users {
+ if u.Email == user.Email {
+ user.Status = "onboarded"
+ users[i] = user
+ return nil
+ }
+ }
+ return errors.New("user not found")
+}
func GetLocationsFromFile() []models.Location {
locations := data.Locations
@@ -95,19 +116,19 @@ func GetLocationFromFile(id int) models.Location {
func AddLocationToFile(location models.Location) error {
locations := data.Locations
- // Find the maximum ID in the current user list
- maxID := 0
- for _, location := range locations {
- if location.Id > maxID {
- maxID = location.Id
- }
- }
+ // Find the maximum ID in the current user list
+ maxID := 0
+ for _, location := range locations {
+ if location.Id > maxID {
+ maxID = location.Id
+ }
+ }
- // Assign the new user an ID that is one greater than the current maximum
- location.Id = maxID + 1
+ // Assign the new user an ID that is one greater than the current maximum
+ location.Id = maxID + 1
- data.Locations = append(data.Locations, location)
- return nil
+ data.Locations = append(data.Locations, location)
+ return nil
}
func DeleteLocationFromFile(id int) error {
@@ -119,4 +140,4 @@ func DeleteLocationFromFile(id int) error {
}
}
return errors.New("location not found")
-}
\ No newline at end of file
+}
diff --git a/api/database/main.go b/api/database/main.go
index 3d0e3f2..b8c1040 100644
--- a/api/database/main.go
+++ b/api/database/main.go
@@ -13,12 +13,18 @@ func GetUserById(id int) models.User {
func GetUserByEmail(email string) models.User {
return GetUserByEmailFromFile(email)
}
-func AddUser(user models.User) error {
+func AddUser(user models.User) (models.User, error) {
return AddUserToFile(user)
}
func DeleteUser(id int) error {
return DeleteUserFromFile(id)
}
+func UpdateUser(user models.User) error {
+ return UpdateUserFromFile(user)
+}
+func OnboardUser(user models.User) error {
+ return OnboardUserToFile(user)
+}
func GetLocations() []models.Location {
return GetLocationsFromFile()
}
diff --git a/api/docs/docs.go b/api/docs/docs.go
index 7f2d9c4..0e93ece 100644
--- a/api/docs/docs.go
+++ b/api/docs/docs.go
@@ -236,6 +236,35 @@ const docTemplate = `{
"schema": {
"$ref": "#/definitions/models.User"
}
+ },
+ "400": {
+ "description": "Bad Request"
+ },
+ "409": {
+ "description": "Conflict"
+ },
+ "500": {
+ "description": "Internal Server Error"
+ }
+ }
+ }
+ },
+ "/api/users/invite": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Invite user",
+ "tags": [
+ "users"
+ ],
+ "summary": "Invite",
+ "operationId": "inviteUser",
+ "responses": {
+ "200": {
+ "description": "OK"
}
}
}
diff --git a/api/docs/swagger.json b/api/docs/swagger.json
index e64a2aa..cc65620 100644
--- a/api/docs/swagger.json
+++ b/api/docs/swagger.json
@@ -229,6 +229,35 @@
"schema": {
"$ref": "#/definitions/models.User"
}
+ },
+ "400": {
+ "description": "Bad Request"
+ },
+ "409": {
+ "description": "Conflict"
+ },
+ "500": {
+ "description": "Internal Server Error"
+ }
+ }
+ }
+ },
+ "/api/users/invite": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Invite user",
+ "tags": [
+ "users"
+ ],
+ "summary": "Invite",
+ "operationId": "inviteUser",
+ "responses": {
+ "200": {
+ "description": "OK"
}
}
}
diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml
index a698b1a..ac8b430 100644
--- a/api/docs/swagger.yaml
+++ b/api/docs/swagger.yaml
@@ -194,6 +194,12 @@ paths:
description: Created
schema:
$ref: '#/definitions/models.User'
+ "400":
+ description: Bad Request
+ "409":
+ description: Conflict
+ "500":
+ description: Internal Server Error
security:
- Bearer: []
summary: Add user
@@ -256,6 +262,18 @@ paths:
summary: Get user
tags:
- users
+ /api/users/invite:
+ post:
+ description: Invite user
+ operationId: inviteUser
+ responses:
+ "200":
+ description: OK
+ security:
+ - Bearer: []
+ summary: Invite
+ tags:
+ - users
securityDefinitions:
Bearer:
in: header
diff --git a/api/encryption/main.go b/api/encryption/main.go
new file mode 100644
index 0000000..9c1f995
--- /dev/null
+++ b/api/encryption/main.go
@@ -0,0 +1,126 @@
+package encryption
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/md5"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "encoding/base64"
+ "errors"
+ "hash"
+)
+
+func DecryptWithPrivateKey(ciphertext string, privateKey *rsa.PrivateKey) ([]byte, error) {
+ cipheredValue, _ := base64.StdEncoding.DecodeString(ciphertext)
+ out, err := rsa.DecryptPKCS1v15(nil, privateKey, cipheredValue)
+ return out, err
+}
+
+func SignWithPrivateKey(data []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
+ hashed := sha256.Sum256(data)
+ signature, err := rsa.SignPKCS1v15(nil, privateKey, crypto.SHA256, hashed[:])
+ return signature, err
+}
+
+func AesEncrypt(content []byte, password string) ([]byte, error) {
+ salt := make([]byte, 8)
+ _, err := rand.Read(salt)
+ if err != nil {
+ return nil, err
+ }
+ key, iv, err := DefaultEvpKDF([]byte(password), salt)
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ mode := cipher.NewCBCEncrypter(block, iv)
+ cipherBytes := PKCS5Padding(content, aes.BlockSize)
+ mode.CryptBlocks(cipherBytes, cipherBytes)
+
+ cipherText := make([]byte, 16+len(cipherBytes))
+ copy(cipherText[:8], []byte("Salted__"))
+ copy(cipherText[8:16], salt)
+ copy(cipherText[16:], cipherBytes)
+ return cipherText, nil
+}
+
+func AesDecrypt(cipherText []byte, password string) ([]byte, error) {
+ if string(cipherText[:8]) != "Salted__" {
+ return nil, errors.New("invalid crypto js aes encryption")
+ }
+
+ salt := cipherText[8:16]
+ cipherBytes := cipherText[16:]
+ key, iv, err := DefaultEvpKDF([]byte(password), salt)
+ if err != nil {
+ return nil, err
+ }
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ mode := cipher.NewCBCDecrypter(block, iv)
+ mode.CryptBlocks(cipherBytes, cipherBytes)
+
+ result := PKCS5UnPadding(cipherBytes)
+ return result, nil
+}
+
+func EvpKDF(password []byte, salt []byte, keySize int, iterations int, hashAlgorithm string) ([]byte, error) {
+ var block []byte
+ var hasher hash.Hash
+ derivedKeyBytes := make([]byte, 0)
+ switch hashAlgorithm {
+ case "md5":
+ hasher = md5.New()
+ default:
+ return []byte{}, errors.New("not implement hasher algorithm")
+ }
+ for len(derivedKeyBytes) < keySize*4 {
+ if len(block) > 0 {
+ hasher.Write(block)
+ }
+ hasher.Write(password)
+ hasher.Write(salt)
+ block = hasher.Sum([]byte{})
+ hasher.Reset()
+
+ for i := 1; i < iterations; i++ {
+ hasher.Write(block)
+ block = hasher.Sum([]byte{})
+ hasher.Reset()
+ }
+ derivedKeyBytes = append(derivedKeyBytes, block...)
+ }
+ return derivedKeyBytes[:keySize*4], nil
+}
+
+func DefaultEvpKDF(password []byte, salt []byte) (key []byte, iv []byte, err error) {
+ keySize := 256 / 32
+ ivSize := 128 / 32
+ derivedKeyBytes, err := EvpKDF(password, salt, keySize+ivSize, 1, "md5")
+ if err != nil {
+ return []byte{}, []byte{}, err
+ }
+ return derivedKeyBytes[:keySize*4], derivedKeyBytes[keySize*4:], nil
+}
+
+func PKCS5UnPadding(src []byte) []byte {
+ length := len(src)
+ unpadding := int(src[length-1])
+ return src[:(length - unpadding)]
+}
+
+func PKCS5Padding(src []byte, blockSize int) []byte {
+ padding := blockSize - len(src)%blockSize
+ padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+ return append(src, padtext...)
+}
diff --git a/api/go.mod b/api/go.mod
index a818dc8..0111898 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -1,53 +1,62 @@
module github.com/uug-ai/facial-access-control/api
-go 1.22.1
+go 1.22.2
require (
github.com/appleboy/gin-jwt/v2 v2.9.2
- github.com/gin-contrib/cors v1.7.1
- github.com/gin-contrib/pprof v1.4.0
- github.com/gin-gonic/gin v1.9.1
+ github.com/gin-contrib/cors v1.7.2
+ github.com/gin-contrib/pprof v1.5.0
+ github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v4 v4.5.0
+ github.com/mailgun/mailgun-go/v4 v4.12.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/sirupsen/logrus v1.9.3
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.3
+ golang.org/x/crypto v0.23.0
+ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
- github.com/bytedance/sonic v1.11.3 // indirect
- github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
- github.com/chenzhuoyu/iasm v0.9.1 // indirect
+ github.com/PuerkitoBio/purell v1.1.1 // indirect
+ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
+ github.com/bytedance/sonic v1.11.6 // indirect
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
- github.com/go-openapi/jsonpointer v0.21.0 // indirect
- github.com/go-openapi/jsonreference v0.21.0 // indirect
- github.com/go-openapi/spec v0.21.0 // indirect
- github.com/go-openapi/swag v0.23.0 // indirect
+ github.com/go-chi/chi/v5 v5.0.8 // indirect
+ github.com/go-openapi/jsonpointer v0.19.5 // indirect
+ github.com/go-openapi/jsonreference v0.19.6 // indirect
+ github.com/go-openapi/spec v0.20.4 // indirect
+ github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
- github.com/go-playground/validator/v10 v10.19.0 // indirect
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
- github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
- github.com/pelletier/go-toml/v2 v2.2.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+ github.com/pkg/errors v0.8.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
- golang.org/x/arch v0.7.0 // indirect
- golang.org/x/crypto v0.23.0 // direct
+ golang.org/x/arch v0.8.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
- golang.org/x/tools v0.21.0 // indirect
- google.golang.org/protobuf v1.33.0 // indirect
+ golang.org/x/tools v0.7.0 // indirect
+ google.golang.org/protobuf v1.34.1 // indirect
+ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/api/go.sum b/api/go.sum
index 06d8e12..37378bb 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -1,69 +1,73 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/appleboy/gin-jwt/v2 v2.9.2 h1:GeS3lm9mb9HMmj7+GNjYUtpp3V1DAQ1TkUFa5poiZ7Y=
github.com/appleboy/gin-jwt/v2 v2.9.2/go.mod h1:mxGjKt9Lrx9Xusy1SrnmsCJMZG6UJwmdHN9bN27/QDw=
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
-github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
-github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
-github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
-github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
-github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
-github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
-github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
-github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
-github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
-github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
-github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
+github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
+github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
+github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
+github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
+github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
-github.com/gin-contrib/cors v1.7.1 h1:s9SIppU/rk8enVvkzwiC2VK3UZ/0NNGsWfUKvV55rqs=
-github.com/gin-contrib/cors v1.7.1/go.mod h1:n/Zj7B4xyrgk/cX1WCX2dkzFfaNm/xJb6oIUk7WTtps=
+github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
+github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
-github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
-github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
+github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYVrU=
+github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
-github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
-github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
-github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
-github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
-github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
-github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
-github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
-github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
-github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
-github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
-github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
-github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
+github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
+github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
+github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
+github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
-github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
-github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
-github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
-github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -71,39 +75,39 @@ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuV
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mailgun/mailgun-go/v4 v4.12.0 h1:TtuQCgqSp4cB6swPxP5VF/u4JeeBIAjTdpuQ+4Usd/w=
+github.com/mailgun/mailgun-go/v4 v4.12.0/go.mod h1:L9s941Lgk7iB3TgywTPz074pK2Ekkg4kgbnAaAyJ2z8=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
-github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
-github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
-github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
-github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -133,37 +137,32 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
-github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
-github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
-golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -184,24 +183,29 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
-golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
+golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
diff --git a/api/models/user.go b/api/models/user.go
index 8ab94e3..e6d3417 100644
--- a/api/models/user.go
+++ b/api/models/user.go
@@ -1,25 +1,37 @@
package models
type User struct {
- Id int `json:"id" bson:"id"`
- FirstName string `json:"firstname" bson:"firstname"`
- LastName string `json:"lastname" bson:"lastname"`
- Email string `json:"email" bson:"email"`
- Password string `json:"password" bson:"password"`
- Role string `json:"role" bson:"role"`
- Language string `json:"language" bson:"language"`
-
+ Id int `json:"id" bson:"id"`
+ FirstName string `json:"firstname" bson:"firstname"`
+ LastName string `json:"lastname" bson:"lastname"`
+ Email string `json:"email" bson:"email"`
+ Password string `json:"password" bson:"password"`
+ Role string `json:"role" bson:"role"`
+ Language string `json:"language" bson:"language"`
+ Status string `json:"status" bson:"status"`
+ Video []byte `json:"video" bson:"video"`
+ PhoneNumber string `json:"phoneNumber" bson:"phoneNumber"`
+ DateOfBirth string `json:"dateOfBirth" bson:"dateOfBirth"`
}
type Authentication struct {
- Email string `json:"email" bson:"email"`
+ Email string `json:"email" bson:"email"`
Password string `json:"password" bson:"password"`
}
type Authorization struct {
- Code int `json:"code" bson:"code"`
- Token string `json:"token" bson:"token"`
- Expire string `json:"expire" bson:"expire"`
- Email string `json:"email" bson:"email"`
- Role string `json:"role" bson:"role"`
+ Code int `json:"code" bson:"code"`
+ Token string `json:"token" bson:"token"`
+ Expire string `json:"expire" bson:"expire"`
+ Email string `json:"email" bson:"email"`
+ Role string `json:"role" bson:"role"`
+}
+
+type UserFingerprint struct {
+ Id int `json:"id" bson:"id"`
+ Email string `json:"email" bson:"email"`
+ FirstName string `json:"firstname" bson:"firstname"`
+ LastName string `json:"lastname" bson:"lastname"`
+ Expiration int64 `json:"expiration" bson:"expiration"`
+ Creation int64 `json:"creation" bson:"creation"`
}
diff --git a/api/notifications/mail.go b/api/notifications/mail.go
new file mode 100644
index 0000000..b10c907
--- /dev/null
+++ b/api/notifications/mail.go
@@ -0,0 +1,51 @@
+package notifications
+
+import (
+ "context"
+ "time"
+
+ mailgun "github.com/mailgun/mailgun-go/v4"
+)
+
+type Mail struct {
+ Domain string `json:"domain,omitempty"`
+ ApiKey string `json:"api_key,omitempty"`
+ TemplateId string `json:"templateId,omitempty"`
+ EmailTo string `json:"email_to,omitempty"`
+ EmailFrom string `json:"email_from,omitempty"`
+}
+
+func (mail Mail) Send(message Message) error {
+
+ domain := mail.Domain
+ ApiKey := mail.ApiKey
+
+ mg := mailgun.NewMailgun(domain, ApiKey)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+ defer cancel()
+
+ msg := mg.NewMessage(mail.EmailFrom,
+ message.Title,
+ "")
+ msg.SetTemplate(mail.TemplateId)
+
+ // Add recipients
+ if mail.EmailTo != "" {
+ msg.AddRecipient(mail.EmailTo)
+ } else {
+ msg.AddRecipient(message.Email)
+ }
+
+ // Add the variables to be used by the template
+ msg.AddVariable("user", message.User)
+ msg.AddVariable("text", message.Body)
+
+ // Iterate over data object and modify
+ for key, element := range message.Data {
+ msg.AddVariable(key, element)
+ }
+
+ _, _, err := mg.Send(ctx, msg)
+
+ return err
+}
diff --git a/api/notifications/message.go b/api/notifications/message.go
new file mode 100644
index 0000000..2a32085
--- /dev/null
+++ b/api/notifications/message.go
@@ -0,0 +1,18 @@
+package notifications
+
+type Message struct {
+ Type string `json:"type,omitempty" bson:"type,omitempty"`
+ Id string `json:"id,omitempty" bson:"id,omitempty"`
+ Timestamp int64 `json:"timestamp,omitempty" bson:"timestamp,omitempty"`
+ NotificationType string `json:"notification_type,omitempty" bson:"notification_type,omitempty"` // generic, counting, region
+ Title string `json:"title,omitempty" bson:"title,omitempty"`
+ Body string `json:"body,omitempty" bson:"body,omitempty"`
+ Unread bool `json:"unread,omitempty" bson:"unread,omitempty"`
+ User string `json:"user,omitempty" bson:"user,omitempty"`
+ UserId string `json:"userid,omitempty" bson:"userid,omitempty"`
+ Timezone string `json:"timezone,omitempty" bson:"timezone,omitempty"`
+ Email string `json:"email,omitempty" bson:"email,omitempty"`
+ DataUsage string `json:"data_usage,omitempty" bson:"data_usage,omitempty"`
+ Data map[string]string `json:"data,omitempty" bson:"data,omitempty"`
+ Fingerprint string `json:"fingerprint,omitempty" bson:"fingerprint,omitempty"`
+}
diff --git a/api/notifications/smtp.go b/api/notifications/smtp.go
new file mode 100644
index 0000000..741ea4f
--- /dev/null
+++ b/api/notifications/smtp.go
@@ -0,0 +1,98 @@
+package notifications
+
+import (
+ "crypto/tls"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/uug-ai/facial-access-control/api/templates"
+ "gopkg.in/gomail.v2"
+)
+
+type SMTP struct {
+ Server string `json:"server,omitempty"`
+ Port string `json:"port,omitempty"`
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+ EmailFrom string `json:"email_from,omitempty"`
+ EmailTo string `json:"email_to,omitempty"`
+ TemplateId string `json:"template_id,omitempty"`
+}
+
+func (smtp SMTP) Send(message Message) error {
+ port, _ := strconv.Atoi(smtp.Port)
+ d := gomail.NewDialer(smtp.Server, port, smtp.Username, smtp.Password)
+ d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
+
+ m := gomail.NewMessage()
+ m.SetHeader("From", smtp.EmailFrom)
+ m.SetHeader("To", smtp.EmailTo)
+ m.SetHeader("Subject", message.Title)
+ timeNow := time.Now().Unix()
+ m.SetHeader("Message-Id", "<"+strconv.FormatInt(timeNow, 10)+"@uug.ai>")
+
+ textBody := templates.GetTextTemplate(smtp.TemplateId)
+ m.SetBody("text/plain", ReplaceValues(textBody, message))
+
+ body := templates.GetTemplate(smtp.TemplateId)
+ m.AddAlternative("text/html", ReplaceValues(body, message))
+
+ err := d.DialAndSend(m)
+ fmt.Println(err)
+ return err
+}
+
+// This function will replace the variables in the email template. We have following variables available:
+// - {{user}}: user that triggered the message
+// - {{text}}: text of the message
+// - {{link}}: link to the invite page
+
+func ReplaceValues(body string, message Message) string {
+
+ body = strings.ReplaceAll(body, "{{tab1_title}}", "")
+ body = strings.ReplaceAll(body, "{{tab2_title}}", "")
+
+ // Add the variables to be used by the template
+ //body = strings.ReplaceAll(body, "{{user}}", "")
+ if message.User != "" {
+ body = strings.ReplaceAll(body, "{{user}}", message.User)
+ } else {
+ body = strings.ReplaceAll(body, "{{user}}", message.Data["user"])
+ }
+
+ body = strings.ReplaceAll(body, "{{text}}", message.Body)
+
+ if message.Data["link"] != "" {
+ body = strings.ReplaceAll(body, "{{link}}", message.Data["link"])
+ }
+
+ // {{eventtime}} of the notification
+ if message.Timestamp > 0 {
+ t := time.Unix(message.Timestamp, 0)
+ // Get time with timezone
+ if message.Timezone != "" {
+ loc, _ := time.LoadLocation(message.Timezone)
+ t = t.In(loc)
+ }
+ body = strings.ReplaceAll(body, "{{eventdate}}", t.Format("2006-01-02"))
+ body = strings.ReplaceAll(body, "{{eventtime}}", t.Format("15:04:05"))
+ body = strings.ReplaceAll(body, "{{eventdatetime}}", t.Format("2006-01-02 15:04:05"))
+ }
+
+ // {{timezone}} of the account generating the event
+ if message.Timezone != "" {
+ body = strings.ReplaceAll(body, "{{timezone}}", message.Timezone)
+ }
+
+ // Whipe out all variables with the {{variable}} syntax (regex: {{.*}})
+ body = strings.ReplaceAll(body, "{{.*}}", "")
+
+ // Iterate over data object and modify
+ for key, element := range message.Data {
+ body = strings.ReplaceAll(body, "{{"+key+"}}", element)
+ }
+
+ return body
+}
diff --git a/api/routers/http/routes.go b/api/routers/http/routes.go
index 4768928..22fd4c7 100644
--- a/api/routers/http/routes.go
+++ b/api/routers/http/routes.go
@@ -12,6 +12,10 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) *gin.RouterG
{
api.POST("/login", authMiddleware.LoginHandler)
+ api.POST("/users/onboard", func(c *gin.Context) {
+ controllers.OnboardUser(c)
+ })
+
// Secured endpoints..
api.Use(authMiddleware.MiddlewareFunc())
{
@@ -34,10 +38,17 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) *gin.RouterG
controllers.AddUser(c)
})
+ api.POST("/users/invite", func(c *gin.Context) {
+ controllers.InviteUser(c)
+ })
+
api.DELETE("/users/:id", func(c *gin.Context) {
controllers.DeleteUser(c)
})
- // End users
+
+ api.PATCH("/users/:id", func(c *gin.Context) {
+ controllers.UpdateUser(c)
+ })
// Locations
api.GET("/locations", func(c *gin.Context) {
diff --git a/api/templates/activate.go b/api/templates/activate.go
new file mode 100644
index 0000000..c65185e
--- /dev/null
+++ b/api/templates/activate.go
@@ -0,0 +1,430 @@
+package templates
+
+var TEMPLATE_ACTIVATE_TEXT = `
+Uug.ai
+------------
+
+Hey, {{user}}
+Welcome to Facial Access Controll
+Activate your account
+{{link}}
+
+However before you get started this amazing applications, please activate your account.
+
+Get in touch
+------------
+support@uug.ai
+Kerkstraat 108, 9050 Ghent, Belgium
+https://uug.ai
+
+About Uug.ai
+------------
+Welcome to the revolutionary video analytics and video management platform. Open, modular, and extensible for everyone, anywhere.
+`
+
+var TEMPLATE_ACTIVATE = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+Hey, {{user}}
+Your account is now active
+ |
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+All set. Let's go!
+You're ready. Go ahead and login to your Kerberos Hub account. Start exploring the different features and functions to support your business usecases.
+Once you are convinced, choose the subscription plan that you like the most, and start consuming only what you need.
+ |
+ |
+
+
+
+ |
+
+
+
+
+`
diff --git a/api/templates/forgot.go b/api/templates/forgot.go
new file mode 100644
index 0000000..fa8cdfc
--- /dev/null
+++ b/api/templates/forgot.go
@@ -0,0 +1,440 @@
+package templates
+
+var TEMPLATE_FORGOT_TEXT = `
+Kerberos.io
+Uug.ai
+------------
+
+Hey, {{user}}
+Welcome to Facial Access Controll
+Forgot your password?
+{{link}}
+
+However before you get started this amazing applications, please activate your account.
+
+Get in touch
+------------
+support@uug.ai
+KErkstraat 108, 9050 Ghent, Belgium
+https://uug.ai
+
+About Uug.ai
+------------
+Welcome to the revolutionary video analytics and video management platform. Open, modular, and extensible for everyone, anywhere.
+`
+
+var TEMPLATE_FORGOT = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ Hey, {{user}}
+ You forgot your Kerberos Hub password!
+
+ Your new password: {{password}}
+
+ |
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+ We've created a new password for you
+ Someone, we hope it was you ({{ipaddress}}), requested a new password for your account "{{user}}". Therefore we've created a new password to access your account.
+ Your existing password can still be used, until you signed in with the password below.
+ Afterwards your original password is overwritten with the attached password.
+
+
+
+
+ |
+ |
+
+
+
+ |
+
+
+
+
+
+`
diff --git a/api/templates/invite.go b/api/templates/invite.go
new file mode 100644
index 0000000..f9ab128
--- /dev/null
+++ b/api/templates/invite.go
@@ -0,0 +1,350 @@
+package templates
+
+var TEMPLATE_INVITE_TEXT = `
+Uug.ai
+------------
+
+Hey, {{user}}
+Welcome to Facial Access Controll!
+
+However before you get started this amazing applications, please activate your account.
+
+Get in touch
+------------
+support@uug.ai
+KErkstraat 108, 9050 Ghent, Belgium
+https://uug.ai
+
+About Uug.ai
+------------
+Welcome to the revolutionary video analytics and video management platform. Open, modular, and extensible for everyone, anywhere.
+`
+
+var TEMPLATE_INVITE = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+ Welcome to UUG.ai
+ You can sign up with the button down below!
+ Hey, {{firstname}}
+ Welcome to UUG.AI!
+
+
+
+ We are thrilled to have you join our community. At UUG.AI,
+ we strive to create a secure and innovative environment, and our facial access control system is a key part of this commitment.
+ This advanced technology not only enhances the security of our premises but also provides a seamless and convenient access experience for our employees and visitors.
+
+
+ Activate your account ->
+
+ At UUG.AI, we are committed to fostering a secure and innovative workplace. Our facial access control system is just one example of how we leverage cutting-edge technology to create a better environment for our team and visitors.
+
+ We look forward to your contributions and to working together towards our common goals. If you have any questions or need assistance, please do not hesitate to reach out to our support team at [support email/phone number].
+
+ Welcome aboard, and here’s to a successful journey ahead!
+ |
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+`
diff --git a/api/templates/main.go b/api/templates/main.go
new file mode 100644
index 0000000..513da19
--- /dev/null
+++ b/api/templates/main.go
@@ -0,0 +1,53 @@
+package templates
+
+import (
+ "os"
+)
+
+func HasTemplateOnFileSystem(path string, template string) bool {
+ if _, err := os.Stat(path + "/" + template); err == nil {
+ return true
+ } else {
+ return false
+ }
+}
+
+func Readfile(path string, template string) string {
+ content, err := os.ReadFile(path + "/" + template)
+ if err != nil {
+ return err.Error()
+ }
+ return string(content)
+}
+
+func GetTemplate(name string) string {
+ path := "/mail/templates"
+ if HasTemplateOnFileSystem(path, name+".html") {
+ return Readfile(path, name+".html")
+ } else {
+ if name == "invite" {
+ return TEMPLATE_INVITE
+ } else if name == "forgot" {
+ return TEMPLATE_FORGOT
+ } else if name == "activate" {
+ return TEMPLATE_ACTIVATE
+ }
+ }
+ return ""
+}
+
+func GetTextTemplate(name string) string {
+ path := "/mail/templates"
+ if HasTemplateOnFileSystem(path, name+".txt") {
+ return Readfile(path, name+".txt")
+ } else {
+ if name == "invite" {
+ return TEMPLATE_INVITE_TEXT
+ } else if name == "forgot" {
+ return TEMPLATE_FORGOT_TEXT
+ } else if name == "activate" {
+ return TEMPLATE_ACTIVATE_TEXT
+ }
+ }
+ return ""
+}
diff --git a/api/templates/welcome.go b/api/templates/welcome.go
new file mode 100644
index 0000000..f776c56
--- /dev/null
+++ b/api/templates/welcome.go
@@ -0,0 +1,446 @@
+package templates
+
+var TEMPLATE_TEXT = `
+Uug.ai
+------------
+
+Hey, {{user}}
+Welcome to Facial Access Controll!
+
+However before you get started this amazing applications, please activate your account.
+
+Get in touch
+------------
+support@uug.ai
+KErkstraat 108, 9050 Ghent, Belgium
+https://uug.ai
+
+About Uug.ai
+------------
+Welcome to the revolutionary video analytics and video management platform. Open, modular, and extensible for everyone, anywhere.
+`
+
+var TEMPLATE_WELCOME = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ Kerberos Hub in a nutshell
+ With Kerberos Hub you can access your surveillance media remotely. By subscribing to a plan, you get access to a set of features, from advanced filtering, notifications to machine learning.
+ However before you get started this amazing applications, please activate your acount.
+ |
+ |
+
+
+
+ |
+
+
+
+
+`
diff --git a/api/update-mod.sh b/api/update-mod.sh
new file mode 100755
index 0000000..f7ca4d1
--- /dev/null
+++ b/api/update-mod.sh
@@ -0,0 +1,4 @@
+export GOSUMDB=off
+rm -rf go.*
+go mod init github.com/uug-ai/facial-access-control/api
+go mod tidy
\ No newline at end of file
diff --git a/api/utils/main.go b/api/utils/main.go
index 37cceb4..5213e24 100644
--- a/api/utils/main.go
+++ b/api/utils/main.go
@@ -1,7 +1,11 @@
package utils
import (
+ "encoding/base64"
"fmt"
+ "math/rand"
+ "strconv"
+ "time"
"golang.org/x/crypto/bcrypt"
)
@@ -20,10 +24,154 @@ func PrintASCIIArt() {
}
func Hash(str string) (string, error) {
- hashed, err := bcrypt.GenerateFromPassword([]byte(str), bcrypt.DefaultCost)
- return string(hashed), err
+ hashed, err := bcrypt.GenerateFromPassword([]byte(str), bcrypt.DefaultCost)
+ return string(hashed), err
}
func IsSame(str string, hashed string) bool {
- return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(str)) == nil
+ return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(str)) == nil
+}
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+const letterBytes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+func RandStringBytesRmndr(n int) string {
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
+ }
+ return string(b)
+}
+
+func Contains(arr []string, str string) bool {
+ for _, a := range arr {
+ if a == str {
+ return true
+ }
+ }
+ return false
+}
+
+func GetDate(timezone string, timestamp int64) string {
+ t := time.Unix(timestamp, 0)
+ loc, _ := time.LoadLocation(timezone)
+ return t.In(loc).Format("02-01-2006")
+}
+
+func GetHour(timezone string, timestamp int64) int {
+ t := time.Unix(timestamp, 0)
+ loc, _ := time.LoadLocation(timezone)
+ return t.In(loc).Hour()
+}
+
+func GetTime(timezone string, timestamp int64) string {
+ t := time.Unix(timestamp, 0)
+ loc, _ := time.LoadLocation(timezone)
+ return t.In(loc).Format("15:04:05")
+}
+
+func StringToInt(s string) int {
+ i, err := strconv.Atoi(s)
+ if err != nil {
+ return 0
+ }
+ return i
+}
+
+func GetDateTime(timezone string, timestamp int64) string {
+ t := time.Unix(timestamp, 0)
+ loc, _ := time.LoadLocation(timezone)
+ return t.In(loc).Format("02-01-2006 - 15:04:05")
+}
+
+func GetDateTimeLong(timezone string, timestamp int64) string {
+ t := time.Unix(timestamp, 0)
+ loc, _ := time.LoadLocation(timezone)
+ timeInLocation := t.In(loc)
+
+ suffix := "th"
+ switch timeInLocation.Day() {
+ case 1, 21, 31:
+ suffix = "st"
+ case 2, 22:
+ suffix = "nd"
+ case 3, 23:
+ suffix = "rd"
+ }
+
+ return timeInLocation.Format("January 2" + suffix + " 2006, 15:04:05")
+}
+
+func GetDateShort(timezone string, timestamp int64) string {
+ t := time.Unix(timestamp, 0)
+ loc, _ := time.LoadLocation(timezone)
+ timeInLocation := t.In(loc)
+
+ suffix := "th"
+ switch timeInLocation.Day() {
+ case 1, 21, 31:
+ suffix = "st"
+ case 2, 22:
+ suffix = "nd"
+ case 3, 23:
+ suffix = "rd"
+ }
+
+ return timeInLocation.Format("January 2" + suffix + ",Monday")
+}
+
+func GetTimestamp(timezone string, date string) int64 {
+ layout := "02-01-2006"
+ loc, _ := time.LoadLocation(timezone)
+ t, err := time.ParseInLocation(layout, date, loc)
+ if err != nil {
+ fmt.Println(err)
+ return -1
+ }
+ return t.Unix()
+}
+
+func Uniq(slice []string) []string {
+ // create a map with all the values as key
+ uniqMap := make(map[string]struct{})
+ for _, v := range slice {
+ uniqMap[v] = struct{}{}
+ }
+
+ // turn the map keys into a slice
+ uniqSlice := make([]string, 0, len(uniqMap))
+ for v := range uniqMap {
+ uniqSlice = append(uniqSlice, v)
+ }
+ return uniqSlice
+}
+
+func Base64Encode(value string) string {
+ data := []byte(value)
+ str := base64.StdEncoding.EncodeToString(data)
+ return str
+}
+
+func Base64Decode(value string) (string, error) {
+ data, _ := base64.StdEncoding.DecodeString(value)
+ return string(data), nil
+}
+
+func EncodeURL(value string) string {
+ return base64.RawURLEncoding.EncodeToString([]byte(value))
+}
+
+func DecodeURL(value string) (string, error) {
+ data, _ := base64.RawURLEncoding.DecodeString(value)
+ return string(data), nil
+}
+
+func RemoveTrailingSlash(value string) string {
+ if value[len(value)-1:] == "/" {
+ return value[:len(value)-1]
+ }
+ return value
}
diff --git a/ui/.env.local b/ui/.env.local
deleted file mode 100644
index 2092301..0000000
--- a/ui/.env.local
+++ /dev/null
@@ -1 +0,0 @@
-NEXTAUTH_SECRET=/owlN1tQyTBDtSY5FjKL72lBjmuMQ7EpTAX7SFbj23A=
\ No newline at end of file
diff --git a/ui/.env.production b/ui/.env.production
index ec6cead..07fd4c1 100644
--- a/ui/.env.production
+++ b/ui/.env.production
@@ -1,2 +1,2 @@
API_URL=https://api.facialaccess.uug.ai/api/
-NEXTAUTH_URL=https://facialaccess.uug.ai/ssss
\ No newline at end of file
+NEXTAUTH_URL=https://facialaccess.uug.ai/
\ No newline at end of file
diff --git a/ui/index.d.ts b/ui/index.d.ts
new file mode 100644
index 0000000..f6d46c1
--- /dev/null
+++ b/ui/index.d.ts
@@ -0,0 +1 @@
+declare module '*.glb'
\ No newline at end of file
diff --git a/ui/next.config.mjs b/ui/next.config.mjs
index 01978d9..daa00f1 100644
--- a/ui/next.config.mjs
+++ b/ui/next.config.mjs
@@ -1,9 +1,23 @@
-
-
/** @type {import('next').NextConfig} */
const nextConfig = {
// basePath: "/facial-access-control", <==== required for github pages only.
- output: "standalone",
+ output: "standalone",
+ webpack(config) {
+ config.module.rules.push({
+ test: /\.(glb|gltf)$/,
+ use: [
+ {
+ loader: 'file-loader',
+ options: {
+ publicPath: '/_next/static/models/',
+ outputPath: 'static/models/',
+ name: '[name].[ext]',
+ },
+ },
+ ],
+ });
+ return config;
+ },
};
export default nextConfig;
diff --git a/ui/package.json b/ui/package.json
index 266fe3c..3af4ada 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -13,11 +13,19 @@
},
"dependencies": {
"@hookform/resolvers": "^3.5.0",
+ "@react-three/drei": "^9.108.3",
+ "@react-three/fiber": "^8.16.8",
"@reduxjs/toolkit": "^2.2.5",
"@testing-library/jest-dom": "^6.4.6",
- "@uug-ai/ui": "^1.0.36",
+ "@types/crypto-js": "^4.2.2",
+ "@types/three": "^0.166.0",
+ "@uug-ai/ui": "^1.0.39",
"autoprefixer": "^10.4.19",
+ "base-64": "^1.0.0",
"cookies-next": "^4.2.1",
+ "crypto-js": "^4.2.0",
+ "drei": "^2.2.21",
+ "file-loader": "^6.2.0",
"next": "14",
"next-auth": "^4.24.7",
"next-router-mock": "^0.9.13",
@@ -31,6 +39,7 @@
"redux-devtools-extension": "^2.13.9",
"redux-thunk": "^3.1.0",
"tailwindcss": "^3.4.3",
+ "three": "^0.166.1",
"ts-jest": "^29.1.4",
"ts-node": "^10.9.2",
"zod": "^3.23.8"
diff --git a/ui/src/app/dashboard/components/InviteUserDialog.tsx b/ui/src/app/dashboard/components/InviteUserDialog.tsx
index d91024d..ac2ef79 100644
--- a/ui/src/app/dashboard/components/InviteUserDialog.tsx
+++ b/ui/src/app/dashboard/components/InviteUserDialog.tsx
@@ -3,21 +3,25 @@
import Dialog from "@/components/Dialog";
import { Text, Input, Button } from "@/components/ui";
import React, { useState } from "react";
-import { useAddUserMutation } from "@/lib/services/users/userApi";
+import { useInviteUserMutation } from "@/lib/services/users/userApi";
import { useAppSelector } from "@/lib/hooks";
import { RootState } from "@/lib/store";
const InviteUserDialog: React.FC = () => {
- const [addUser, { isLoading }] = useAddUserMutation();
+ const [inviteUser, { isLoading }] = useInviteUserMutation();
const isVisible = useAppSelector(
(store: RootState) => store.dialog.isVisible
);
const [email, setEmail] = useState("");
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
const [submitError, setSubmitError] = useState(null);
const [isSubmitted, setIsSubmitted] = useState(false);
function handleClose() {
setEmail("");
+ setFirstName("");
+ setLastName("");
setIsSubmitted(false);
setSubmitError(null);
console.log("Dialog has closed");
@@ -28,7 +32,12 @@ const InviteUserDialog: React.FC = () => {
setSubmitError(null);
try {
- await addUser({ email }).unwrap();
+ await inviteUser({
+ status: "invited",
+ email: email,
+ firstname: firstName,
+ lastname: lastName,
+ }).unwrap();
setIsSubmitted(true);
console.log("Inviting user with email", email);
} catch (err) {
@@ -53,7 +62,7 @@ const InviteUserDialog: React.FC = () => {
) : (