-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
27 changed files
with
1,735 additions
and
70 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.