Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ DB_NAME=maxit
# Application Configuration
APP_PORT=8000

# File Storage Configuration (using localhost)
# File Storage Configuration (internal server, NOT the public signed-URL port)
FILE_STORAGE_HOST=localhost
FILE_STORAGE_PORT=8888
FILE_STORAGE_PORT=8081

# Queue Configuration (RabbitMQ - using localhost)
QUEUE_NAME=worker_queue
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
${{ runner.os }}-go-tools-

- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2

- name: Install swag CLI tool
run: go install github.com/mini-maxit/swag/cmd/swag@latest
2 changes: 1 addition & 1 deletion .github/workflows/pre-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
go-version: stable

- name: Install dependency tools
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest | go install golang.org/x/tools/cmd/goimports@latest | go install github.com/swaggo/swag/cmd/swag@latest
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 | go install golang.org/x/tools/cmd/goimports@latest | go install github.com/swaggo/swag/cmd/swag@latest

- name: Set up pre-commit Cache
uses: pre-commit/action@v3.0.1
15 changes: 9 additions & 6 deletions internal/api/http/httputils/error_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func TestHttpToErrorCode(t *testing.T) {
}{
{http.StatusNotFound, "ERR_NOT_FOUND"},
{http.StatusInternalServerError, "ERR_INTERNAL_SERVER_ERROR"},
{http.StatusBadRequest, "ERR_BAD_REQUEST"},
{http.StatusBadRequest, errBadRequest},
{http.StatusNonAuthoritativeInfo, "ERR_NON_AUTHORITATIVE_INFORMATION"},
}
for _, tc := range tests {
Expand All @@ -213,7 +213,10 @@ func TestHttpToErrorCode(t *testing.T) {
}
}

const applicationJSON = "application/json"
const (
applicationJSON = "application/json"
errBadRequest = "ERR_BAD_REQUEST"
)

