Compare commits

...

2 Commits

Author SHA1 Message Date
24f1aca97a fix: return updated lesson from UpdateSubModuleLesson after is_active false
GetSubModuleLessonByID filters is_active=true, so refetch failed with 500
after soft-deactivating. Use RETURNING row from the update instead.

Made-with: Cursor
2026-04-18 02:54:47 -07:00
ce1b827768 refresh token fix 2026-04-17 10:16:25 -07:00
17 changed files with 573 additions and 39 deletions

View File

@ -414,7 +414,7 @@ func main() {
)
// Team management service
teamSvc := team.NewService(repository.NewTeamStore(store))
teamSvc := team.NewService(repository.NewTeamStore(store), cfg.RefreshExpiry)
// santimpayClient := santimpay.NewSantimPayClient(cfg)

View File

@ -0,0 +1,2 @@
DROP INDEX IF EXISTS idx_team_refresh_tokens_team_member_id;
DROP TABLE IF EXISTS team_refresh_tokens;

View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS team_refresh_tokens (
id BIGSERIAL PRIMARY KEY,
team_member_id BIGINT NOT NULL REFERENCES team_members(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_team_refresh_tokens_team_member_id
ON team_refresh_tokens (team_member_id);

View File

@ -0,0 +1,19 @@
-- name: RevokeAllActiveTeamRefreshTokensForMember :exec
UPDATE team_refresh_tokens
SET revoked = TRUE
WHERE team_member_id = $1
AND revoked = FALSE;
-- name: CreateTeamRefreshToken :exec
INSERT INTO team_refresh_tokens (team_member_id, token, expires_at, revoked, created_at)
VALUES ($1, $2, $3, $4, $5);
-- name: GetTeamRefreshTokenByToken :one
SELECT *
FROM team_refresh_tokens
WHERE token = $1;
-- name: RevokeTeamRefreshTokenByToken :exec
UPDATE team_refresh_tokens
SET revoked = TRUE
WHERE token = $1;

View File

@ -7682,6 +7682,70 @@ const docTemplate = `{
}
}
},
"/api/v1/team/refresh": {
"post": {
"description": "Exchanges a valid team refresh token for a new access JWT and a rotated refresh token (use only tokens from POST /team/login or this endpoint).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"team"
],
"summary": "Refresh team member tokens",
"parameters": [
{
"description": "Current refresh token",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.teamMemberRefreshReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/handlers.teamMemberLoginRes"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/team/stats": {
"get": {
"security": [
@ -11609,7 +11673,7 @@ const docTemplate = `{
},
"refresh_token": {
"type": "string",
"example": ""
"example": "\u003copaque-refresh-token\u003e"
},
"team_role": {
"type": "string",
@ -11617,6 +11681,17 @@ const docTemplate = `{
}
}
},
"handlers.teamMemberRefreshReq": {
"type": "object",
"required": [
"refresh_token"
],
"properties": {
"refresh_token": {
"type": "string"
}
}
},
"handlers.updateAdminReq": {
"type": "object",
"properties": {

View File

@ -7674,6 +7674,70 @@
}
}
},
"/api/v1/team/refresh": {
"post": {
"description": "Exchanges a valid team refresh token for a new access JWT and a rotated refresh token (use only tokens from POST /team/login or this endpoint).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"team"
],
"summary": "Refresh team member tokens",
"parameters": [
{
"description": "Current refresh token",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.teamMemberRefreshReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/handlers.teamMemberLoginRes"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/team/stats": {
"get": {
"security": [
@ -11601,7 +11665,7 @@
},
"refresh_token": {
"type": "string",
"example": ""
"example": "\u003copaque-refresh-token\u003e"
},
"team_role": {
"type": "string",
@ -11609,6 +11673,17 @@
}
}
},
"handlers.teamMemberRefreshReq": {
"type": "object",
"required": [
"refresh_token"
],
"properties": {
"refresh_token": {
"type": "string"
}
}
},
"handlers.updateAdminReq": {
"type": "object",
"properties": {

View File

@ -1448,12 +1448,19 @@ definitions:
example: 1
type: integer
refresh_token:
example: ""
example: <opaque-refresh-token>
type: string
team_role:
example: admin
type: string
type: object
handlers.teamMemberRefreshReq:
properties:
refresh_token:
type: string
required:
- refresh_token
type: object
handlers.updateAdminReq:
properties:
first_name:
@ -7117,6 +7124,46 @@ paths:
summary: Update team member status
tags:
- team
/api/v1/team/refresh:
post:
consumes:
- application/json
description: Exchanges a valid team refresh token for a new access JWT and a
rotated refresh token (use only tokens from POST /team/login or this endpoint).
parameters:
- description: Current refresh token
in: body
name: body
required: true
schema:
$ref: '#/definitions/handlers.teamMemberRefreshReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
$ref: '#/definitions/handlers.teamMemberLoginRes'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Refresh team member tokens
tags:
- team
/api/v1/team/stats:
get:
consumes:

View File

@ -477,6 +477,15 @@ type TeamMember struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type TeamRefreshToken struct {
ID int64 `json:"id"`
TeamMemberID int64 `json:"team_member_id"`
Token string `json:"token"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
Revoked bool `json:"revoked"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type User struct {
ID int64 `json:"id"`
FirstName pgtype.Text `json:"first_name"`

View File

@ -0,0 +1,79 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: team_refresh_tokens.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateTeamRefreshToken = `-- name: CreateTeamRefreshToken :exec
INSERT INTO team_refresh_tokens (team_member_id, token, expires_at, revoked, created_at)
VALUES ($1, $2, $3, $4, $5)
`
type CreateTeamRefreshTokenParams struct {
TeamMemberID int64 `json:"team_member_id"`
Token string `json:"token"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
Revoked bool `json:"revoked"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
func (q *Queries) CreateTeamRefreshToken(ctx context.Context, arg CreateTeamRefreshTokenParams) error {
_, err := q.db.Exec(ctx, CreateTeamRefreshToken,
arg.TeamMemberID,
arg.Token,
arg.ExpiresAt,
arg.Revoked,
arg.CreatedAt,
)
return err
}
const GetTeamRefreshTokenByToken = `-- name: GetTeamRefreshTokenByToken :one
SELECT id, team_member_id, token, expires_at, revoked, created_at
FROM team_refresh_tokens
WHERE token = $1
`
func (q *Queries) GetTeamRefreshTokenByToken(ctx context.Context, token string) (TeamRefreshToken, error) {
row := q.db.QueryRow(ctx, GetTeamRefreshTokenByToken, token)
var i TeamRefreshToken
err := row.Scan(
&i.ID,
&i.TeamMemberID,
&i.Token,
&i.ExpiresAt,
&i.Revoked,
&i.CreatedAt,
)
return i, err
}
const RevokeAllActiveTeamRefreshTokensForMember = `-- name: RevokeAllActiveTeamRefreshTokensForMember :exec
UPDATE team_refresh_tokens
SET revoked = TRUE
WHERE team_member_id = $1
AND revoked = FALSE
`
func (q *Queries) RevokeAllActiveTeamRefreshTokensForMember(ctx context.Context, teamMemberID int64) error {
_, err := q.db.Exec(ctx, RevokeAllActiveTeamRefreshTokensForMember, teamMemberID)
return err
}
const RevokeTeamRefreshTokenByToken = `-- name: RevokeTeamRefreshTokenByToken :exec
UPDATE team_refresh_tokens
SET revoked = TRUE
WHERE token = $1
`
func (q *Queries) RevokeTeamRefreshTokenByToken(ctx context.Context, token string) error {
_, err := q.db.Exec(ctx, RevokeTeamRefreshTokenByToken, token)
return err
}

View File

@ -6,12 +6,14 @@ import (
)
var (
ErrTeamMemberNotFound = errors.New("team member not found")
ErrTeamMemberEmailExists = errors.New("team member email already exists")
ErrInvalidTeamRole = errors.New("invalid team role")
ErrInvalidTeamMemberStatus = errors.New("invalid team member status")
ErrInvalidEmploymentType = errors.New("invalid employment type")
ErrTeamMemberNotFound = errors.New("team member not found")
ErrTeamMemberEmailExists = errors.New("team member email already exists")
ErrInvalidTeamRole = errors.New("invalid team role")
ErrInvalidTeamMemberStatus = errors.New("invalid team member status")
ErrInvalidEmploymentType = errors.New("invalid employment type")
ErrTeamMemberEmailNotVerified = errors.New("team member email not verified")
ErrTeamRefreshTokenNotFound = errors.New("team refresh token not found")
ErrTeamRefreshTokenExpired = errors.New("team refresh token expired")
)
type TeamRole string
@ -80,6 +82,16 @@ func (e EmploymentType) IsValid() bool {
}
}
// TeamRefreshToken is a persisted refresh token for team member sessions (separate from users.refresh_tokens).
type TeamRefreshToken struct {
ID int64
TeamMemberID int64
Token string
ExpiresAt time.Time
Revoked bool
CreatedAt time.Time
}
type TeamMember struct {
ID int64
FirstName string

View File

@ -2,6 +2,7 @@ package ports
import (
"context"
"time"
"Yimaru-Backend/internal/domain"
)
@ -30,4 +31,9 @@ type TeamStore interface {
GetTeamMembersByRole(ctx context.Context, role string) ([]domain.TeamMember, error)
CountTeamMembersByStatus(ctx context.Context) (domain.TeamMemberStats, error)
UpdateTeamMemberEmailVerified(ctx context.Context, memberID int64, verified bool) error
RevokeAllActiveTeamRefreshTokensForMember(ctx context.Context, memberID int64) error
CreateTeamRefreshToken(ctx context.Context, memberID int64, token string, expiresAt, createdAt time.Time) error
GetTeamRefreshTokenByToken(ctx context.Context, token string) (domain.TeamRefreshToken, error)
RevokeTeamRefreshTokenByToken(ctx context.Context, token string) error
}

View File

@ -455,3 +455,42 @@ func mapGetTeamMembersByRoleRow(row dbgen.GetTeamMembersByRoleRow) domain.TeamMe
func parseDate(dateStr string) (time.Time, error) {
return time.Parse("2006-01-02", dateStr)
}
func (s *Store) RevokeAllActiveTeamRefreshTokensForMember(ctx context.Context, memberID int64) error {
return s.queries.RevokeAllActiveTeamRefreshTokensForMember(ctx, memberID)
}
func (s *Store) CreateTeamRefreshToken(ctx context.Context, memberID int64, token string, expiresAt, createdAt time.Time) error {
return s.queries.CreateTeamRefreshToken(ctx, dbgen.CreateTeamRefreshTokenParams{
TeamMemberID: memberID,
Token: token,
ExpiresAt: pgtype.Timestamptz{Time: expiresAt, Valid: true},
Revoked: false,
CreatedAt: pgtype.Timestamptz{Time: createdAt, Valid: true},
})
}
func (s *Store) GetTeamRefreshTokenByToken(ctx context.Context, token string) (domain.TeamRefreshToken, error) {
row, err := s.queries.GetTeamRefreshTokenByToken(ctx, token)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.TeamRefreshToken{}, domain.ErrTeamRefreshTokenNotFound
}
return domain.TeamRefreshToken{}, err
}
if !row.ExpiresAt.Valid || !row.CreatedAt.Valid {
return domain.TeamRefreshToken{}, domain.ErrTeamRefreshTokenNotFound
}
return domain.TeamRefreshToken{
ID: row.ID,
TeamMemberID: row.TeamMemberID,
Token: row.Token,
ExpiresAt: row.ExpiresAt.Time,
Revoked: row.Revoked,
CreatedAt: row.CreatedAt.Time,
}, nil
}
func (s *Store) RevokeTeamRefreshTokenByToken(ctx context.Context, token string) error {
return s.queries.RevokeTeamRefreshTokenByToken(ctx, token)
}

View File

@ -5,11 +5,16 @@ import (
)
type Service struct {
teamStore ports.TeamStore
teamStore ports.TeamStore
refreshExpirySec int
}
func NewService(teamStore ports.TeamStore) *Service {
func NewService(teamStore ports.TeamStore, refreshExpirySeconds int) *Service {
if refreshExpirySeconds <= 0 {
refreshExpirySeconds = 7 * 24 * 3600
}
return &Service{
teamStore: teamStore,
teamStore: teamStore,
refreshExpirySec: refreshExpirySeconds,
}
}

View File

@ -0,0 +1,69 @@
package team
import (
"context"
"crypto/rand"
"encoding/base32"
"time"
"Yimaru-Backend/internal/domain"
)
func generateOpaqueRefreshToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b), nil
}
// IssueRefreshTokenOnLogin revokes prior active team refresh tokens for the member and stores a new one.
func (s *Service) IssueRefreshTokenOnLogin(ctx context.Context, memberID int64) (string, error) {
if err := s.teamStore.RevokeAllActiveTeamRefreshTokensForMember(ctx, memberID); err != nil {
return "", err
}
tok, err := generateOpaqueRefreshToken()
if err != nil {
return "", err
}
now := time.Now()
exp := now.Add(time.Duration(s.refreshExpirySec) * time.Second)
if err := s.teamStore.CreateTeamRefreshToken(ctx, memberID, tok, exp, now); err != nil {
return "", err
}
return tok, nil
}
// RefreshSession validates a team refresh token, rotates it, and returns the member plus the new refresh token plaintext.
func (s *Service) RefreshSession(ctx context.Context, refreshTokenPlain string) (domain.TeamMember, string, error) {
rt, err := s.teamStore.GetTeamRefreshTokenByToken(ctx, refreshTokenPlain)
if err != nil {
return domain.TeamMember{}, "", err
}
if rt.Revoked {
return domain.TeamMember{}, "", domain.ErrTeamRefreshTokenNotFound
}
if time.Now().After(rt.ExpiresAt) {
return domain.TeamMember{}, "", domain.ErrTeamRefreshTokenExpired
}
if err := s.teamStore.RevokeTeamRefreshTokenByToken(ctx, refreshTokenPlain); err != nil {
return domain.TeamMember{}, "", err
}
newTok, err := generateOpaqueRefreshToken()
if err != nil {
return domain.TeamMember{}, "", err
}
now := time.Now()
exp := now.Add(time.Duration(s.refreshExpirySec) * time.Second)
if err := s.teamStore.CreateTeamRefreshToken(ctx, rt.TeamMemberID, newTok, exp, now); err != nil {
return domain.TeamMember{}, "", err
}
member, err := s.teamStore.GetTeamMemberByID(ctx, rt.TeamMemberID)
if err != nil {
return domain.TeamMember{}, "", err
}
if member.Status != domain.TeamMemberStatusActive {
return domain.TeamMember{}, "", domain.ErrInvalidTeamMemberStatus
}
return member, newTok, nil
}

View File

@ -35,11 +35,11 @@ type createCourseReq struct {
}
type updateCourseReq struct {
Title *string `json:"title"`
Description *string `json:"description"`
Thumbnail *string `json:"thumbnail"`
Title *string `json:"title"`
Description *string `json:"description"`
Thumbnail *string `json:"thumbnail"`
IntroVideoURL *string `json:"intro_video_url"`
IsActive *bool `json:"is_active"`
IsActive *bool `json:"is_active"`
}
type updateCourseThumbnailReq struct {
@ -1239,16 +1239,16 @@ func (h *Handler) UnifiedHierarchyByCourse(c *fiber.Ctx) error {
Message: "Course hierarchy retrieved successfully",
Data: []map[string]interface{}{
{
"course_id": course.ID,
"course_title": course.Title,
"level_id": nil,
"cefr_level": nil,
"level_title": nil,
"level_description": nil,
"level_thumbnail": nil,
"module_id": nil,
"module_title": nil,
"module_icon_url": nil,
"course_id": course.ID,
"course_title": course.Title,
"level_id": nil,
"cefr_level": nil,
"level_title": nil,
"level_description": nil,
"level_thumbnail": nil,
"module_id": nil,
"module_title": nil,
"module_icon_url": nil,
"sub_module_id": nil,
"sub_module_title": nil,
"sub_module_description": nil,
@ -1956,7 +1956,7 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error {
targetIsActive = *req.IsActive
}
if _, err := h.analyticsDB.UpdateSubModuleLesson(c.Context(), dbgen.UpdateSubModuleLessonParams{
updatedLesson, err := h.analyticsDB.UpdateSubModuleLesson(c.Context(), dbgen.UpdateSubModuleLessonParams{
SubModuleID: targetSubModuleID,
Title: targetTitle,
Description: targetDescription,
@ -1968,17 +1968,10 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error {
DisplayOrder: targetDisplayOrder,
IsActive: targetIsActive,
ID: lessonID,
}); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update lesson",
Error: err.Error(),
})
}
updatedLesson, err := h.analyticsDB.GetSubModuleLessonByID(c.Context(), lessonID)
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Lesson updated but failed to fetch latest detail",
Message: "Failed to update lesson",
Error: err.Error(),
})
}
@ -2989,4 +2982,3 @@ func (h *Handler) DeleteModuleCapstone(c *fiber.Ctx) error {
}
return c.JSON(domain.Response{Message: "Module capstone deleted"})
}

View File

@ -17,11 +17,15 @@ import (
// teamMemberLoginRes represents the response body for team member login
type teamMemberLoginRes struct {
AccessToken string `json:"access_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
RefreshToken string `json:"refresh_token" example:""`
RefreshToken string `json:"refresh_token" example:"<opaque-refresh-token>"`
MemberID int64 `json:"member_id" example:"1"`
TeamRole string `json:"team_role" example:"admin"`
}
type teamMemberRefreshReq struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
// changePasswordReq represents the request body for changing password
type changePasswordReq struct {
CurrentPassword string `json:"current_password" validate:"required" example:"oldpassword123"`
@ -139,6 +143,20 @@ func (h *Handler) TeamMemberLogin(c *fiber.Ctx) error {
})
}
refreshTokenStr, err := h.teamSvc.IssueRefreshTokenOnLogin(c.Context(), loginRes.ID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to issue team refresh token",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Int64("member_id", loginRes.ID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to login",
Error: "Failed to issue refresh token",
})
}
h.mongoLoggerSvc.Info("Team member login successful",
zap.Int("status_code", fiber.StatusOK),
zap.Int64("member_id", loginRes.ID),
@ -150,7 +168,7 @@ func (h *Handler) TeamMemberLogin(c *fiber.Ctx) error {
Message: "Login successful",
Data: teamMemberLoginRes{
AccessToken: accessToken,
RefreshToken: "",
RefreshToken: refreshTokenStr,
MemberID: loginRes.ID,
TeamRole: string(loginRes.TeamRole),
},
@ -159,6 +177,81 @@ func (h *Handler) TeamMemberLogin(c *fiber.Ctx) error {
})
}
// TeamMemberRefresh godoc
// @Summary Refresh team member tokens
// @Description Exchanges a valid team refresh token for a new access JWT and a rotated refresh token (use only tokens from POST /team/login or this endpoint).
// @Tags team
// @Accept json
// @Produce json
// @Param body body teamMemberRefreshReq true "Current refresh token"
// @Success 200 {object} domain.Response{data=teamMemberLoginRes}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 401 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/team/refresh [post]
func (h *Handler) TeamMemberRefresh(c *fiber.Ctx) error {
var req teamMemberRefreshReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Validation failed",
Error: errMsg,
})
}
member, newRefresh, err := h.teamSvc.RefreshSession(c.Context(), req.RefreshToken)
if err != nil {
switch {
case errors.Is(err, domain.ErrTeamRefreshTokenNotFound), errors.Is(err, domain.ErrTeamRefreshTokenExpired):
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Message: "Refresh failed",
Error: "Invalid or expired refresh token",
})
case errors.Is(err, domain.ErrInvalidTeamMemberStatus):
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Refresh failed",
Error: "Account is not active",
})
default:
h.mongoLoggerSvc.Error("Team refresh failed", zap.Error(err), zap.Time("timestamp", time.Now()))
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Refresh failed",
Error: err.Error(),
})
}
}
role := mapTeamRoleToRole(member.TeamRole)
accessToken, err := jwtutil.CreateJwt(member.ID, role, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Refresh failed",
Error: "Failed to generate access token",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Token refreshed successfully",
Data: teamMemberLoginRes{
AccessToken: accessToken,
RefreshToken: newRefresh,
MemberID: member.ID,
TeamRole: string(member.TeamRole),
},
Success: true,
StatusCode: fiber.StatusOK,
})
}
// CreateTeamMember godoc
// @Summary Create a new team member
// @Description Create a new internal team member (admin only)

View File

@ -330,6 +330,7 @@ func (a *App) initAppRoutes() {
// Team Management
teamGroup := groupV1.Group("/team")
teamGroup.Post("/login", h.TeamMemberLogin)
teamGroup.Post("/refresh", h.TeamMemberRefresh)
teamGroup.Get("/me", a.authMiddleware, a.RequirePermission("team.profile.get_mine"), h.GetMyTeamProfile)
teamGroup.Get("/stats", a.authMiddleware, a.RequirePermission("team.stats"), h.GetTeamMemberStats)
teamGroup.Get("/members", a.authMiddleware, a.RequirePermission("team.members.list"), h.GetAllTeamMembers)