Merging with dev/cedric

This commit is contained in:
gom-by
2025-01-28 18:30:56 +01:00
54 changed files with 1083 additions and 624 deletions

2
backend/.gitignore vendored
View File

@@ -1,2 +1,2 @@
tmp tmp
media /media

18
backend/README.md Normal file
View File

@@ -0,0 +1,18 @@
# Prerequisites for development
- Installing dependencies: `go mod tidy`
- Verifying that the `.env` file is present and well configured.
## Running migrations
A migration cli is made available through the package: `./cmd/migrate`
At first it is needed to run: `go run ./cmd/migrate db init` to initialize the
tables needed by golang's bun.
Then, if there are migrations needed to be applied, run: `go run ./cmd/migrate db migrate`
Everything should be taken care of.
## Running the server
The server can be ran as easily as: `go run .`

View File

@@ -0,0 +1,58 @@
package blogs
import (
"context"
"fmt"
"net/http"
core "fr.latosa-escrima/api/core"
)
func HandleBlog(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
blog_uuid := r.PathValue("uuid")
var blog core.Blog
_, err := core.DB.NewSelect().
Model(&blog).
Where("blog_id = ?", blog_uuid).
Relation("Author").
ScanAndCount(context.Background())
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNotAcceptable)
return
}
core.JSONSuccess{
Status: core.Success,
Message: "Status OK",
Data: blog,
}.Respond(w, http.StatusOK)
return
}
func HandleGetBlogs(w http.ResponseWriter, r *http.Request) {
var blog []core.Blog
count, err := core.DB.NewSelect().
Model(&blog).
Relation("Author").
ScanAndCount(context.Background())
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNotAcceptable)
return
}
core.JSONSuccess{
Status: core.Success,
Message: fmt.Sprint("%d blogs objects sent", count),
Data: blog,
}.Respond(w, http.StatusOK)
return
}

View File

