Skip to content

Commit

Permalink
Adds Basic Infrastructure (#12)
Browse files Browse the repository at this point in the history
* Adds Basic Infrastructure

* Adds missing Files

* Adds Workflow and Settings

* Adds health endpoint

* Adapts health endpoint

* Adapts docker compose

* Fixes typo

* Updates Go Docs

---------

Co-authored-by: Aaron Zielstorff <[email protected]>
  • Loading branch information
jannikf02 and aaronzi authored Aug 12, 2024
1 parent e07ed40 commit cc2f1d2
Show file tree
Hide file tree
Showing 27 changed files with 1,735 additions and 70 deletions.
52 changes: 52 additions & 0 deletions .github/workflows/go-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Go Application Tests

on:
pull_request:
paths:
- 'backend/aashub/**'
push:
branches: [main]
paths:
- 'backend/aashub/**'

jobs:
unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Get dependencies
working-directory: backend/aashub
run: go mod download
- name: Run unit tests
run: go test ./... -v -tags=unit
working-directory: backend/aashub

integration-tests:
name: Run Integration Tests
needs: unit-tests
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
- name: Start all services with Docker Compose
run: docker-compose -f ci/docker-compose.yml up -d
- name: Wait for services to be ready
run: |
until [ "$(docker inspect --format='{{.State.Health.Status}}' app)" == "healthy" ]; do
echo "Current health status: $(docker inspect --format='{{.State.Health.Status}}' app)"
sleep 5
done
- name: Run integration tests
run: docker exec app /bin/sh -c "cd /workspace/backend/aashub && go test ./... -tags=integration"
- name: Shutdown services
run: docker-compose -f ci/docker-compose.yml down
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/backend/aashub/main.go",
"program": "${workspaceFolder}/backend/aashub/cmd/aashub/main.go",
"env": {},
"args": []
}
Expand Down
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"go.buildFlags": ["-tags=unit,integration"],
"go.testFlags": ["-tags=unit,integration"],
"[json]": {
"editor.insertSpaces": true,
"editor.formatOnSave": true,
Expand Down
15 changes: 15 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@
},
"problemMatcher": [],
"detail": "Fix code formatting using Prettier"
},
{
"label": "Build Go Docs",
"type": "shell",
"command": "swag",
"args": ["init", "-g", "cmd/aashub/main.go", "--parseDependency", "--parseInternal", "-o", "docs"],
"options": {
"cwd": "${workspaceFolder}/backend/aashub"
},
"group": {
"kind": "build",
"isDefault": false
},
"problemMatcher": [],
"detail": "Build Go documentation using Swag"
}
]
}
157 changes: 157 additions & 0 deletions backend/aashub/api/handler/users.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package api

import (
b64 "encoding/base64"
"encoding/json"
"net/http"
"time"

repositories "github.com/aas-hub-org/aashub/internal/database/repositories"
interfaces "github.com/aas-hub-org/aashub/internal/interfaces"
)

type APIUser struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
}

type UserHandler struct {
Repo interfaces.UserRepositoryInterface
}

type VerificationHandler struct {
VerificationRepository interfaces.VerificationRepositoryInterface
}

// RegisterUser registers a new user in the system.
// @Summary Register a new user
// @Description Registers a new user with the provided username, email, and password.
// @Tags users
// @Accept json
// @Produce json
// @Param user body APIUser true "User to register"
// @Success 201 {string} string "Successfully registered the user"
// @Failure 400 {string} string "Invalid request parameters"
// @Failure 500 {string} string "Internal server error"
// @Router /users/register [post]
func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
var user APIUser
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// Check if any of the required fields are empty
if user.Username == "" || user.Email == "" || user.Password == "" {
http.Error(w, "Missing required field(s)", http.StatusBadRequest)
return
}

err = h.Repo.RegisterUser(user.Username, user.Email, user.Password)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusCreated)
}

