mirror of
https://github.com/Dvorinka/Productier.git
synced 2026-06-03 20:13:01 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,570 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"productier/apps/backend/internal/authsession"
|
||||
"productier/apps/backend/internal/store"
|
||||
)
|
||||
|
||||
const sessionContextKey = "sessionUser"
|
||||
|
||||
func (s *Server) registerRoutes() {
|
||||
v1 := s.engine.Group("/v1")
|
||||
{
|
||||
v1.GET("/health", func(c *gin.Context) {
|
||||
now := time.Now().UTC()
|
||||
probeCtx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
storageStatus := gin.H{
|
||||
"provider": s.files.Provider(),
|
||||
"ok": true,
|
||||
}
|
||||
if err := s.files.Probe(probeCtx); err != nil {
|
||||
storageStatus["ok"] = false
|
||||
storageStatus["error"] = err.Error()
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"ok": false,
|
||||
"mode": s.mode,
|
||||
"timestamp": now,
|
||||
"storage": storageStatus,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"ok": true,
|
||||
"mode": s.mode,
|
||||
"timestamp": now,
|
||||
"storage": storageStatus,
|
||||
})
|
||||
})
|
||||
|
||||
v1.GET("/metrics", func(c *gin.Context) {
|
||||
if !s.authorizeMetricsRequest(c) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, s.metrics.snapshot())
|
||||
})
|
||||
|
||||
v1.GET("/metrics/prometheus", func(c *gin.Context) {
|
||||
if !s.authorizeMetricsRequest(c) {
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "text/plain; version=0.0.4; charset=utf-8", []byte(s.metrics.snapshotPrometheus()))
|
||||
})
|
||||
|
||||
v1.GET("/invites/:token", func(c *gin.Context) {
|
||||
invite, err := s.store.GetInviteByToken(c.Param("token"))
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": invite})
|
||||
})
|
||||
}
|
||||
|
||||
authorized := v1.Group("/")
|
||||
authorized.Use(s.requireSession())
|
||||
{
|
||||
authorized.GET("/workspaces", func(c *gin.Context) {
|
||||
user := s.sessionUser(c)
|
||||
workspaces := s.store.ListWorkspaces()
|
||||
visible := make([]store.Workspace, 0, len(workspaces))
|
||||
for _, workspace := range workspaces {
|
||||
if _, ok := s.requireWorkspaceMemberByEmail(workspace.Slug, user.Email); ok {
|
||||
visible = append(visible, workspace)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": visible})
|
||||
})
|
||||
|
||||
authorized.GET("/members", func(c *gin.Context) {
|
||||
workspaceSlug := c.Query("workspaceSlug")
|
||||
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": s.store.ListMembers(workspaceSlug)})
|
||||
})
|
||||
|
||||
authorized.PATCH("/members/:memberId", func(c *gin.Context) {
|
||||
member, err := s.store.GetMemberByID(c.Param("memberId"))
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
actingMember, ok := s.requireWorkspaceMember(c, member.WorkspaceSlug)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if actingMember.Role != "owner" && actingMember.Role != "admin" {
|
||||
s.writeStatusError(c, http.StatusForbidden, "member management permissions required")
|
||||
return
|
||||
}
|
||||
|
||||
var input store.UpdateMemberInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := s.store.UpdateMember(member.ID, input)
|
||||
if err != nil {
|
||||
switch err.Error() {
|
||||
case "invalid member role", "invalid member status":
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
case "workspace must have at least one active owner":
|
||||
s.writeStatusError(c, http.StatusConflict, err.Error())
|
||||
default:
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": updated})
|
||||
})
|
||||
|
||||
authorized.GET("/invites", func(c *gin.Context) {
|
||||
workspaceSlug := c.Query("workspaceSlug")
|
||||
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": s.store.ListInvites(workspaceSlug)})
|
||||
})
|
||||
|
||||
authorized.POST("/invites", func(c *gin.Context) {
|
||||
var input store.CreateInviteInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
member, ok := s.requireWorkspaceMember(c, input.WorkspaceSlug)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if member.Role != "owner" && member.Role != "admin" {
|
||||
s.writeStatusError(c, http.StatusForbidden, "invite permissions required")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"data": s.store.CreateInvite(input)})
|
||||
})
|
||||
|
||||
authorized.POST("/invites/:token/revoke", func(c *gin.Context) {
|
||||
invite, err := s.store.GetInviteByID(c.Param("token"))
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
member, ok := s.requireWorkspaceMember(c, invite.WorkspaceSlug)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if member.Role != "owner" && member.Role != "admin" {
|
||||
s.writeStatusError(c, http.StatusForbidden, "invite permissions required")
|
||||
return
|
||||
}
|
||||
if err := s.store.RevokeInvite(invite.ID); err != nil {
|
||||
switch err.Error() {
|
||||
case "only pending invites can be revoked":
|
||||
s.writeStatusError(c, http.StatusConflict, err.Error())
|
||||
default:
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
})
|
||||
|
||||
authorized.POST("/invites/:token/accept", func(c *gin.Context) {
|
||||
var input store.AcceptInviteInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user := s.sessionUser(c)
|
||||
invite, err := s.store.AcceptInvite(c.Param("token"), store.AcceptInviteInput{
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
})
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": invite})
|
||||
})
|
||||
|
||||
authorized.GET("/activity", func(c *gin.Context) {
|
||||
workspaceSlug := c.Query("workspaceSlug")
|
||||
if strings.TrimSpace(workspaceSlug) == "" {
|
||||
s.writeStatusError(c, http.StatusBadRequest, "workspaceSlug is required")
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
params, err := parseActivityListParams(c.Query("limit"), c.Query("type"), c.Query("q"))
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
activities := s.store.ListActivities(workspaceSlug)
|
||||
c.JSON(http.StatusOK, gin.H{"data": filterActivityEntries(activities, params)})
|
||||
})
|
||||
|
||||
authorized.GET("/board-groups", func(c *gin.Context) {
|
||||
workspaceSlug := c.Query("workspaceSlug")
|
||||
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": s.store.ListBoardGroups(workspaceSlug)})
|
||||
})
|
||||
|
||||
authorized.POST("/board-groups", func(c *gin.Context) {
|
||||
var input store.CreateBoardGroupInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, input.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"data": s.store.CreateBoardGroup(input)})
|
||||
})
|
||||
|
||||
authorized.PATCH("/board-groups/:groupId", func(c *gin.Context) {
|
||||
group, err := s.store.GetBoardGroupByID(c.Param("groupId"))
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, group.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var input store.UpdateBoardGroupInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := s.store.UpdateBoardGroup(c.Param("groupId"), input)
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": updated})
|
||||
})
|
||||
|
||||
authorized.GET("/labels", func(c *gin.Context) {
|
||||
workspaceSlug := c.Query("workspaceSlug")
|
||||
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": s.store.ListLabels(workspaceSlug)})
|
||||
})
|
||||
|
||||
authorized.POST("/labels", func(c *gin.Context) {
|
||||
var input store.CreateLabelInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, input.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"data": s.store.CreateLabel(input)})
|
||||
})
|
||||
|
||||
authorized.GET("/tasks", func(c *gin.Context) {
|
||||
workspaceSlug := c.Query("workspaceSlug")
|
||||
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": s.store.ListTasks(workspaceSlug)})
|
||||
})
|
||||
|
||||
authorized.POST("/tasks", func(c *gin.Context) {
|
||||
var input store.CreateTaskInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, input.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
task := s.store.CreateTask(input)
|
||||
|
||||
// Trigger webhooks for task creation
|
||||
s.store.TriggerWebhooks(input.WorkspaceSlug, "task.created", map[string]interface{}{
|
||||
"taskId": task.ID,
|
||||
"title": task.Title,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"data": task})
|
||||
})
|
||||
|
||||
authorized.PATCH("/tasks/:taskId", func(c *gin.Context) {
|
||||
task, err := s.store.GetTaskByID(c.Param("taskId"))
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, task.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var input store.UpdateTaskInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check if assignee is being set (task assignment notification)
|
||||
if input.AssigneeID != nil && *input.AssigneeID != "" {
|
||||
// Get assignee email from member ID
|
||||
members := s.store.ListMembers(task.WorkspaceSlug)
|
||||
for _, member := range members {
|
||||
if member.ID == *input.AssigneeID && member.Status == "active" {
|
||||
// Create notification for the assignee
|
||||
s.store.CreateNotificationForTaskAssignment(
|
||||
task.WorkspaceSlug,
|
||||
member.Email,
|
||||
task.Title,
|
||||
task.ID,
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if status is being changed to done (task completion notification)
|
||||
if input.Status != nil && *input.Status == "done" && task.Status != "done" && task.AssigneeID != nil {
|
||||
// Notify the task creator or workspace owner
|
||||
members := s.store.ListMembers(task.WorkspaceSlug)
|
||||
for _, member := range members {
|
||||
if member.Role == "owner" || member.Role == "admin" {
|
||||
s.store.CreateNotificationForTaskCompletion(
|
||||
task.WorkspaceSlug,
|
||||
member.Email,
|
||||
task.Title,
|
||||
task.ID,
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := s.store.UpdateTask(c.Param("taskId"), input)
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger webhooks for task updates
|
||||
s.store.TriggerWebhooks(task.WorkspaceSlug, "task.updated", map[string]interface{}{
|
||||
"taskId": task.ID,
|
||||
"title": task.Title,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": updated})
|
||||
})
|
||||
|
||||
authorized.GET("/calendar/events", func(c *gin.Context) {
|
||||
workspaceSlug := c.Query("workspaceSlug")
|
||||
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": s.store.ListEvents(workspaceSlug)})
|
||||
})
|
||||
|
||||
authorized.POST("/calendar/events", func(c *gin.Context) {
|
||||
var input store.CreateEventInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, input.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"data": s.store.CreateEvent(input)})
|
||||
})
|
||||
|
||||
authorized.PATCH("/calendar/events/:eventId", func(c *gin.Context) {
|
||||
event, err := s.store.GetEventByID(c.Param("eventId"))
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, event.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var input store.UpdateEventInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := s.store.UpdateEvent(c.Param("eventId"), input)
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": updated})
|
||||
})
|
||||
|
||||
authorized.GET("/notes", func(c *gin.Context) {
|
||||
workspaceSlug := c.Query("workspaceSlug")
|
||||
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": s.store.ListNotes(workspaceSlug)})
|
||||
})
|
||||
|
||||
authorized.POST("/notes", func(c *gin.Context) {
|
||||
var input store.CreateNoteInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, input.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"data": s.store.CreateNote(input)})
|
||||
})
|
||||
|
||||
authorized.PATCH("/notes/:noteId", func(c *gin.Context) {
|
||||
note, err := s.store.GetNoteByID(c.Param("noteId"))
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, note.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var input store.UpdateNoteInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := s.store.UpdateNote(c.Param("noteId"), input)
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": updated})
|
||||
})
|
||||
|
||||
authorized.GET("/focus/sessions", func(c *gin.Context) {
|
||||
workspaceSlug := c.Query("workspaceSlug")
|
||||
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": s.store.ListFocusSessions(workspaceSlug)})
|
||||
})
|
||||
|
||||
authorized.POST("/focus/sessions", func(c *gin.Context) {
|
||||
var input store.CreateFocusSessionInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, input.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"data": s.store.CreateFocusSession(input)})
|
||||
})
|
||||
|
||||
authorized.PATCH("/focus/sessions/:sessionId", func(c *gin.Context) {
|
||||
session, err := s.store.GetFocusSessionByID(c.Param("sessionId"))
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
if _, ok := s.requireWorkspaceMember(c, session.WorkspaceSlug); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var input store.UpdateFocusSessionInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
s.writeStatusError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := s.store.UpdateFocusSession(c.Param("sessionId"), input)
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": updated})
|
||||
})
|
||||
|
||||
s.registerTaskAttachmentRoutes(authorized)
|
||||
s.registerMailRoutes(authorized)
|
||||
s.registerCRMRoutes(authorized)
|
||||
s.registerProductivityRoutes(authorized)
|
||||
s.registerIntegrationRoutes(authorized)
|
||||
s.registerOAuthRoutes(authorized)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) requireSession() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
user, err := s.authClient.GetUser(c.Request.Context(), c.GetHeader("Cookie"))
|
||||
if err != nil {
|
||||
s.writeStatusError(c, http.StatusUnauthorized, "session lookup failed")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
s.writeStatusError(c, http.StatusUnauthorized, "authentication required")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set(sessionContextKey, user)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) sessionUser(c *gin.Context) *authsession.User {
|
||||
value, exists := c.Get(sessionContextKey)
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
user, _ := value.(*authsession.User)
|
||||
return user
|
||||
}
|
||||
|
||||
func (s *Server) requireWorkspaceMember(c *gin.Context, workspaceSlug string) (store.Member, bool) {
|
||||
user := s.sessionUser(c)
|
||||
if user == nil {
|
||||
s.writeStatusError(c, http.StatusUnauthorized, "authentication required")
|
||||
return store.Member{}, false
|
||||
}
|
||||
member, ok := s.requireWorkspaceMemberByEmail(workspaceSlug, user.Email)
|
||||
if !ok {
|
||||
s.writeStatusError(c, http.StatusForbidden, "workspace membership required")
|
||||
return store.Member{}, false
|
||||
}
|
||||
return member, true
|
||||
}
|
||||
|
||||
func (s *Server) requireWorkspaceMemberByEmail(workspaceSlug string, email string) (store.Member, bool) {
|
||||
for _, member := range s.store.ListMembers(workspaceSlug) {
|
||||
if strings.EqualFold(member.Email, email) && member.Status == "active" {
|
||||
return member, true
|
||||
}
|
||||
}
|
||||
return store.Member{}, false
|
||||
}
|
||||
Reference in New Issue
Block a user