@@ -1,14 +1,22 @@
package api package blogs
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"net/http" "net/http"
"io"
core "fr.latosa-escrima/api/core" core "fr.latosa-escrima/api/core"
) )
func HandleCreateBlog(w http.ResponseWriter, r *http.Request) { func HandleNew(w http.ResponseWriter, r *http.Request) {
_, err := io.ReadAll(r.Body)
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNoContent)
return
}
var blog core.Blog var blog core.Blog
if err := json.NewDecoder(r.Body).Decode(&blog); err != nil { if err := json.NewDecoder(r.Body).Decode(&blog); err != nil {
core.JSONError{ core.JSONError{

View File

@@ -27,13 +27,6 @@ func (dsn *DSN) ToString() string {
return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dsn.User, dsn.Password, dsn.Hostname, dsn.Port, dsn.DBName) return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dsn.User, dsn.Password, dsn.Hostname, dsn.Port, dsn.DBName)
} }
type Role string
const (
AdminRole Role = "admin"
UserRole Role = "user"
)
type Status string type Status string
const ( const (
@@ -41,20 +34,35 @@ const (
Inactive Status = "Inactive" Inactive Status = "Inactive"
) )
type Group string
const (
LatosaGroup Group = "latosa"
WingTsunGroup Group = "wing-tsun"
)
type UserAttributes struct {
Groups []Group `json:"groups"`
}
type PermissionConditions struct {
Groups *[]Group `json:"groups,omitempty"`
}
type User struct { type User struct {
bun.BaseModel `bun:"table:users"` bun.BaseModel `bun:"table:users"`
UserID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"userId"` UserID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"userId"`
FirstName string `bun:"firstname,notnull" json:"firstname"` FirstName string `bun:"firstname,notnull" json:"firstname"`
LastName string `bun:"lastname,notnull" json:"lastname"` LastName string `bun:"lastname,notnull" json:"lastname"`
Email string `bun:"email,unique,notnull" json:"email"` Email string `bun:"email,unique,notnull" json:"email"`
Password string `bun:"password,notnull" json:"password,omitempty"` Password string `bun:"password,notnull" json:"password,omitempty"`
Phone string `bun:"phone,notnull" json:"phone"` Phone string `bun:"phone,notnull" json:"phone"`
Role Role `bun:"role,notnull,default:'user'" json:"role"` CreatedAt time.Time `bun:"created_at,default:current_timestamp" json:"createdAt"`
CreatedAt time.Time `bun:"created_at,default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `bun:"updated_at,default:current_timestamp" json:"updatedAt"`
UpdatedAt time.Time `bun:"updated_at,default:current_timestamp" json:"updatedAt"` Events []Event `bun:"m2m:events_to_users,join:User=Event" json:"events,omitempty"`
Events []Event `bun:"m2m:events_to_users,join:User=Event" json:"events,omitempty"` Articles []*Blog `bun:"rel:has-many,join:user_id=blog_id" json:"articles,omitempty"`
Articles []*Blog `bun:"rel:has-many,join:user_id=blog_id" json:"articles,omitempty"` Attributes UserAttributes `bun:"attributes,type:jsonb" json:"attributes"`
} }
func (u *User) Insert(ctx context.Context) (sql.Result, error) { func (u *User) Insert(ctx context.Context) (sql.Result, error) {
@@ -66,15 +74,6 @@ func (u *User) Insert(ctx context.Context) (sql.Result, error) {
} }
func Verify(ctx context.Context, email, password string) (*User, error) { func Verify(ctx context.Context, email, password string) (*User, error) {
// var user User
// query := `
// SELECT *
// FROM users
// WHERE email = ? AND password = crypt(?, password)
// `
//
// err := DB.NewRaw(query, email, password).Scan(ctx, user)
var user User var user User
count, err := DB.NewSelect(). count, err := DB.NewSelect().
Model(&user). Model(&user).
@@ -94,6 +93,40 @@ func Verify(ctx context.Context, email, password string) (*User, error) {
return &user, nil return &user, nil
} }
type Permission struct {
bun.BaseModel `bun:"table:permissions"`
ID int `bun:"id,pk,autoincrement" json:"id"`
Resource string `bun:"resource,notnull" json:"resource"`
Action string `bun:"action,notnull" json:"action"`
Conditions PermissionConditions `bun:"conditions,type:jsonb" json:"conditions"`
}
type Role struct {
bun.BaseModel `bun:"table:roles"`
ID uuid.UUID `bun:"id,pk,type:uuid,default:gen_random_uuid()" json:"id"`
Name string `bun:"name,unique,notnull" json:"name"`
}
type PermissionToRole struct {
bun.BaseModel `bun:"table:permissions_to_users"`
PermissionID int `bun:"permission_id,pk"`
RoleID uuid.UUID `bun:"type:uuid,pk"`
Permission *Permission `bun:"rel:belongs-to,join:permission_id=id"`
Role *Role `bun:"rel:belongs-to,join:role_id=id"`
}
type UserToRole struct {
bun.BaseModel `bun:"table:users_to_roles"`
UserID uuid.UUID `bun:"user_id,type:uuid,pk"`
RoleID uuid.UUID `bun:"type:uuid,pk"`
User *User `bun:"rel:belongs-to,join:user_id=user_id"`
Role *Role `bun:"rel:belongs-to,join:role_id=id"`
}
type Event struct { type Event struct {
bun.BaseModel `bun:"table:events"` bun.BaseModel `bun:"table:events"`
@@ -188,6 +221,8 @@ func InitDatabase(dsn DSN) (*bun.DB, error) {
return nil, err return nil, err
} }
db.RegisterModel((*EventToUser)(nil)) db.RegisterModel((*EventToUser)(nil))
db.RegisterModel((*PermissionToRole)(nil))
db.RegisterModel((*UserToRole)(nil))
_, err = db.NewCreateTable().Model((*User)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*User)(nil)).IfNotExists().Exec(ctx)
_, err = db.NewCreateTable().Model((*Event)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*Event)(nil)).IfNotExists().Exec(ctx)
_, err = db.NewCreateTable().Model((*EventToUser)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*EventToUser)(nil)).IfNotExists().Exec(ctx)
@@ -195,6 +230,10 @@ func InitDatabase(dsn DSN) (*bun.DB, error) {
_, err = db.NewCreateTable().Model((*WebsiteSettings)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*WebsiteSettings)(nil)).IfNotExists().Exec(ctx)
_, err = db.NewCreateTable().Model((*Media)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*Media)(nil)).IfNotExists().Exec(ctx)
_, err = db.NewCreateTable().Model((*Shortcode)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*Shortcode)(nil)).IfNotExists().Exec(ctx)
_, err = db.NewCreateTable().Model((*Role)(nil)).IfNotExists().Exec(ctx)
_, err = db.NewCreateTable().Model((*Permission)(nil)).IfNotExists().Exec(ctx)
_, err = db.NewCreateTable().Model((*PermissionToRole)(nil)).IfNotExists().Exec(ctx)
_, err = db.NewCreateTable().Model((*UserToRole)(nil)).IfNotExists().Exec(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,4 +1,4 @@
package api package events
import ( import (
"context" "context"
@@ -8,7 +8,7 @@ import (
"fr.latosa-escrima/api/core" "fr.latosa-escrima/api/core"
) )
func HandleDeleteEvent(w http.ResponseWriter, r *http.Request) { func HandleDelete(w http.ResponseWriter, r *http.Request) {
uuid := r.PathValue("event_uuid") uuid := r.PathValue("event_uuid")
var event core.Event var event core.Event
res, err := core.DB.NewDelete(). res, err := core.DB.NewDelete().

View File

@@ -1,4 +1,4 @@
package api package events
import ( import (
"context" "context"
@@ -8,7 +8,7 @@ import (
core "fr.latosa-escrima/api/core" core "fr.latosa-escrima/api/core"
) )
func HandleGetEvent(w http.ResponseWriter, r *http.Request) { func HandleEvent(w http.ResponseWriter, r *http.Request) {
event_uuid := r.PathValue("event_uuid") event_uuid := r.PathValue("event_uuid")
var event core.Event var event core.Event
_, err := core.DB.NewSelect().Model(&event).Where("uuid = ?", event_uuid).ScanAndCount(context.Background()) _, err := core.DB.NewSelect().Model(&event).Where("uuid = ?", event_uuid).ScanAndCount(context.Background())
@@ -28,7 +28,7 @@ func HandleGetEvent(w http.ResponseWriter, r *http.Request) {
return return
} }
func HangleGetEvents(w http.ResponseWriter, r *http.Request) { func HandleEvents(w http.ResponseWriter, r *http.Request) {
var events []core.Event var events []core.Event
rowsCount, err := core.DB.NewSelect().Model(&events).ScanAndCount(context.Background()) rowsCount, err := core.DB.NewSelect().Model(&events).ScanAndCount(context.Background())
if err != nil { if err != nil {
@@ -46,4 +46,3 @@ func HangleGetEvents(w http.ResponseWriter, r *http.Request) {
}.Respond(w, http.StatusOK) }.Respond(w, http.StatusOK)
return return
} }

View File

@@ -1,4 +1,4 @@
package api package events
import ( import (
"context" "context"
@@ -8,7 +8,7 @@ import (
core "fr.latosa-escrima/api/core" core "fr.latosa-escrima/api/core"
) )
func HandleCreateEvent(w http.ResponseWriter, r *http.Request) { func HandleNew(w http.ResponseWriter, r *http.Request) {
var event core.Event var event core.Event
err := json.NewDecoder(r.Body).Decode(&event) err := json.NewDecoder(r.Body).Decode(&event)
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
package api package events
import ( import (
"context" "context"
@@ -10,7 +10,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
func HandleUpdateEvent(w http.ResponseWriter, r *http.Request) { func HandleUpdate(w http.ResponseWriter, r *http.Request) {
var event core.Event var event core.Event
err := json.NewDecoder(r.Body).Decode(&event) err := json.NewDecoder(r.Body).Decode(&event)
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
package api package blogs
import ( import (
"context" "context"
@@ -8,7 +8,9 @@ import (
core "fr.latosa-escrima/api/core" core "fr.latosa-escrima/api/core"
) )
func HandleGetBlog(w http.ResponseWriter, r *http.Request) { func HandleBlog(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
blog_uuid := r.PathValue("uuid") blog_uuid := r.PathValue("uuid")
var blog core.Blog var blog core.Blog
@@ -19,7 +21,7 @@ func HandleGetBlog(w http.ResponseWriter, r *http.Request) {
ScanAndCount(context.Background()) ScanAndCount(context.Background())
if err != nil { if err != nil {
core.JSONError{ core.JSONError{
Status: core.Error, Status: core.Error,
Message: err.Error(), Message: err.Error(),
}.Respond(w, http.StatusNotAcceptable) }.Respond(w, http.StatusNotAcceptable)
return return
@@ -28,7 +30,7 @@ func HandleGetBlog(w http.ResponseWriter, r *http.Request) {
core.JSONSuccess{ core.JSONSuccess{
Status: core.Success, Status: core.Success,
Message: "Status OK", Message: "Status OK",
Data: blog, Data: blog,
}.Respond(w, http.StatusOK) }.Respond(w, http.StatusOK)
return return
} }

View File

@@ -1,4 +1,4 @@
package api package media
import ( import (
"context" "context"
@@ -9,7 +9,7 @@ import (
"fr.latosa-escrima/api/core" "fr.latosa-escrima/api/core"
) )
func HandleDeleteMedia(w http.ResponseWriter, r *http.Request) { func HandleDelete(w http.ResponseWriter, r *http.Request) {
uuid := r.PathValue("media_uuid") uuid := r.PathValue("media_uuid")
var media core.Media var media core.Media
res, err := core.DB.NewDelete(). res, err := core.DB.NewDelete().

View File

@@ -1,4 +1,4 @@
package api package media
import ( import (
"context" "context"
@@ -11,7 +11,7 @@ import (
"fr.latosa-escrima/utils" "fr.latosa-escrima/utils"
) )
func HandleGetMedia(w http.ResponseWriter, r *http.Request) { func HandleMedia(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query() queryParams := r.URL.Query()
page, err := strconv.Atoi(queryParams.Get("page")) page, err := strconv.Atoi(queryParams.Get("page"))
limit, err := strconv.Atoi(queryParams.Get("limit")) limit, err := strconv.Atoi(queryParams.Get("limit"))
@@ -69,7 +69,7 @@ func HandleGetMedia(w http.ResponseWriter, r *http.Request) {
}.Respond(w, http.StatusOK) }.Respond(w, http.StatusOK)
} }
func HandleGetMediaDetails(w http.ResponseWriter, r *http.Request) { func HandleMediaDetails(w http.ResponseWriter, r *http.Request) {
uuid := r.PathValue("media_uuid") uuid := r.PathValue("media_uuid")
var media core.Media var media core.Media
err := core.DB.NewSelect(). err := core.DB.NewSelect().
@@ -96,7 +96,7 @@ func HandleGetMediaDetails(w http.ResponseWriter, r *http.Request) {
}.Respond(w, http.StatusOK) }.Respond(w, http.StatusOK)
} }
func HandleGetMediaFile(w http.ResponseWriter, r *http.Request) { func HandleMediaFile(w http.ResponseWriter, r *http.Request) {
uuid := r.PathValue("media_uuid") uuid := r.PathValue("media_uuid")
var media core.Media var media core.Media
err := core.DB.NewSelect(). err := core.DB.NewSelect().

View File

@@ -0,0 +1,3 @@
package media
// TODO

View File

@@ -1,4 +1,4 @@
package api package media
import ( import (
"context" "context"
@@ -14,7 +14,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
func HandleUploadMedia(w http.ResponseWriter, r *http.Request) { func HandleUpload(w http.ResponseWriter, r *http.Request) {
// Parse the multipart form // Parse the multipart form
err := r.ParseMultipartForm(10 << 20) // Limit file size to 10 MB err := r.ParseMultipartForm(10 << 20) // Limit file size to 10 MB
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
package api package media
import ( import (
"encoding/json" "encoding/json"
@@ -18,7 +18,7 @@ type FileArgs struct {
SizeByte int64 `json:"size"` SizeByte int64 `json:"size"`
} }
func HandleVerifyMedia(w http.ResponseWriter, r *http.Request) { func HandleVerify(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
core.JSONError{ core.JSONError{

View File

@@ -2,99 +2,33 @@ package api
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"os"
"strings" "strings"
"time"
core "fr.latosa-escrima/api/core" core "fr.latosa-escrima/api/core"
"fr.latosa-escrima/api/users"
"fr.latosa-escrima/utils" "fr.latosa-escrima/utils"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
var MySigningKey = []byte("COUCOU") func CORS(next http.Handler) http.Handler {
CORS_AllowOrigin := os.Getenv("CORS_AllowOrigin")
type LoginArgs struct { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Email string `json:"email"` // Allow all origins (can restrict to specific origins)
Password string `json:"password"` w.Header().Set("Access-Control-Allow-Origin", CORS_AllowOrigin)
} // Allow certain HTTP methods (you can customize these as needed)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
type Claims struct { // Allow certain headers (you can add more as needed)
UserID string `json:"user_id"` w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CSRF-Token")
jwt.RegisteredClaims w.Header().Set("Access-Control-Allow-Credentials", "true")
} // Handle OPTIONS pre-flight request
if r.Method == http.MethodOptions {
func HandleLogin(w http.ResponseWriter, r *http.Request) { return
if r.Body == nil { }
core.JSONError{ next.ServeHTTP(w, r)
Status: core.Error, })
Message: "No body has been provided.",
}.Respond(w, http.StatusNoContent)
return
}
body, err := io.ReadAll(r.Body)
fmt.Println(body)
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNoContent)
return
}
var login LoginArgs
err = json.Unmarshal(body, &login)
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNoContent)
return
}
user, err := core.Verify(context.Background(), login.Email, login.Password)
if user == nil {
core.JSONError{
Status: core.Error,
Message: "User not found.",
}.Respond(w, http.StatusNotFound)
return
}
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNoContent)
return
}
claims := Claims{
UserID: user.UserID.String(),
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "latosa-escrima.fr",
Subject: "authentification",
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(MySigningKey)
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNoContent)
return
}
core.JSONSuccess{
Status: core.Success,
Message: "JWT Created",
Data: signed,
}.Respond(w, http.StatusCreated)
} }
func AuthJWT(next http.Handler) http.Handler { func AuthJWT(next http.Handler) http.Handler {
@@ -125,7 +59,7 @@ func AuthJWT(next http.Handler) http.Handler {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
} }
return MySigningKey, nil return users.MySigningKey, nil
}) })
if err != nil || !token.Valid { if err != nil || !token.Valid {

View File

@@ -0,0 +1 @@
package permissions

View File

@@ -0,0 +1 @@
package permissions

View File

@@ -0,0 +1 @@
package permissions

View File

@@ -0,0 +1 @@
package permissions

View File

@@ -0,0 +1 @@
package permissions

View File

@@ -0,0 +1 @@
package roles

View File

@@ -0,0 +1 @@
package roles

1
backend/api/roles/new.go Normal file
View File

@@ -0,0 +1 @@
package roles

View File

@@ -0,0 +1 @@
package roles

View File

@@ -0,0 +1 @@
package roles

View File

@@ -0,0 +1 @@
package roles

View File

@@ -0,0 +1 @@
package roles

View File

@@ -0,0 +1 @@
package roles

View File

@@ -1,4 +1,4 @@
package api package shortcodes
import ( import (
"context" "context"
@@ -7,7 +7,7 @@ import (
core "fr.latosa-escrima/api/core" core "fr.latosa-escrima/api/core"
) )
func HandleDeleteShortcode(w http.ResponseWriter, r *http.Request) { func HandleDelete(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("shortcode") code := r.PathValue("shortcode")
_, err := core.DB.NewDelete(). _, err := core.DB.NewDelete().
Model((*core.Shortcode)(nil)). Model((*core.Shortcode)(nil)).

View File

@@ -1,4 +1,4 @@
package api package shortcodes
import ( import (
"context" "context"
@@ -8,7 +8,7 @@ import (
"fr.latosa-escrima/api/core" "fr.latosa-escrima/api/core"
) )
func HandleCreateShortcode(w http.ResponseWriter, r *http.Request) { func HandleNew(w http.ResponseWriter, r *http.Request) {
var shortcode core.Shortcode var shortcode core.Shortcode
err := json.NewDecoder(r.Body).Decode(&shortcode) err := json.NewDecoder(r.Body).Decode(&shortcode)
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
package api package shortcodes
import ( import (
"context" "context"
@@ -7,7 +7,7 @@ import (
"fr.latosa-escrima/api/core" "fr.latosa-escrima/api/core"
) )
func HandleGetShortcode(w http.ResponseWriter, r *http.Request) { func HandleShortcode(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("shortcode") code := r.PathValue("shortcode")
var shortcode core.Shortcode var shortcode core.Shortcode
err := core.DB.NewSelect(). err := core.DB.NewSelect().

View File

@@ -1,4 +1,4 @@
package api package shortcodes
import ( import (
"context" "context"
@@ -7,7 +7,7 @@ import (
"fr.latosa-escrima/api/core" "fr.latosa-escrima/api/core"
) )
func HandleGetShortcodes(w http.ResponseWriter, r *http.Request) { func HandleShortcodes(w http.ResponseWriter, r *http.Request) {
var shortcodes []core.Shortcode var shortcodes []core.Shortcode
err := core.DB.NewSelect().Model(&shortcodes).Scan(context.Background()) err := core.DB.NewSelect().Model(&shortcodes).Scan(context.Background())
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
package api package shortcodes
import ( import (
"context" "context"
@@ -20,7 +20,7 @@ type UpdateShortcodeArgs struct {
MediaID *uuid.UUID `json:"media_id,omitempty"` // Nullable reference to another table's ID MediaID *uuid.UUID `json:"media_id,omitempty"` // Nullable reference to another table's ID
} }
func HandleUpdateShortcode(w http.ResponseWriter, r *http.Request) { func HandleUpdate(w http.ResponseWriter, r *http.Request) {
var updateArgs UpdateShortcodeArgs var updateArgs UpdateShortcodeArgs
err := json.NewDecoder(r.Body).Decode(&updateArgs) err := json.NewDecoder(r.Body).Decode(&updateArgs)
if err != nil { if err != nil {

96
backend/api/users/auth.go Normal file
View File

@@ -0,0 +1,96 @@
package users
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
core "fr.latosa-escrima/api/core"
"github.com/golang-jwt/jwt/v5"
)
var MySigningKey = []byte("COUCOU")
type LoginArgs struct {
Email string `json:"email"`
Password string `json:"password"`
}
type Claims struct {
UserID string `json:"user_id"`
jwt.RegisteredClaims
}
func HandleLogin(w http.ResponseWriter, r *http.Request) {
if r.Body == nil {
core.JSONError{
Status: core.Error,
Message: "No body has been provided.",
}.Respond(w, http.StatusNoContent)
return
}
body, err := io.ReadAll(r.Body)
fmt.Println(body)
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNoContent)
return
}
var login LoginArgs
err = json.Unmarshal(body, &login)
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNoContent)
return
}
user, err := core.Verify(context.Background(), login.Email, login.Password)
if user == nil {
core.JSONError{
Status: core.Error,
Message: "User not found.",
}.Respond(w, http.StatusNotFound)
return
}
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNoContent)
return
}
claims := Claims{
UserID: user.UserID.String(),
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "latosa-escrima.fr",
Subject: "authentification",
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(MySigningKey)
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNoContent)
return
}
core.JSONSuccess{
Status: core.Success,
Message: "JWT Created",
Data: signed,
}.Respond(w, http.StatusCreated)
}

View File

@@ -1,4 +1,4 @@
package api package users
import ( import (
"context" "context"
@@ -7,7 +7,7 @@ import (
"fr.latosa-escrima/api/core" "fr.latosa-escrima/api/core"
) )
func HandleDeleteUser(w http.ResponseWriter, r *http.Request) { func HandleDelete(w http.ResponseWriter, r *http.Request) {
uuid := r.PathValue("user_uuid") uuid := r.PathValue("user_uuid")
_, err := core.DB.NewDelete(). _, err := core.DB.NewDelete().
Model((*core.User)(nil)). Model((*core.User)(nil)).

View File

@@ -1,4 +1,4 @@
package api package users
import ( import (
"net/http" "net/http"
@@ -7,7 +7,7 @@ import (
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
func HandleGetMe(w http.ResponseWriter, r *http.Request) { func HandleMe(w http.ResponseWriter, r *http.Request) {
token, ok := r.Context().Value("token").(*jwt.Token) token, ok := r.Context().Value("token").(*jwt.Token)
if !ok { if !ok {
core.JSONError{ core.JSONError{
@@ -29,5 +29,5 @@ func HandleGetMe(w http.ResponseWriter, r *http.Request) {
uuid := claims["user_id"].(string) uuid := claims["user_id"].(string)
r.SetPathValue("user_uuid", uuid) r.SetPathValue("user_uuid", uuid)
HandleGetUser(w, r) HandleUser(w, r)
} }

View File

@@ -1,4 +1,4 @@
package api package users
import ( import (
"context" "context"
@@ -9,7 +9,7 @@ import (
core "fr.latosa-escrima/api/core" core "fr.latosa-escrima/api/core"
) )
func HandleCreateUser(w http.ResponseWriter, r *http.Request) { func HandleNew(w http.ResponseWriter, r *http.Request) {
var user core.User var user core.User
err := json.NewDecoder(r.Body).Decode(&user) err := json.NewDecoder(r.Body).Decode(&user)
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
package api package users
import ( import (
"context" "context"
@@ -13,15 +13,15 @@ import (
) )
type UpdateUserArgs struct { type UpdateUserArgs struct {
FirstName *string `json:"firstname,omitempty"` FirstName *string `json:"firstname,omitempty"`
LastName *string `json:"lastname,omitempty"` LastName *string `json:"lastname,omitempty"`
Email *string `json:"email,omitempty"` Email *string `json:"email,omitempty"`
Password *string `json:"password,omitempty"` Password *string `json:"password,omitempty"`
Phone *string `json:"phone,omitempty"` Phone *string `json:"phone,omitempty"`
Role *core.Role `json:"role,omitempty"` Attributes *core.UserAttributes `json:"attributes"`
} }
func HandleUpdateUser(w http.ResponseWriter, r *http.Request) { func HandleUpdate(w http.ResponseWriter, r *http.Request) {
var updateArgs UpdateUserArgs var updateArgs UpdateUserArgs
err := json.NewDecoder(r.Body).Decode(&updateArgs) err := json.NewDecoder(r.Body).Decode(&updateArgs)
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
package api package users
import ( import (
"context" "context"
@@ -7,7 +7,7 @@ import (
"fr.latosa-escrima/api/core" "fr.latosa-escrima/api/core"
) )
func HandleGetUser(w http.ResponseWriter, r *http.Request) { func HandleUser(w http.ResponseWriter, r *http.Request) {
uuid := r.PathValue("user_uuid") uuid := r.PathValue("user_uuid")
var user core.User var user core.User
count, err := core.DB.NewSelect(). count, err := core.DB.NewSelect().

View File

@@ -1,4 +1,4 @@
package api package users
import ( import (
"context" "context"
@@ -8,7 +8,7 @@ import (
"fr.latosa-escrima/utils" "fr.latosa-escrima/utils"
) )
func HandleGetUsers(w http.ResponseWriter, r *http.Request) { func HandleUsers(w http.ResponseWriter, r *http.Request) {
var users []core.User var users []core.User
count, err := core.DB.NewSelect(). count, err := core.DB.NewSelect().
Model(&users). Model(&users).
@@ -20,9 +20,9 @@ func HandleGetUsers(w http.ResponseWriter, r *http.Request) {
}) })
if count == 0 { if count == 0 {
core.JSONError{ core.JSONSuccess{
Status: core.Error, Status: core.Success,
Message: "Not users.", Message: "No users.",
}.Respond(w, http.StatusNotFound) }.Respond(w, http.StatusNotFound)
return return
} }
@@ -35,7 +35,6 @@ func HandleGetUsers(w http.ResponseWriter, r *http.Request) {
return return
} }
// TODO : Remove password
core.JSONSuccess{ core.JSONSuccess{
Status: core.Success, Status: core.Success,
Message: "Users found.", Message: "Users found.",

219
backend/cmd/migrate/main.go Normal file
View File

@@ -0,0 +1,219 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"strings"
"fr.latosa-escrima/api/core"
"fr.latosa-escrima/cmd/migrate/migrations"
"github.com/joho/godotenv"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
"github.com/uptrace/bun/extra/bundebug"
"github.com/uptrace/bun/migrate"
"github.com/urfave/cli/v2"
"github.com/uptrace/bun"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
environ := os.Getenv("ENVIRONMENT")
hostname := os.Getenv("DATABASE_HOSTNAME")
postgres_port := os.Getenv("POSTGRES_DOCKER_PORT")
if environ == "DEV" {
hostname = "localhost"
postgres_port = os.Getenv("POSTGRES_PORT")
}
dsn := core.DSN{
Hostname: hostname,
Port: postgres_port,
DBName: os.Getenv("POSTGRES_DB"),
User: os.Getenv("POSTGRES_USER"),
Password: os.Getenv("POSTGRES_PASSWORD"),
}
fmt.Println(dsn.ToString())
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn.ToString())))
db := bun.NewDB(sqldb, pgdialect.New())
db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
defer db.Close()
if err != nil {
panic(err)
}
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithEnabled(false),
bundebug.FromEnv(),
))
app := &cli.App{
Name: "bun",
Commands: []*cli.Command{
newDBCommand(migrate.NewMigrator(db, migrations.Migrations)),
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func newDBCommand(migrator *migrate.Migrator) *cli.Command {
return &cli.Command{
Name: "db",
Usage: "database migrations",
Subcommands: []*cli.Command{
{
Name: "init",
Usage: "create migration tables",
Action: func(c *cli.Context) error {
return migrator.Init(c.Context)
},
},
{
Name: "migrate",
Usage: "migrate database",
Action: func(c *cli.Context) error {
if err := migrator.Lock(c.Context); err != nil {
return err
}
defer migrator.Unlock(c.Context) //nolint:errcheck
group, err := migrator.Migrate(c.Context)
if err != nil {
return err
}
if group.IsZero() {
fmt.Printf("there are no new migrations to run (database is up to date)\n")
return nil
}
fmt.Printf("migrated to %s\n", group)
return nil
},
},
{
Name: "rollback",
Usage: "rollback the last migration group",
Action: func(c *cli.Context) error {
if err := migrator.Lock(c.Context); err != nil {
return err
}
defer migrator.Unlock(c.Context) //nolint:errcheck
group, err := migrator.Rollback(c.Context)
if err != nil {
return err
}
if group.IsZero() {
fmt.Printf("there are no groups to roll back\n")
return nil
}
fmt.Printf("rolled back %s\n", group)
return nil
},
},
{
Name: "lock",
Usage: "lock migrations",
Action: func(c *cli.Context) error {
return migrator.Lock(c.Context)
},
},
{
Name: "unlock",
Usage: "unlock migrations",
Action: func(c *cli.Context) error {
return migrator.Unlock(c.Context)
},
},
{
Name: "create_go",
Usage: "create Go migration",
Action: func(c *cli.Context) error {
name := strings.Join(c.Args().Slice(), "_")
mf, err := migrator.CreateGoMigration(c.Context, name)
if err != nil {
return err
}
fmt.Printf("created migration %s (%s)\n", mf.Name, mf.Path)
return nil
},
},
{
Name: "create_sql",
Usage: "create up and down SQL migrations",
Action: func(c *cli.Context) error {
name := strings.Join(c.Args().Slice(), "_")
files, err := migrator.CreateSQLMigrations(c.Context, name)
if err != nil {
return err
}
for _, mf := range files {
fmt.Printf("created migration %s (%s)\n", mf.Name, mf.Path)
}
return nil
},
},
{
Name: "create_tx_sql",
Usage: "create up and down transactional SQL migrations",
Action: func(c *cli.Context) error {
name := strings.Join(c.Args().Slice(), "_")
files, err := migrator.CreateTxSQLMigrations(c.Context, name)
if err != nil {
return err
}
for _, mf := range files {
fmt.Printf("created transaction migration %s (%s)\n", mf.Name, mf.Path)
}
return nil
},
},
{
Name: "status",
Usage: "print migrations status",
Action: func(c *cli.Context) error {
ms, err := migrator.MigrationsWithStatus(c.Context)
if err != nil {
return err
}
fmt.Printf("migrations: %s\n", ms)
fmt.Printf("unapplied migrations: %s\n", ms.Unapplied())
fmt.Printf("last migration group: %s\n", ms.LastGroup())
return nil
},
},
{
Name: "mark_applied",
Usage: "mark migrations as applied without actually running them",
Action: func(c *cli.Context) error {
group, err := migrator.Migrate(c.Context, migrate.WithNopMigration())
if err != nil {
return err
}
if group.IsZero() {
fmt.Printf("there are no new migrations to mark as applied\n")
return nil
}
fmt.Printf("marked as applied %s\n", group)
return nil
},
},
},
}
}

View File

@@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN attributes;

View File

@@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN attributes jsonb;

View File

@@ -0,0 +1,11 @@
package migrations
import "github.com/uptrace/bun/migrate"
var Migrations = migrate.NewMigrations()
func init() {
if err := Migrations.DiscoverCaller(); err != nil {
panic(err)
}
}

View File

@@ -12,6 +12,12 @@ require (
github.com/uptrace/bun/driver/pgdriver v1.2.8 github.com/uptrace/bun/driver/pgdriver v1.2.8
) )
require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
)
require ( require (
github.com/gorilla/csrf v1.7.2 // direct github.com/gorilla/csrf v1.7.2 // direct
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
@@ -26,6 +32,7 @@ require (
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/uptrace/bun/extra/bundebug v1.2.8 // direct github.com/uptrace/bun/extra/bundebug v1.2.8 // direct
github.com/urfave/cli/v2 v2.27.5
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.31.0 // indirect

View File

@@ -1,3 +1,5 @@
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -27,6 +29,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
@@ -39,10 +43,14 @@ github.com/uptrace/bun/driver/pgdriver v1.2.8 h1:5XrNn/9enSrWhhrUpz+6PY9S1vcg/jh
github.com/uptrace/bun/driver/pgdriver v1.2.8/go.mod h1:cwRRwqabgePwYBiLlXtbeNmPD7LGJnqP21J2ZKP4ah8= github.com/uptrace/bun/driver/pgdriver v1.2.8/go.mod h1:cwRRwqabgePwYBiLlXtbeNmPD7LGJnqP21J2ZKP4ah8=
github.com/uptrace/bun/extra/bundebug v1.2.8 h1:Epv0ycLOnoKWPky+rufP2F/PrcSlKkd4tmVIFOdq90A= github.com/uptrace/bun/extra/bundebug v1.2.8 h1:Epv0ycLOnoKWPky+rufP2F/PrcSlKkd4tmVIFOdq90A=
github.com/uptrace/bun/extra/bundebug v1.2.8/go.mod h1:ucnmuPw/5ePbNFj2SPmV0lQh3ZvL+3HCrpvRxIYZyWQ= github.com/uptrace/bun/extra/bundebug v1.2.8/go.mod h1:ucnmuPw/5ePbNFj2SPmV0lQh3ZvL+3HCrpvRxIYZyWQ=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -12,7 +12,12 @@ import (
_ "github.com/lib/pq" _ "github.com/lib/pq"
"fr.latosa-escrima/api" "fr.latosa-escrima/api"
"fr.latosa-escrima/api/blogs"
"fr.latosa-escrima/api/core" "fr.latosa-escrima/api/core"
"fr.latosa-escrima/api/events"
"fr.latosa-escrima/api/media"
"fr.latosa-escrima/api/shortcodes"
"fr.latosa-escrima/api/users"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )
@@ -21,22 +26,6 @@ var CORS_AllowOrigin string
func handler(w http.ResponseWriter, r *http.Request) { func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<html><body><h1>Hello, World!</h1></body></html>") fmt.Fprintf(w, "<html><body><h1>Hello, World!</h1></body></html>")
} }
func Cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow all origins (can restrict to specific origins)
w.Header().Set("Access-Control-Allow-Origin", CORS_AllowOrigin)
// Allow certain HTTP methods (you can customize these as needed)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
// Allow certain headers (you can add more as needed)
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CSRF-Token")
w.Header().Set("Access-Control-Allow-Credentials", "true")
// Handle OPTIONS pre-flight request
if r.Method == http.MethodOptions {
return
}
next.ServeHTTP(w, r)
})
}
func main() { func main() {
err := godotenv.Load() err := godotenv.Load()
@@ -48,7 +37,6 @@ func main() {
port := os.Getenv("BACKEND_DOCKER_PORT") port := os.Getenv("BACKEND_DOCKER_PORT")
hostname := os.Getenv("DATABASE_HOSTNAME") hostname := os.Getenv("DATABASE_HOSTNAME")
postgres_port := os.Getenv("POSTGRES_DOCKER_PORT") postgres_port := os.Getenv("POSTGRES_DOCKER_PORT")
CORS_AllowOrigin = os.Getenv("CORS_AllowOrigin")
if environ == "DEV" { if environ == "DEV" {
port = os.Getenv("BACKEND_PORT") port = os.Getenv("BACKEND_PORT")
hostname = "localhost" hostname = "localhost"
@@ -85,67 +73,67 @@ func main() {
Handler: handler, Handler: handler,
Middlewares: []core.Middleware{api.Methods("get")}}, Middlewares: []core.Middleware{api.Methods("get")}},
"/users/login": { "/users/login": {
Handler: api.HandleLogin, Handler: users.HandleLogin,
Middlewares: []core.Middleware{api.Methods("POST")}}, Middlewares: []core.Middleware{api.Methods("POST")}},
"/users/me": { "/users/me": {
Handler: api.HandleGetMe, Handler: users.HandleMe,
Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT}}, Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT}},
"/users": { "/users": {
Handler: api.HandleGetUsers, Handler: users.HandleUsers,
Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT}}, Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT}},
"/users/new": { "/users/new": {
Handler: api.HandleCreateUser, Handler: users.HandleNew,
Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT}}, Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT}},
"/users/{user_uuid}": { "/users/{user_uuid}": {
Handler: api.HandleGetUser, Handler: users.HandleUser,
Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT}}, Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT}},
"/users/{user_uuid}/delete": { "/users/{user_uuid}/delete": {
Handler: api.HandleDeleteUser, Handler: users.HandleDelete,
Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT}}, Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT}},
"/users/{user_uuid}/update": { "/users/{user_uuid}/update": {
Handler: api.HandleUpdateUser, Handler: users.HandleUpdate,
Middlewares: []core.Middleware{api.Methods("PATCH"), api.AuthJWT}}, Middlewares: []core.Middleware{api.Methods("PATCH"), api.AuthJWT}},
"/events": { "/events": {
Handler: api.HangleGetEvents, Handler: events.HandleEvents,
Middlewares: []core.Middleware{api.Methods("GET")}}, Middlewares: []core.Middleware{api.Methods("GET")}},
"/events/new": { "/events/new": {
Handler: api.HandleCreateEvent, Handler: events.HandleNew,
Middlewares: []core.Middleware{api.Methods("POST")}}, Middlewares: []core.Middleware{api.Methods("POST")}},
"/events/{event_uuid}": { "/events/{event_uuid}": {
Handler: api.HandleGetEvent, Handler: events.HandleEvent,
Middlewares: []core.Middleware{api.Methods("GET")}}, Middlewares: []core.Middleware{api.Methods("GET")}},
"/events/{event_uuid}/delete": { "/events/{event_uuid}/delete": {
Handler: api.HandleDeleteEvent, Handler: events.HandleDelete,
Middlewares: []core.Middleware{api.Methods("DELETE")}}, Middlewares: []core.Middleware{api.Methods("DELETE")}},
"/events/{event_uuid}/update": { "/events/{event_uuid}/update": {
Handler: api.HandleUpdateEvent, Handler: events.HandleUpdate,
Middlewares: []core.Middleware{api.Methods("PATCH")}}, Middlewares: []core.Middleware{api.Methods("PATCH")}},
"/blogs/new": { "/blogs/new": {
Handler: api.HandleCreateBlog, Handler: blogs.HandleNew,
Middlewares: []core.Middleware{api.Methods(("POST"))}}, Middlewares: []core.Middleware{api.Methods(("POST"))}},
"/blogs/{uuid}": { "/blogs/{uuid}": {
Handler: api.HandleGetBlog, Handler: blogs.HandleBlog,
Middlewares: []core.Middleware{api.Methods("GET")}}, Middlewares: []core.Middleware{api.Methods("GET")}},
"/media/upload": { "/media/upload": {
Handler: api.HandleUploadMedia, Handler: media.HandleUpload,
Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT}}, Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT}},
"/media/verify": { "/media/verify": {
Handler: api.HandleVerifyMedia, Handler: media.HandleVerify,
Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT}, Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT},
}, },
// Paginated media response // Paginated media response
"/media/": { "/media/": {
Handler: api.HandleGetMedia, Handler: media.HandleMedia,
Middlewares: []core.Middleware{api.Methods("GET")}, Middlewares: []core.Middleware{api.Methods("GET")},
}, },
// Unique element // Unique element
"/media/{media_uuid}": { "/media/{media_uuid}": {
Handler: api.HandleGetMediaDetails, Handler: media.HandleMediaDetails,
Middlewares: []core.Middleware{api.Methods("GET")}, Middlewares: []core.Middleware{api.Methods("GET")},
}, },
// Get the image, video, GIF etc. // Get the image, video, GIF etc.
"/media/{media_uuid}/file": { "/media/{media_uuid}/file": {
Handler: api.HandleGetMediaFile, Handler: media.HandleMediaFile,
Middlewares: []core.Middleware{api.Methods("GET")}, Middlewares: []core.Middleware{api.Methods("GET")},
}, },
// "/media/{media_uuid}/update": { // "/media/{media_uuid}/update": {
@@ -153,29 +141,81 @@ func main() {
// Middlewares: []core.Middleware{api.Methods("PATCH"), api.AuthJWT}, // Middlewares: []core.Middleware{api.Methods("PATCH"), api.AuthJWT},
// }, // },
"/media/{media_uuid}/delete": { "/media/{media_uuid}/delete": {
Handler: api.HandleDeleteMedia, Handler: media.HandleDelete,
Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT}, Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT},
}, },
"/shortcodes/new": { "/shortcodes/new": {
Handler: api.HandleCreateShortcode, Handler: shortcodes.HandleNew,
Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT}, Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT},
}, },
"/shortcodes/": { "/shortcodes/": {
Handler: api.HandleGetShortcodes, Handler: shortcodes.HandleShortcodes,
Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT}, Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT},
}, },
"/shortcodes/{shortcode}": { "/shortcodes/{shortcode}": {
Handler: api.HandleGetShortcode, Handler: shortcodes.HandleShortcode,
Middlewares: []core.Middleware{api.Methods("GET")}, Middlewares: []core.Middleware{api.Methods("GET")},
}, },
"/shortcodes/{shortcode}/delete": { "/shortcodes/{shortcode}/delete": {
Handler: api.HandleDeleteShortcode, Handler: shortcodes.HandleDelete,
Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT}, Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT},
}, },
"/shortcodes/{shortcode}/update": { "/shortcodes/{shortcode}/update": {
Handler: api.HandleUpdateShortcode, Handler: shortcodes.HandleUpdate,
Middlewares: []core.Middleware{api.Methods("PATCH"), api.AuthJWT}, Middlewares: []core.Middleware{api.Methods("PATCH"), api.AuthJWT},
}, },
// "/roles": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT},
// },
// "/roles/new": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT},
// },
// "/roles/{role_uuid}": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT},
// },
// "/roles/{role_uuid}/update": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("PATCH"), api.AuthJWT},
// },
// "/roles/{role_uuid}/delete": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT},
// },
// "/permissions": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT},
// },
// "/permissions/new": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT},
// },
// "/permissions/{permission_id}": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("PATCH"), api.AuthJWT},
// },
// "/permissions/{permission_id}/update": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("PATCH"), api.AuthJWT},
// },
// "/permissions/{permission_id}/delete": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT},
// },
// "/roles/{role_uuid}/permissions/": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT},
// },
// "/roles/{role_uuid}/permissions/{permission_id}/add": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT},
// },
// "/roles/{role_uuid}/permissions/{permission_id}/remove": {
// Handler: nil,
// Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT},
// },
"/contact": { "/contact": {
Handler: api.HandleContact, Handler: api.HandleContact,
Middlewares: []core.Middleware{api.Methods("POST"), CSRFMiddleware}, Middlewares: []core.Middleware{api.Methods("POST"), CSRFMiddleware},
@@ -187,7 +227,7 @@ func main() {
}) })
fmt.Printf("Serving on port %s\n", port) fmt.Printf("Serving on port %s\n", port)
err = http.ListenAndServe(fmt.Sprintf(":%s", port), Cors(mux)) err = http.ListenAndServe(fmt.Sprintf(":%s", port), api.CORS(mux))
if err != nil { if err != nil {
fmt.Printf("Error starting server: %s\n", err) fmt.Printf("Error starting server: %s\n", err)
} }

View File

@@ -1,233 +0,0 @@
"use client";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import useApiMutation from "@/hooks/use-api";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const formSchema = z.object({
firstname: z.string().min(2, { message: "Prénom trop court." }),
lastname: z.string().min(2, { message: "Nom trop court." }),
role: z.enum(["admin", "user"], { message: "Rôle invalide." }),
phone: z
.string()
.max(10, { message: "Un numéro de téléphone à 10 chiffres." })
.optional(),
email: z.string().email({ message: "Email invalide." })
});
export default function CreateMemberForm() {
const {
trigger
} = useApiMutation(
"/users/new",
{ onSuccess: () => console.log("Member created") },
"POST",
true,
false
)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
firstname: "",
lastname: "",
role: "user",
phone: "",
email: "",
},
});
async function onSubmit(values: z.infer<typeof formSchema>) {
try {
const res = await trigger(values)
if (!res) throw new Error("The server hasn't responded.");
if (res.status === "Error") throw new Error(res.message);
} catch (error) {
console.error("Form submission error", error);
}
console.log("submited");
}
return (
<div className="flex min-h-[60vh] h-full w-full items-center justify-center px-4">
<Card className="mx-auto max-w-md">
<CardHeader>
<CardTitle className="text-2xl">Création de membre</CardTitle>
<CardDescription>
Remplissez les différents champs pour créer un membre.
Un code sera envoyé par mail à l'utilisateur.
Il pourra ensuite modifier le mot de passe de son compte avec ce code.
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8"
>
<div className="grid gap-4">
{/* Firstname Field */}
<FormField
control={form.control}
name="firstname"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="name">
Prénom
</FormLabel>
<FormControl>
<Input
id="firstname"
placeholder="John Doe"
type="text"
autoComplete="firstname"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid gap-4">
{/* Firstname Field */}
<FormField
control={form.control}
name="lastname"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="lastname">
Nom
</FormLabel>
<FormControl>
<Input
id="lastname"
placeholder="John Doe"
type="text"
autoComplete="lastname"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Email Field */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="email">
Email
</FormLabel>
<FormControl>
<Input
id="email"
placeholder="johndoe@mail.com"
type="email"
autoComplete="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="role">
Role
</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(value: string) => field.onChange(value)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Role</SelectLabel>
<SelectItem value="admin">Administrateur</SelectItem>
<SelectItem value="user">Utilisateur</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Phone Field */}
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="phone">
Phone number
</FormLabel>
<FormControl>
<Input
id="phone"
placeholder="0648265441"
type="tel"
autoComplete="phone"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Add member
</Button>
</div>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,10 @@
"use server";
import MembersTable from "@/components/members-table";
export default async function Page({}) {
return (
<div className="container mx-auto px-4 py-10">
<MembersTable />
</div>
);
}

View File

@@ -55,8 +55,8 @@ const data = {
isActive: true, isActive: true,
items: [ items: [
{ {
title: "Création d'un membre", title: "Liste des membres",
url: "/dashboard/members/new", url: "/dashboard/members",
}, },
], ],
}, },

View File

@@ -0,0 +1,280 @@
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "./ui/select";
const memberSchema = z.object({
userId: z.string().optional(),
firstname: z.string().min(1, "Prénom est requis."),
lastname: z.string().min(1, "Nom de famille est requis."),
email: z.string().email("Adresse email invalide."),
password: z
.string()
.min(6, "Le mot de passe doit avoir au moins 6 caractères.")
.optional(),
phone: z.string().regex(/^\d{10}$/, "Le numéro doit avoir 10 chiffres."),
role: z.string().min(1, "Le rôle est requis."),
});
const updateMemberSchema = memberSchema.partial();
export type Member = z.infer<typeof memberSchema>;
interface MemberDialogProps {
isOpen: boolean;
onClose: () => void;
member: Member | null;
onSave: (member: Member) => void;
}
export default function MemberDialog({
isOpen,
onClose,
member,
onSave,
}: MemberDialogProps) {
const schema = member?.userId ? updateMemberSchema : memberSchema;
const form = useForm<Member>({
resolver: zodResolver(schema),
defaultValues: member?.userId
? member
: {
userId: "",
firstname: "",
lastname: "",
email: "",
password: "",
phone: "",
role: "",
},
});
useEffect(() => {
if (member) {
form.reset(member);
} else {
form.reset({
userId: "",
firstname: "",
lastname: "",
email: "",
password: "",
phone: "",
role: "",
});
}
}, [member, form]);
const onSubmit = (data: Member) => {
onSave(data);
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{member
? "Mise à jour du membre"
: "Créer un nouveau membre"}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8"
>
<div className="grid gap-4">
{/* Firstname Field */}
<FormField
control={form.control}
name="firstname"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="name">
Prénom
</FormLabel>
<FormControl>
<Input
id="firstname"
placeholder="John Doe"
type="text"
autoComplete="firstname"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid gap-4">
{/* Firstname Field */}
<FormField
control={form.control}
name="lastname"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="lastname">
Nom
</FormLabel>
<FormControl>
<Input
id="lastname"
placeholder="John Doe"
type="text"
autoComplete="lastname"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Email Field */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="email">
Email
</FormLabel>
<FormControl>
<Input
id="email"
placeholder="johndoe@mail.com"
type="email"
autoComplete="email webauthn"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Password Field */}
{!member?.userId && (
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="password">
Mot de passe
</FormLabel>
<FormControl>
<Input
id="password"
placeholder=""
type="password"
autoComplete="new-password webauthn"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="role">
Role
</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(
value: string,
) => field.onChange(value)}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Séléctionner un rôle" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
Role
</SelectLabel>
<SelectItem value="admin">
Administrateur
</SelectItem>
<SelectItem value="user">
Utilisateur
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Phone Field */}
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel htmlFor="phone">
Phone number
</FormLabel>
<FormControl>
<Input
id="phone"
placeholder="0648265441"
type="tel"
autoComplete="phone"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<DialogFooter>
<Button type="submit">
{member ? "Actualiser" : "Ajouter"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@@ -12,93 +12,28 @@ import {
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { UpdateMemberDialog } from "./UpdateMemberDialog"; import MemberDialog, { Member } from "./member-dialog";
import { AddMemberDialog } from "./AddMemberDialog"; import * as z from "zod";
import { request, useApi } from "@/hooks/use-api";
import {
CircleX,
Loader2,
Trash2,
UserRoundPen,
UserRoundPlus,
} from "lucide-react";
interface Member { export default function MembersTable() {
user_id: string; const {
firstname: string; data: members,
lastname: string; error,
email: string; mutate,
password: string; success,
phone: string; isLoading,
role: string; } = useApi<Member[]>("/users", undefined, true, false);
}
const initialMembers: Member[] = [
// Add some sample data here
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
// Add more sample members...
];
export function MembersTable() {
const [members, setMembers] = useState<Member[]>(initialMembers);
const [selectMode, setSelectMode] = useState(false); const [selectMode, setSelectMode] = useState(false);
const [selectedMembers, setSelectedMembers] = useState<string[]>([]); const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
const [updateDialogOpen, setUpdateDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [currentMember, setCurrentMember] = useState<Member | null>(null); const [currentMember, setCurrentMember] = useState<Member | null>(null);
const toggleSelectMode = () => { const toggleSelectMode = () => {
@@ -114,124 +49,135 @@ export function MembersTable() {
); );
}; };
const handleUpdate = (member: Member) => { const handleOpenDialog = (member: Member | null) => {
setCurrentMember(member); setCurrentMember(member);
setUpdateDialogOpen(true); setDialogOpen(true);
}; };
const handleDelete = (userId: string) => { const handleSaveMember = async (savedMember: Member) => {
setMembers((prev) => if (savedMember.userId) {
prev.filter((member) => member.user_id !== userId), const res = await request<unknown>(
); `/users/${savedMember.userId}/update`,
{
body: savedMember,
requiresAuth: true,
method: "PATCH",
},
);
if (res.status === "Success") mutate();
// Update existing member
// setMembers((prev) =>
// prev.map((m) =>
// m.user_id === savedMember.user_id ? savedMember : m,
// ),
// );
} else {
delete savedMember.userId;
const res = await request<unknown>("/users/new", {
body: savedMember,
method: "POST",
requiresAuth: true,
});
if (res.status === "Success") mutate();
}
}; };
const handleAdd = (newMember: Member) => { const handleDelete = async (userId: string) => {
setMembers((prev) => [ const res = await request<unknown>(`/users/${userId}/delete`, {
...prev, method: "DELETE",
{ ...newMember, user_id: String(prev.length + 1) }, requiresAuth: true,
]); });
if (res.status === "Success") mutate();
}; };
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between"> <div className="flex justify-between">
<Button onClick={toggleSelectMode}> <Button onClick={toggleSelectMode}>
{selectMode ? "Cancel Selection" : "Select"} {selectMode ? <CircleX /> : "Selectionner"}
</Button> </Button>
<Button onClick={() => setAddDialogOpen(true)}> <Button onClick={() => handleOpenDialog(null)}>
Add New Member <UserRoundPlus />
</Button> </Button>
</div> </div>
<div className="relative"> <div className="relative">
<ScrollArea className="h-[400px] rounded-md border"> <ScrollArea className="h-full rounded-md border">
<Table> <Table>
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm"> <TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
<TableRow> <TableRow>
{selectMode && ( {selectMode && (
<TableHead className="w-[50px]"> <TableHead className="w-[50px]">
Select Selectionner
</TableHead> </TableHead>
)} )}
<TableHead>User ID</TableHead> <TableHead>Prénom</TableHead>
<TableHead>First Name</TableHead> <TableHead>Nom</TableHead>
<TableHead>Last Name</TableHead>
<TableHead>Email</TableHead> <TableHead>Email</TableHead>
<TableHead>Password</TableHead> <TableHead>Téléphone</TableHead>
<TableHead>Phone</TableHead> <TableHead>Rôle</TableHead>
<TableHead>Role</TableHead>
<TableHead className="text-right"> <TableHead className="text-right">
Actions Actions
</TableHead> </TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{members.map((member) => ( {isLoading && <Loader2 className="animate-spin" />}
<TableRow key={member.user_id}> {members &&
{selectMode && ( members.map((member) => (
<TableRow key={member.userId}>
{selectMode && (
<TableCell>
<Checkbox
checked={selectedMembers.includes(
member.userId!,
)}
onCheckedChange={() =>
toggleMemberSelection(
member.userId!,
)
}
/>
</TableCell>
)}
<TableCell> <TableCell>
<Checkbox {member.firstname}
checked={selectedMembers.includes(
member.user_id,
)}
onCheckedChange={() =>
toggleMemberSelection(
member.user_id,
)
}
/>
</TableCell> </TableCell>
)} <TableCell>{member.lastname}</TableCell>
<TableCell>{member.user_id}</TableCell> <TableCell>{member.email}</TableCell>
<TableCell>{member.firstname}</TableCell> <TableCell>{member.phone}</TableCell>
<TableCell>{member.lastname}</TableCell> <TableCell>{member.role}</TableCell>
<TableCell>{member.email}</TableCell> <TableCell className="text-right">
<TableCell>{member.password}</TableCell> <Button
<TableCell>{member.phone}</TableCell> variant="outline"
<TableCell>{member.role}</TableCell> size="sm"
<TableCell className="text-right"> className="mr-2"
<Button onClick={() =>
variant="outline" handleOpenDialog(member)
size="sm" }
className="mr-2" >
onClick={() => handleUpdate(member)} <UserRoundPen />
> </Button>
Modify <Button
</Button> variant="destructive"
<Button size="sm"
variant="destructive" onClick={() =>
size="sm" handleDelete(member.userId!)
onClick={() => }
handleDelete(member.user_id) >
} <Trash2 />
> </Button>
Delete </TableCell>
</Button> </TableRow>
</TableCell> ))}
</TableRow>
))}
</TableBody> </TableBody>
</Table> </Table>
</ScrollArea> </ScrollArea>
</div> </div>
<UpdateMemberDialog <MemberDialog
isOpen={updateDialogOpen} isOpen={dialogOpen}
onClose={() => setUpdateDialogOpen(false)} onClose={() => setDialogOpen(false)}
member={currentMember} member={currentMember}
onUpdate={(updatedMember) => { onSave={handleSaveMember}
setMembers((prev) =>
prev.map((m) =>
m.user_id === updatedMember.user_id
? updatedMember
: m,
),
);
setUpdateDialogOpen(false);
}}
/>
<AddMemberDialog
isOpen={addDialogOpen}
onClose={() => setAddDialogOpen(false)}
onAdd={handleAdd}
/> />
</div> </div>
); );