// VerifyUser godoc
// @Summary Verify user
// @Description Verifies a user using base64 URL encoded email and verification code.
// @Tags verification
// @Accept json
// @Produce json
// @Param email query string true "Base64 URL Encoded Email"
// @Param code query string true "Base64 URL Encoded Verification Code"
// @Success 200 {string} string "User verified successfully"
// @Failure 400 {string} string "Invalid email or code"
// @Failure 500 {string} string "Verification failed"
// @Router /verify [get]
func (h *VerificationHandler) VerifyUser(w http.ResponseWriter, r *http.Request) {
// Extract query parameters
email_byte, mail_decode_err := b64.RawURLEncoding.DecodeString(r.URL.Query().Get("email"))
code_byte, code_decode_err := b64.RawURLEncoding.DecodeString(r.URL.Query().Get("code"))

if mail_decode_err != nil || code_decode_err != nil {
http.Error(w, "Invalid email or code", http.StatusBadRequest)
return
}

email := string(email_byte)
code := string(code_byte)

error_type, err := h.VerificationRepository.Verify(email, code)
if err != nil {
if error_type == "system" {
http.Error(w, "Verification failed", http.StatusInternalServerError)
return
} else {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}

// Write success response
w.WriteHeader(http.StatusOK)
w.Write([]byte("User verified successfully"))
}

// LoginUser logs in a user, sets a cookie with a JWT token, and returns the token in the response
// @Summary User login and set cookie
// @Description Logs in a user by identifier (username or email) and password, sets a cookie with a JWT token if successful, and returns the JWT token in the response.
// @Tags users
// @Accept multipart/form-data
// @Produce json
// @Param identifier formData string true "Username or Email"
// @Param password formData string true "Password"
// @Success 204 "Successfully logged in"
// @Failure 400 {object} map[string]string "Missing required field(s) or bad request"
// @Failure 404 {object} map[string]string "User not found"
// @Failure 500 {object} map[string]string "Internal Server Error"
// @Router /users/login [post]
func (h *UserHandler) LoginUser(w http.ResponseWriter, r *http.Request) {
// Parse form data
err := r.ParseMultipartForm(32 << 20)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

identifier := r.FormValue("identifier")
password := r.FormValue("password")

// Check if any of the required fields are empty
if identifier == "" || password == "" {
http.Error(w, "Missing required field(s)", http.StatusBadRequest)
return
}

token, err := h.Repo.LoginUser(identifier, password)
if err != nil {
if err == repositories.ErrUserRepoNotFound {
http.Error(w, "User not found", http.StatusNotFound)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Create a cookie
expiration := time.Now().Add(24 * time.Hour) // Set expiration to 24 hours from now
cookie := http.Cookie{
Name: "token", // Name of the cookie
Value: token, // Token value
Expires: expiration, // Expiration time
HttpOnly: true, // Make the cookie HTTP-only (not accessible via JavaScript)
Path: "/", // Cookie path
// Secure: true, // Uncomment this if you are serving your site over HTTPS
}

// Set the cookie in the response header
http.SetCookie(w, &cookie)

w.WriteHeader(http.StatusNoContent)
}
77 changes: 77 additions & 0 deletions backend/aashub/cmd/aashub/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import (
"log"
"net/http"
"time"

api "github.com/aas-hub-org/aashub/api/handler"
"github.com/aas-hub-org/aashub/internal/database"
repositories "github.com/aas-hub-org/aashub/internal/database/repositories"

docs "github.com/aas-hub-org/aashub/docs"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)

// @BasePath /api/v1

// Health godoc
// @Summary Health Check
// @Description Responds with OK if the service is up and running
// @Tags health
// @Produce plain
// @Success 200 {string} string "OK"
// @Router /health [get]
func Health(g *gin.Context) {
g.JSON(http.StatusOK, "healthy")
}

func main() {
r := gin.Default()

// Configure CORS
corsConfig := cors.Config{
AllowAllOrigins: true,
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}
r.Use(cors.New(corsConfig))

// Initialize database
database, err := database.NewDB()
if err != nil {
log.Fatalf("Could not connect to the database: %v", err)
}

// Initialize repositories
verificationRepo := &repositories.VerificationRepository{DB: database}
mailVerificationRepo := &repositories.EmailVerificationRepository{VerificationRepository: verificationRepo}
userRepo := &repositories.UserRepository{DB: database, VerificationRepository: mailVerificationRepo}

// Initialize handlers
userHandler := &api.UserHandler{Repo: userRepo}
verificationHandler := &api.VerificationHandler{VerificationRepository: verificationRepo}

docs.SwaggerInfo.BasePath = "/api/v1"
v1 := r.Group("/api/v1")
{
ug := v1.Group("/users")
{
ug.POST("/register", gin.WrapF(userHandler.RegisterUser))
ug.POST("/login", gin.WrapF(userHandler.LoginUser))
}
vg := v1.Group("/verify")
{
vg.GET("/", gin.WrapF(verificationHandler.VerifyUser))
}
}
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
r.GET("/health", Health)
r.Run(":9000")
}
Loading

0 comments on commit cc2f1d2

Please sign in to comment.