func TestReturnError(t *testing.T) {
w := httptest.NewRecorder()
Expand All @@ -233,8 +236,8 @@ func TestReturnError(t *testing.T) {
if resp.Ok {
t.Fatalf("expected ok=false")
}
if resp.Data.Code != "ERR_BAD_REQUEST" {
t.Fatalf("expected code ERR_BAD_REQUEST, got %s", resp.Data.Code)
if resp.Data.Code != errBadRequest {
t.Fatalf("expected code %s, got %s", errBadRequest, resp.Data.Code)
}
if resp.Data.Message != "bad req" {
t.Fatalf("expected message 'bad req', got %s", resp.Data.Message)
Expand Down Expand Up @@ -348,8 +351,8 @@ func TestHandleValidationError(t *testing.T) {
if resp.Ok {
t.Fatalf("expected ok=false")
}
if resp.Data.Code != "ERR_BAD_REQUEST" {
t.Fatalf("expected code ERR_BAD_REQUEST, got %s", resp.Data.Code)
if resp.Data.Code != errBadRequest {
t.Fatalf("expected code %s, got %s", errBadRequest, resp.Data.Code)
}
if resp.Data.Message != InvalidRequestBodyMessage {
t.Fatalf("expected message %q, got %q", InvalidRequestBodyMessage, resp.Data.Message)
Expand Down
50 changes: 29 additions & 21 deletions internal/api/http/middleware/cors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import (
"github.com/mini-maxit/backend/internal/config"
)

const (
corsAllowOriginHeader = "Access-Control-Allow-Origin"
corsAllowCredentialsHeader = "Access-Control-Allow-Credentials"
corsLocalOrigin = "http://localhost:3000"
corsAllowedOrigins = "http://localhost:3000,http://localhost:5173"
corsCredentialsTrue = "true"
)

func TestCORSMiddleware(t *testing.T) {
tests := []struct {
name string
Expand All @@ -28,51 +36,51 @@ func TestCORSMiddleware(t *testing.T) {
},
expectedStatus: http.StatusNoContent,
expectedHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH",
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
"Access-Control-Allow-Credentials": "false",
"Access-Control-Max-Age": "86400",
corsAllowOriginHeader: "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH",
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
corsAllowCredentialsHeader: "false",
"Access-Control-Max-Age": "86400",
},
},
{
name: "GET request with specific allowed origin",
method: http.MethodGet,
origin: "http://localhost:3000",
origin: corsLocalOrigin,
corsConfig: &config.CORSConfig{
AllowedOrigins: "http://localhost:3000,http://localhost:5173",
AllowedOrigins: corsAllowedOrigins,
AllowCredentials: true,
},
expectedStatus: http.StatusOK,
expectedHeaders: map[string]string{
"Access-Control-Allow-Origin": "http://localhost:3000",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH",
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Expose-Headers": "Content-Length, Content-Type",
corsAllowOriginHeader: corsLocalOrigin,
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH",
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
corsAllowCredentialsHeader: corsCredentialsTrue,
"Access-Control-Expose-Headers": "Content-Length, Content-Type",
},
},
{
name: "POST request with credentials and matching origin",
method: http.MethodPost,
origin: "http://localhost:5173",
corsConfig: &config.CORSConfig{
AllowedOrigins: "http://localhost:3000,http://localhost:5173",
AllowedOrigins: corsAllowedOrigins,
AllowCredentials: true,
},
expectedStatus: http.StatusOK,
expectedHeaders: map[string]string{
"Access-Control-Allow-Origin": "http://localhost:5173",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Expose-Headers": "Content-Length, Content-Type",
corsAllowOriginHeader: "http://localhost:5173",
corsAllowCredentialsHeader: corsCredentialsTrue,
"Access-Control-Expose-Headers": "Content-Length, Content-Type",
},
},
{
name: "Request from non-allowed origin",
method: http.MethodGet,
origin: "http://evil.com",
corsConfig: &config.CORSConfig{
AllowedOrigins: "http://localhost:3000,http://localhost:5173",
AllowedOrigins: corsAllowedOrigins,
AllowCredentials: true,
},
expectedStatus: http.StatusForbidden,
Expand All @@ -83,7 +91,7 @@ func TestCORSMiddleware(t *testing.T) {
method: http.MethodGet,
origin: "",
corsConfig: &config.CORSConfig{
AllowedOrigins: "http://localhost:3000",
AllowedOrigins: corsLocalOrigin,
AllowCredentials: true,
},
expectedStatus: http.StatusOK,
Expand All @@ -92,15 +100,15 @@ func TestCORSMiddleware(t *testing.T) {
{
name: "Origin with whitespace in allowed list",
method: http.MethodGet,
origin: "http://localhost:3000",
origin: corsLocalOrigin,
corsConfig: &config.CORSConfig{
AllowedOrigins: "http://localhost:3000 , http://localhost:5173",
AllowCredentials: true,
},
expectedStatus: http.StatusOK,
expectedHeaders: map[string]string{
"Access-Control-Allow-Origin": "http://localhost:3000",
"Access-Control-Allow-Credentials": "true",
corsAllowOriginHeader: corsLocalOrigin,
corsAllowCredentialsHeader: corsCredentialsTrue,
},
},
}
Expand Down
8 changes: 4 additions & 4 deletions internal/api/http/routes/access_control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func setupAccessControlTest(t *testing.T) (*gomock.Controller, *mock_service.Moc
currentUser := schemas.User{
ID: 1,
Name: "Test",
Surname: "User",
Email: "test@email.com",
Surname: testSurname,
Email: testUserEmail,
Role: types.UserRoleAdmin,
}
return ctrl, acs, route, db, currentUser
Expand Down Expand Up @@ -209,7 +209,7 @@ func TestGetContestCollaborators(t *testing.T) {
w := httptest.NewRecorder()

expectedCollaborators := []schemas.Collaborator{
{UserID: 1, UserName: "User1", UserEmail: "user1@email.com", Permission: types.PermissionOwner, AddedAt: "2024-01-01T00:00:00Z"},
{UserID: 1, UserName: testUserName, UserEmail: testUser1Email, Permission: types.PermissionOwner, AddedAt: "2024-01-01T00:00:00Z"},
{UserID: 2, UserName: "User2", UserEmail: "user2@email.com", Permission: types.PermissionEdit, AddedAt: "2024-01-02T00:00:00Z"},
}

Expand Down Expand Up @@ -491,7 +491,7 @@ func TestGetTaskCollaborators(t *testing.T) {
w := httptest.NewRecorder()

expectedCollaborators := []schemas.Collaborator{
{UserID: 1, UserName: "User1", UserEmail: "user1@email.com", Permission: types.PermissionOwner, AddedAt: "2024-01-01T00:00:00Z"},
{UserID: 1, UserName: testUserName, UserEmail: testUser1Email, Permission: types.PermissionOwner, AddedAt: "2024-01-01T00:00:00Z"},
}

acs.EXPECT().GetCollaborators(gomock.Any(), gomock.Any(), gomock.Any(), int64(1)).Return(expectedCollaborators, nil).Times(1)
Expand Down
24 changes: 17 additions & 7 deletions internal/api/http/routes/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@ func newAuthResponse(tokens *schemas.JWTTokens) AuthResponse {
}

// setRefreshTokenCookie sets the refresh token as an httpOnly cookie
func setRefreshTokenCookie(w http.ResponseWriter, path, refreshToken string) {
func setRefreshTokenCookie(w http.ResponseWriter, path, refreshToken string, secure *bool) {
secureFlag := false
if secure != nil {
secureFlag = *secure
}
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: refreshToken,
Path: path,
HttpOnly: true,
Secure: false, // Set to true in production with HTTPS
Secure: secureFlag,
SameSite: http.SameSiteStrictMode,
MaxAge: 7 * 24 * 60 * 60, // 7 days
})
Expand All @@ -49,6 +53,7 @@ type AuthRoute interface {

type AuthRouteImpl struct {
refreshTokenPath string
cookieSecure *bool
userService service.UserService
authService service.AuthService
logger *zap.SugaredLogger
Expand Down Expand Up @@ -88,7 +93,7 @@ func (ar *AuthRouteImpl) Login(w http.ResponseWriter, r *http.Request) {
return
}

setRefreshTokenCookie(w, ar.refreshTokenPath, tokens.RefreshToken)
setRefreshTokenCookie(w, ar.refreshTokenPath, tokens.RefreshToken, ar.cookieSecure)

authResponse := newAuthResponse(tokens)

Expand Down Expand Up @@ -130,7 +135,7 @@ func (ar *AuthRouteImpl) Register(w http.ResponseWriter, r *http.Request) {
return
}

setRefreshTokenCookie(w, ar.refreshTokenPath, tokens.RefreshToken)
setRefreshTokenCookie(w, ar.refreshTokenPath, tokens.RefreshToken, ar.cookieSecure)

authResponse := newAuthResponse(tokens)

Expand Down Expand Up @@ -173,7 +178,7 @@ func (ar *AuthRouteImpl) RefreshToken(w http.ResponseWriter, r *http.Request) {
return
}

setRefreshTokenCookie(w, ar.refreshTokenPath, tokens.RefreshToken)
setRefreshTokenCookie(w, ar.refreshTokenPath, tokens.RefreshToken, ar.cookieSecure)

authResponse := newAuthResponse(tokens)

Expand All @@ -196,21 +201,26 @@ func (ar *AuthRouteImpl) Logout(w http.ResponseWriter, r *http.Request) {
}

// Clear the refresh token cookie
secureFlag := false
if ar.cookieSecure != nil {
secureFlag = *ar.cookieSecure
}
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Path: ar.refreshTokenPath,
Value: "",
MaxAge: -1,
HttpOnly: true,
Secure: false, // Set to true in production with HTTPS
Secure: secureFlag,
})

httputils.ReturnSuccess(w, http.StatusOK, httputils.NewMessageResponse("Logged out successfully"))
}

func NewAuthRoute(userService service.UserService, authService service.AuthService, refreshTokenPath string) AuthRoute {
func NewAuthRoute(userService service.UserService, authService service.AuthService, refreshTokenPath string, cookieSecure bool) AuthRoute {
route := &AuthRouteImpl{
refreshTokenPath: refreshTokenPath,
cookieSecure: &cookieSecure,
userService: userService,
authService: authService,
logger: utils.NewNamedLogger("auth"),
Expand Down
Loading
Loading