2.0 - WebUI
This commit is contained in:
parent
23b80bc084
commit
dab97b2de6
33 changed files with 1009 additions and 191 deletions
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
@ -0,0 +1,9 @@
|
|||
.env
|
||||
.gitignore
|
||||
.idea
|
||||
.dockerignore
|
||||
*.db
|
||||
|
||||
build/
|
||||
tmp/
|
||||
templates/*.go
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,8 @@
|
|||
*.db
|
||||
.env
|
||||
.idea
|
||||
|
||||
tmp/
|
||||
build/
|
||||
|
||||
templates/*.go
|
20
Dockerfile
Normal file
20
Dockerfile
Normal file
|
@ -0,0 +1,20 @@
|
|||
FROM docker.io/golang:alpine AS build
|
||||
LABEL authors="estym"
|
||||
|
||||
RUN apk add --no-cache gcc musl-dev ca-certificates git
|
||||
RUN go install github.com/a-h/templ/cmd/templ@v0.3.833
|
||||
|
||||
|
||||
RUN mkdir /app
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN templ generate
|
||||
RUN go install -ldflags='-s -w -extldflags "-static"' .
|
||||
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /go/bin/sonarqube-badge /usr/local/bin/sonarqube-badge
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/sonarqube-badge"]
|
5
Makefile
5
Makefile
|
@ -1,5 +0,0 @@
|
|||
all:
|
||||
templ generate
|
||||
mkdir -p build
|
||||
cd build
|
||||
go build -o build/sonarqube-badges .
|
46
README.md
Normal file
46
README.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
# SonarQube Badges
|
||||

|
||||
|
||||
## Why?
|
||||
SonarQube has a feature known as badges,
|
||||
you must have seen those kind of badge on different kind of readme
|
||||
for things such as CI status, Current version or event dependencies.
|
||||
|
||||
SonarQube has those but for the compliance of the codebase to some standards.
|
||||
|
||||
However, the links that SonarQube creates contains sensitive information such as a token that can be used for various things.
|
||||
|
||||
That is why, I decided to make an app that hides the tokens in order to be able to publicly share those badges.
|
||||
|
||||
## Build
|
||||
|
||||
In order to build this app, you will need to install [Templ](https://github.com/a-h/templ) to compile the templates
|
||||
|
||||
```shell
|
||||
templ generate
|
||||
go build .
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
This app has 3 environment variables which are the following:
|
||||
- `DATA_PATH` : Location of the data folder of the application that'll contain the sqlite database.
|
||||
- `SECRET` : A secret that'll be used to encrypt the SonarQube token in the database
|
||||
- `SQ_HOST` : Your SonarQube instance URL
|
||||
- `PORT` : The port of the web application `defaults to 8080`
|
||||
|
||||
## First run
|
||||
|
||||
On your first run, you'll get default credentials displayed as such:
|
||||
|
||||
```
|
||||
Default User:
|
||||
- admin@example.com
|
||||
- adec2454d43489be2cdb758f645a4973ec785159546db83563dd16e6d650faa261c8b429dbdf8074f95cf4cdc118b8a21753d06412d2
|
||||
```
|
||||
|
||||
You will be able to change those credential by accessing the `/user` path in the app and updating your profile
|
||||
|
||||
## Docker
|
||||
|
||||
[A docker image is available on my own Registry](https://registry.regnault.dev/#!/taglist/sonarqube-badge)
|
|
@ -6,10 +6,10 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
SqHost string `envconfig:"SQ_HOST"`
|
||||
AppPassword string `envconfig:"APP_PASSWORD"`
|
||||
Secret string `envconfig:"SECRET"`
|
||||
DataPath string `envconfig:"DATA_PATH"`
|
||||
SqHost string `envconfig:"SQ_HOST"`
|
||||
Secret string `envconfig:"SECRET"`
|
||||
DataPath string `envconfig:"DATA_PATH"`
|
||||
Port int `envconfig:"PORT"`
|
||||
}
|
||||
|
||||
func ProcessConfiguration(ctx context.Context) context.Context {
|
||||
|
|
2
go.mod
2
go.mod
|
@ -14,10 +14,10 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
)
|
||||
|
|
4
go.sum
4
go.sum
|
@ -8,6 +8,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
|
|||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
|
@ -16,6 +18,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
|||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
|
|
25
main.go
25
main.go
|
@ -2,15 +2,40 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/joho/godotenv"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sonarqube-badge/config"
|
||||
"sonarqube-badge/router"
|
||||
"sonarqube-badge/store"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load(".env")
|
||||
|
||||
if os.Getenv("DATA_PATH") == "" || os.Getenv("SECRET") == "" || os.Getenv("SQ_HOST") == "" {
|
||||
println("Environment variables missing")
|
||||
println("Please ensure that the following environment variables are set: ")
|
||||
println("- DATA_PATH")
|
||||
println("- SECRET")
|
||||
println("- SQ_HOST")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = config.ProcessConfiguration(ctx)
|
||||
ctx = store.CreateDatabase(ctx)
|
||||
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGQUIT)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGKILL)
|
||||
go func() {
|
||||
<-c
|
||||
println("Stopping server")
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
router.StartServer(ctx)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package api
|
|||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"sonarqube-badge/security"
|
||||
"sonarqube-badge/router/utils"
|
||||
)
|
||||
|
||||
func postLogin(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -14,20 +14,16 @@ func postLogin(res http.ResponseWriter, req *http.Request) {
|
|||
email := req.Form.Get("email")
|
||||
password := req.Form.Get("password")
|
||||
|
||||
if email == "1@example.com" && password == "hello" {
|
||||
token, err := security.CreateToken(email)
|
||||
ctx := req.Context()
|
||||
|
||||
if utils.UserExists(ctx, email, password) {
|
||||
user := utils.GetUser(ctx, email)
|
||||
cookie, err := utils.CreateJWTCookie(user, req)
|
||||
if err != nil {
|
||||
http.Error(res, err.Error(), http.StatusInternalServerError)
|
||||
res.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cookie := http.Cookie{
|
||||
Name: "jwt-token",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(res, &cookie)
|
||||
http.SetCookie(res, cookie)
|
||||
} else {
|
||||
res.WriteHeader(http.StatusUnauthorized)
|
||||
res.Write([]byte("Wrong email or password"))
|
||||
|
|
90
router/api/project.go
Normal file
90
router/api/project.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"sonarqube-badge/router/middlewares"
|
||||
"sonarqube-badge/router/utils"
|
||||
"sonarqube-badge/security/aes"
|
||||
"sonarqube-badge/store"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ProjectPayload struct {
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func setProject(w http.ResponseWriter, r *http.Request) {
|
||||
db, cfg, _, isError := utils.VerifyUser(w, r)
|
||||
if isError {
|
||||
return
|
||||
}
|
||||
|
||||
idString := mux.Vars(r)["id"]
|
||||
id, err := strconv.Atoi(idString)
|
||||
payload := ProjectPayload{}
|
||||
err = json.NewDecoder(r.Body).Decode(&payload)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
project := store.Project{}
|
||||
db.Where("id = ?", id).First(&project)
|
||||
if project.ID == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
project.ProjectName = payload.Name
|
||||
if project.Token != payload.Token {
|
||||
project.Token = aes.EncryptAES(cfg.Secret, payload.Token)
|
||||
}
|
||||
db.Save(&project)
|
||||
}
|
||||
|
||||
func createProject(w http.ResponseWriter, r *http.Request) {
|
||||
db, _, user, isError := utils.VerifyUser(w, r)
|
||||
if isError {
|
||||
return
|
||||
}
|
||||
|
||||
project := user.CreateProject(db)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(project)
|
||||
}
|
||||
|
||||
func deleteProject(w http.ResponseWriter, r *http.Request) {
|
||||
db, _, user, isError := utils.VerifyUser(w, r)
|
||||
if isError {
|
||||
return
|
||||
}
|
||||
|
||||
projectIdString := mux.Vars(r)["id"]
|
||||
projectId, err := strconv.Atoi(projectIdString)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
project := store.Project{}
|
||||
db.Where("id = ? AND owner_id = ?", projectId, user.ID).First(&project)
|
||||
if project.ID == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
db.Delete(&project)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func ProjectRouter(r *mux.Router) {
|
||||
sr := r.PathPrefix("").Subrouter()
|
||||
sr.Use(middlewares.CheckJwtToken)
|
||||
sr.HandleFunc("", createProject).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/{id}", setProject).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/{id}", deleteProject).Methods(http.MethodDelete)
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sonarqube-badge/config"
|
||||
"sonarqube-badge/router/middlewares"
|
||||
"sonarqube-badge/security"
|
||||
"sonarqube-badge/store"
|
||||
)
|
||||
|
||||
type setProjectBadgePayload struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func getProjectBadge(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
projectName := vars["name"]
|
||||
metric := vars["metric"]
|
||||
|
||||
print(projectName)
|
||||
|
||||
project := store.SQProject{}
|
||||
ctx := r.Context()
|
||||
cfg := ctx.Value("config").(config.Config)
|
||||
ctx.Value("db").(*gorm.DB).First(&project, "project_name = ?", projectName)
|
||||
if project.ID == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("project not found"))
|
||||
return
|
||||
}
|
||||
|
||||
token := security.DecryptAES(cfg.Secret, project.Token)
|
||||
fmt.Printf("%s", token)
|
||||
|
||||
url := fmt.Sprintf("%s/api/project_badges/measure?metric=%s&project=%s&token=%s", cfg.SqHost, metric, projectName, token)
|
||||
request, err := http.Get(url)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
os.Stderr.Write([]byte(err.Error()))
|
||||
w.Write([]byte("internal server error"))
|
||||
return
|
||||
}
|
||||
bytes, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
os.Stderr.Write([]byte(err.Error()))
|
||||
w.Write([]byte("internal server error"))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
w.WriteHeader(request.StatusCode)
|
||||
w.Write(bytes)
|
||||
}
|
||||
|
||||
func setProjectBadge(w http.ResponseWriter, r *http.Request) {
|
||||
var payload setProjectBadgePayload
|
||||
vars := mux.Vars(r)
|
||||
projectName := vars["name"]
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&payload)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
os.Stderr.Write([]byte(err.Error()))
|
||||
w.Write([]byte("invalid payload"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
|
||||
if !middlewares.CheckAuth(ctx, r) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
project := store.SQProject{}
|
||||
|
||||
db.First(&project, "project_name = ?", projectName)
|
||||
|
||||
project.ProjectName = projectName
|
||||
project.Token = security.EncryptAES(ctx.Value("config").(config.Config).Secret, payload.Token)
|
||||
|
||||
db.Save(&project)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Successfully added"))
|
||||
}
|
||||
|
||||
func ProjectBadgeRouter(r *mux.Router) {
|
||||
r.HandleFunc("/{name}/{metric}", getProjectBadge).Methods("GET")
|
||||
r.HandleFunc("/{name}", setProjectBadge).Methods("POST")
|
||||
}
|
81
router/api/user.go
Normal file
81
router/api/user.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"sonarqube-badge/router/middlewares"
|
||||
"sonarqube-badge/router/utils"
|
||||
"sonarqube-badge/store"
|
||||
)
|
||||
|
||||
func changePassword(w http.ResponseWriter, r *http.Request) {
|
||||
db, _, user, done := utils.VerifyUser(w, r)
|
||||
if done {
|
||||
return
|
||||
}
|
||||
|
||||
password := r.FormValue("password")
|
||||
verifyPassword := r.FormValue("verify_password")
|
||||
|
||||
if password != verifyPassword {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("passwords do not match"))
|
||||
return
|
||||
}
|
||||
|
||||
user.ChangePassword(db, password)
|
||||
|
||||
refreshCookie(db, user.ID, r, w)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func changeEmail(w http.ResponseWriter, r *http.Request) {
|
||||
db, _, user, done := utils.VerifyUser(w, r)
|
||||
if done {
|
||||
return
|
||||
}
|
||||
|
||||
newEmail := r.FormValue("email")
|
||||
if user.ChangeEmail(db, newEmail) != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user.Email = newEmail
|
||||
|
||||
refreshCookie(db, user.ID, r, w)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func changeUsername(w http.ResponseWriter, r *http.Request) {
|
||||
db, _, user, done := utils.VerifyUser(w, r)
|
||||
if done {
|
||||
return
|
||||
}
|
||||
|
||||
newUsername := r.FormValue("username")
|
||||
user.ChangeUsername(db, newUsername)
|
||||
|
||||
refreshCookie(db, user.ID, r, w)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func refreshCookie(db *gorm.DB, userId uint, r *http.Request, w http.ResponseWriter) {
|
||||
var user store.User
|
||||
db.First(&user, userId)
|
||||
cookie, err := utils.CreateJWTCookie(&user, r)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
func UserRouter(r *mux.Router) {
|
||||
sr := r.PathPrefix("/").Subrouter()
|
||||
sr.Use(middlewares.CheckJwtToken)
|
||||
sr.HandleFunc("/password", changePassword).Methods("POST")
|
||||
sr.HandleFunc("/email", changeEmail).Methods("POST")
|
||||
sr.HandleFunc("/username", changeUsername).Methods("POST")
|
||||
}
|
|
@ -1,40 +1,29 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sonarqube-badge/config"
|
||||
"sonarqube-badge/security"
|
||||
"strings"
|
||||
"sonarqube-badge/security/jwt"
|
||||
)
|
||||
|
||||
func CheckAuth(ctx context.Context, r *http.Request) bool {
|
||||
config := ctx.Value("config").(config.Config)
|
||||
if r.Header.Get("Authorization") == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
token := r.Header.Get("Authorization")
|
||||
if strings.HasPrefix(token, "Bearer ") {
|
||||
token = token[7:]
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
return token == config.AppPassword
|
||||
}
|
||||
|
||||
func CheckJwtToken(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenCookie, err := r.Cookie("jwt-token")
|
||||
if err != nil || tokenCookie == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
if r.Method == "GET" {
|
||||
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_, err = security.VerifyToken(tokenCookie.Value)
|
||||
_, err = jwt.VerifyToken(tokenCookie.Value, r.Context())
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
if r.Method == "GET" {
|
||||
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,12 @@ package router
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"net"
|
||||
"net/http"
|
||||
"sonarqube-badge/config"
|
||||
"sonarqube-badge/router/api"
|
||||
"sonarqube-badge/router/views"
|
||||
)
|
||||
|
@ -14,24 +16,38 @@ func StartServer(ctx context.Context) {
|
|||
r := mux.NewRouter()
|
||||
|
||||
// API ROUTES
|
||||
api.ProjectBadgeRouter(r.PathPrefix("/api/project/badge").Subrouter())
|
||||
api.ProjectRouter(r.PathPrefix("/api/project").Subrouter())
|
||||
api.LoginRouter(r.PathPrefix("/api/login").Subrouter())
|
||||
api.UserRouter(r.PathPrefix("/api/user").Subrouter())
|
||||
|
||||
// VIEW ROUTES
|
||||
views.IndexRouter(r)
|
||||
views.LoginRouter(r)
|
||||
views.ProjectRouter(r)
|
||||
views.BadgeRouter(r)
|
||||
views.UserRouter(r)
|
||||
|
||||
credentials := handlers.AllowCredentials()
|
||||
methods := handlers.AllowedMethods([]string{"POST", "GET", "OPTIONS"})
|
||||
methods := handlers.AllowedMethods([]string{"POST", "GET", "DELETE"})
|
||||
ttl := handlers.MaxAge(3600)
|
||||
|
||||
port := ctx.Value("config").(config.Config).Port
|
||||
if port == 0 {
|
||||
port = 8080
|
||||
}
|
||||
|
||||
fmt.Printf("Starting server on http://localhost:%d ...", port)
|
||||
|
||||
server := http.Server{
|
||||
Addr: ":8080",
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
BaseContext: func(listener net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
Handler: handlers.CORS(credentials, methods, ttl)(r),
|
||||
}
|
||||
|
||||
server.ListenAndServe()
|
||||
err := server.ListenAndServe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
37
router/utils/jwtUtils.go
Normal file
37
router/utils/jwtUtils.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"net/http"
|
||||
jwt2 "sonarqube-badge/security/jwt"
|
||||
"sonarqube-badge/store"
|
||||
)
|
||||
|
||||
func GetToken(req *http.Request) (*jwt.Token, error) {
|
||||
cookie, err := req.Cookie("jwt-token")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := jwt2.VerifyToken(cookie.Value, req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func CreateJWTCookie(user *store.User, req *http.Request) (*http.Cookie, error) {
|
||||
token, err := jwt2.CreateToken(*user, req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cookie := http.Cookie{
|
||||
Name: "jwt-token",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
}
|
||||
return &cookie, nil
|
||||
}
|
26
router/utils/userUtils.go
Normal file
26
router/utils/userUtils.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"sonarqube-badge/store"
|
||||
)
|
||||
|
||||
func UserExists(ctx context.Context, email string, password string) bool {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
|
||||
passwordHash := sha1.Sum([]byte(password))
|
||||
user := store.User{}
|
||||
db.Where("email = ? AND password = ?", email, fmt.Sprintf("%x", passwordHash)).First(&user)
|
||||
|
||||
return user.ID != 0
|
||||
}
|
||||
|
||||
func GetUser(ctx context.Context, email string) *store.User {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
user := store.User{}
|
||||
db.Where("email = ?", email).First(&user)
|
||||
return &user
|
||||
}
|
29
router/utils/verify.go
Normal file
29
router/utils/verify.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"sonarqube-badge/config"
|
||||
"sonarqube-badge/store"
|
||||
)
|
||||
|
||||
func VerifyUser(w http.ResponseWriter, r *http.Request) (*gorm.DB, config.Config, *store.User, bool) {
|
||||
ctx := r.Context()
|
||||
cfg := ctx.Value("config").(config.Config)
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
|
||||
token, err := GetToken(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return nil, cfg, nil, true
|
||||
}
|
||||
|
||||
email := token.Claims.(jwt.MapClaims)["email"].(string)
|
||||
user := GetUser(ctx, email)
|
||||
if user == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return nil, cfg, nil, true
|
||||
}
|
||||
return db, cfg, user, false
|
||||
}
|
93
router/views/badges.go
Normal file
93
router/views/badges.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sonarqube-badge/config"
|
||||
"sonarqube-badge/security/aes"
|
||||
"sonarqube-badge/store"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func postProjectBadge(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
projectIdString := vars["id"]
|
||||
projectId, err := strconv.Atoi(projectIdString)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
metric := r.FormValue("type")
|
||||
|
||||
db := r.Context().Value("db").(*gorm.DB)
|
||||
cfg := r.Context().Value("config").(config.Config)
|
||||
|
||||
var project store.Project
|
||||
db.First(&project, projectId)
|
||||
|
||||
if project.ID == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
projectToken := aes.DecryptAES(cfg.Secret, project.Token)
|
||||
requestProjectImage(w, cfg, project.ProjectName, projectToken, metric)
|
||||
}
|
||||
|
||||
func getProjectBadge(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
projectIdString := vars["id"]
|
||||
projectId, err := strconv.Atoi(projectIdString)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
metric := vars["metric"]
|
||||
|
||||
ctx := r.Context()
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
cfg := ctx.Value("config").(config.Config)
|
||||
|
||||
project := store.Project{}
|
||||
db.First(&project, projectId)
|
||||
|
||||
if project.ID == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
token := aes.DecryptAES(cfg.Secret, project.Token)
|
||||
requestProjectImage(w, cfg, project.ProjectName, token, metric)
|
||||
}
|
||||
|
||||
func requestProjectImage(w http.ResponseWriter, cfg config.Config, projectName string, projectToken string, metric string) {
|
||||
url := fmt.Sprintf("%s/api/project_badges/measure?metric=%s&project=%s&token=%s", cfg.SqHost, metric, projectName, projectToken)
|
||||
request, err := http.Get(url)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
os.Stderr.Write([]byte(err.Error()))
|
||||
w.Write([]byte("internal server error"))
|
||||
return
|
||||
}
|
||||
bytes, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
os.Stderr.Write([]byte(err.Error()))
|
||||
w.Write([]byte("internal server error"))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
w.WriteHeader(request.StatusCode)
|
||||
w.Write(bytes)
|
||||
}
|
||||
|
||||
func BadgeRouter(r *mux.Router) {
|
||||
r.HandleFunc("/project/badge/{id}/{metric}", getProjectBadge).Methods("GET")
|
||||
r.HandleFunc("/project/badge/{id}", postProjectBadge).Methods("POST")
|
||||
}
|
|
@ -4,11 +4,13 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"sonarqube-badge/router/middlewares"
|
||||
"sonarqube-badge/router/utils"
|
||||
"sonarqube-badge/templates"
|
||||
)
|
||||
|
||||
func getIndex(res http.ResponseWriter, req *http.Request) {
|
||||
templates.Layout(templates.Index(), "Index").Render(req.Context(), res)
|
||||
token, _ := utils.GetToken(req)
|
||||
templates.Layout(templates.Index(token), "Projects", req).Render(req.Context(), res)
|
||||
}
|
||||
|
||||
func IndexRouter(r *mux.Router) {
|
||||
|
|
|
@ -3,13 +3,30 @@ package views
|
|||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"sonarqube-badge/router/utils"
|
||||
"sonarqube-badge/templates"
|
||||
)
|
||||
|
||||
func getLogin(res http.ResponseWriter, req *http.Request) {
|
||||
templates.Layout(templates.Login(), "Login").Render(req.Context(), res)
|
||||
if _, err := utils.GetToken(req); err == nil {
|
||||
http.Redirect(res, req, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
templates.Layout(templates.Login(), "Login", req).Render(req.Context(), res)
|
||||
}
|
||||
|
||||
func getDisconnect(res http.ResponseWriter, req *http.Request) {
|
||||
disconnectCookie := http.Cookie{
|
||||
Name: "jwt-token",
|
||||
MaxAge: -1,
|
||||
Value: "",
|
||||
}
|
||||
http.SetCookie(res, &disconnectCookie)
|
||||
http.Redirect(res, req, "/login", 302)
|
||||
}
|
||||
|
||||
func LoginRouter(r *mux.Router) {
|
||||
r.HandleFunc("/login", getLogin).Methods("GET")
|
||||
r.HandleFunc("/disconnect", getDisconnect).Methods("GET")
|
||||
}
|
||||
|
|
71
router/views/project.go
Normal file
71
router/views/project.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/gorilla/mux"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"sonarqube-badge/router/middlewares"
|
||||
"sonarqube-badge/router/utils"
|
||||
"sonarqube-badge/templates"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func getProjects(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
|
||||
token, err := utils.GetToken(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
email := token.Claims.(jwt.MapClaims)["email"].(string)
|
||||
user := utils.GetUser(ctx, email)
|
||||
if user == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
templates.ProjectList(user.GetProjects(db)).Render(ctx, w)
|
||||
}
|
||||
|
||||
func getProject(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
idString := mux.Vars(r)["id"]
|
||||
id, err := strconv.Atoi(idString)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
token, _ := utils.GetToken(r)
|
||||
email := token.Claims.(jwt.MapClaims)["email"].(string)
|
||||
user := utils.GetUser(ctx, email)
|
||||
if user == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
project := user.GetProject(db, id)
|
||||
if project.ID == 0 {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
name := project.ProjectName
|
||||
if name == "" {
|
||||
name = "Untitled Project"
|
||||
}
|
||||
|
||||
templates.Layout(templates.Project(project), "Project - "+name, r).Render(ctx, w)
|
||||
}
|
||||
|
||||
func ProjectRouter(router *mux.Router) {
|
||||
sr := router.PathPrefix("").Subrouter()
|
||||
sr.Use(middlewares.CheckJwtToken)
|
||||
sr.HandleFunc("/project", getProjects).Methods("GET")
|
||||
sr.HandleFunc("/project/{id}", getProject).Methods("GET")
|
||||
}
|
24
router/views/user.go
Normal file
24
router/views/user.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"sonarqube-badge/router/middlewares"
|
||||
"sonarqube-badge/router/utils"
|
||||
"sonarqube-badge/templates"
|
||||
)
|
||||
|
||||
func getUser(w http.ResponseWriter, r *http.Request) {
|
||||
_, _, user, isError := utils.VerifyUser(w, r)
|
||||
if isError {
|
||||
return
|
||||
}
|
||||
|
||||
templates.Layout(templates.User(*user), "User Profile", r).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func UserRouter(r *mux.Router) {
|
||||
sr := r.PathPrefix("").Subrouter()
|
||||
sr.Use(middlewares.CheckJwtToken)
|
||||
sr.HandleFunc("/user", getUser).Methods("GET")
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package security
|
||||
package aes
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
|
@ -1,26 +1,29 @@
|
|||
package security
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"sonarqube-badge/config"
|
||||
"sonarqube-badge/store"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JwtClaims struct {
|
||||
}
|
||||
|
||||
func CreateToken(username string) (string, error) {
|
||||
func CreateToken(user store.User, ctx context.Context) (string, error) {
|
||||
t := jwt.NewWithClaims(jwt.SigningMethodHS256,
|
||||
jwt.MapClaims{
|
||||
"username": username,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||
})
|
||||
return t.SignedString([]byte("secret"))
|
||||
secret := ctx.Value("config").(config.Config).Secret
|
||||
return t.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
func VerifyToken(jwtString string) (*jwt.Token, error) {
|
||||
func VerifyToken(jwtString string, ctx context.Context) (*jwt.Token, error) {
|
||||
secret := ctx.Value("config").(config.Config).Secret
|
||||
parse, err := jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte("secret"), nil
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
45
store/db.go
45
store/db.go
|
@ -2,26 +2,16 @@ package store
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"os"
|
||||
"sonarqube-badge/config"
|
||||
"sonarqube-badge/security/aes"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SQProject struct {
|
||||
gorm.Model
|
||||
ProjectName string `gorm:"unique"`
|
||||
Token string `gorm:"unique"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Username string
|
||||
Password string
|
||||
Email string
|
||||
Salt string
|
||||
}
|
||||
|
||||
func CreateDatabase(ctx context.Context) context.Context {
|
||||
cfg := ctx.Value("config").(config.Config)
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("%s/sqbadge.db", cfg.DataPath)), &gorm.Config{})
|
||||
|
@ -29,10 +19,33 @@ func CreateDatabase(ctx context.Context) context.Context {
|
|||
panic("failed to open database")
|
||||
}
|
||||
|
||||
err = db.AutoMigrate(&SQProject{})
|
||||
err = db.AutoMigrate(&Project{})
|
||||
if err != nil {
|
||||
panic("failed to migrate database")
|
||||
panic("failed to migrate database Project")
|
||||
}
|
||||
|
||||
err = db.AutoMigrate(&User{})
|
||||
if err != nil {
|
||||
panic("failed to migrate database User")
|
||||
}
|
||||
|
||||
provideDefaultUser(cfg, db)
|
||||
|
||||
return context.WithValue(ctx, "db", db)
|
||||
}
|
||||
|
||||
func provideDefaultUser(cfg config.Config, db *gorm.DB) {
|
||||
password := aes.EncryptAES(cfg.Secret, time.Now().String())
|
||||
sha1Hash := sha1.Sum([]byte(password))
|
||||
user := User{
|
||||
Username: "admin",
|
||||
// Random Password
|
||||
Password: fmt.Sprintf("%x", sha1Hash),
|
||||
Email: "admin@example.com",
|
||||
}
|
||||
|
||||
if db.First(&user).Error != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Default User: \n\t- %s\n\t- %s\n", user.Email, password)
|
||||
db.Create(&user)
|
||||
}
|
||||
}
|
||||
|
|
21
store/project.go
Normal file
21
store/project.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package store
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Project struct {
|
||||
gorm.Model
|
||||
ProjectName string
|
||||
Token string
|
||||
OwnerId uint
|
||||
Owner User `gorm:foreignkey:"user_id"`
|
||||
}
|
||||
|
||||
func (project *Project) GetUser(db *gorm.DB) *User {
|
||||
var user User
|
||||
db.Where("id = ?", project.OwnerId).First(&user)
|
||||
return &user
|
||||
}
|
||||
|
||||
func (project *Project) Delete(db *gorm.DB) {
|
||||
db.Where("id = ?", project.ID).Delete(&Project{})
|
||||
}
|
59
store/user.go
Normal file
59
store/user.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Username string
|
||||
Password string
|
||||
Email string `gorm:"unique"`
|
||||
}
|
||||
|
||||
func (user *User) GetProjects(db *gorm.DB) []Project {
|
||||
var projects []Project
|
||||
db.Where(&Project{OwnerId: user.ID}).Find(&projects)
|
||||
return projects
|
||||
}
|
||||
|
||||
func (user *User) GetProject(db *gorm.DB, id int) Project {
|
||||
var project Project
|
||||
db.Where("id = ? AND owner_id = ?", id, user.ID).First(&project)
|
||||
return project
|
||||
}
|
||||
|
||||
func (user *User) CreateProject(db *gorm.DB) Project {
|
||||
var project Project
|
||||
project.OwnerId = user.ID
|
||||
db.Create(&project)
|
||||
return project
|
||||
}
|
||||
|
||||
func (user *User) ChangeUsername(db *gorm.DB, username string) {
|
||||
user.Username = username
|
||||
db.Save(&user)
|
||||
}
|
||||
|
||||
func (user *User) ChangeEmail(db *gorm.DB, email string) error {
|
||||
user.Email = email
|
||||
return db.Save(&user).Error
|
||||
}
|
||||
|
||||
func (user *User) ChangePassword(db *gorm.DB, password string) {
|
||||
sha1Sum := sha1.Sum([]byte(password))
|
||||
user.Password = fmt.Sprintf("%x", sha1Sum)
|
||||
db.Save(&user)
|
||||
}
|
||||
|
||||
func CreateUser(db *gorm.DB, email string, password string, username string) User {
|
||||
user := User{}
|
||||
user.Username = username
|
||||
sha1Hash := sha1.Sum([]byte(password))
|
||||
user.Password = fmt.Sprintf("%x", sha1Hash)
|
||||
user.Email = email
|
||||
db.Create(&user)
|
||||
return user
|
||||
}
|
|
@ -1,5 +1,34 @@
|
|||
package templates
|
||||
|
||||
templ Index() {
|
||||
<form class></form>
|
||||
}
|
||||
import "github.com/golang-jwt/jwt/v5"
|
||||
|
||||
templ Index(token *jwt.Token) {
|
||||
{{
|
||||
username := ""
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
username = claims["username"].(string)
|
||||
}
|
||||
}}
|
||||
<div class="container">
|
||||
<h2 class="my-4">Welcome {username}</h2>
|
||||
<div class="container">
|
||||
<h3 class="my-2">Projects</h3>
|
||||
<div hx-get="/project" hx-trigger="load"></div>
|
||||
<a hx-post="/api/project" id="projectAdder">
|
||||
<button class="btn btn-primary" type="button" >Create project</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/javascript">
|
||||
document.getElementById('projectAdder').addEventListener('htmx:beforeSwap', function (ev) {
|
||||
ev.detail.shouldSwap = false;
|
||||
});
|
||||
document.getElementById('projectAdder').addEventListener('htmx:afterRequest', function (ev) {
|
||||
if (ev.detail.successful) {
|
||||
const id = JSON.parse(ev.detail.xhr.response).ID;
|
||||
location.href = `/project/${id}`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
|
|
|
@ -1,29 +1,62 @@
|
|||
package templates
|
||||
|
||||
import "net/http"
|
||||
import "sonarqube-badge/router/utils"
|
||||
import "github.com/golang-jwt/jwt/v5"
|
||||
import "strings"
|
||||
|
||||
func isActive(title string, key string) string {
|
||||
if strings.HasPrefix(title, key) {
|
||||
return "active"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
templ head(title string) {
|
||||
<head>
|
||||
<title>{ title }</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
}
|
||||
|
||||
templ header() {
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark p-2">
|
||||
<a class="navbar-brand">Sonarqube Badges</a>
|
||||
templ header(loggedIn bool, title string) {
|
||||
<nav class="navbar navbar-expand-lg bg-primary p-2">
|
||||
<a class="navbar-brand" href="/">Sonarqube Badges</a>
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
if loggedIn {
|
||||
<li class="nav-item">
|
||||
<a class={"nav-link " + isActive(title, "Project")} href="/">Projects</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class={"nav-link " + isActive(title, "User Profile")} href="/user">Profile</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/disconnect">Disconnect</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
|
||||
templ Layout(component templ.Component, title string) {
|
||||
<html lang="en">
|
||||
templ layout(component templ.Component, title string, token *jwt.Token) {
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
@head(title)
|
||||
<body hx-ext="response-targets">
|
||||
@header()
|
||||
@header(token != nil, title)
|
||||
<div class="container-fluid m-4">
|
||||
@component
|
||||
</div>
|
||||
</body>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<script src="https://unpkg.com/htmx-ext-response-targets@2.0.3" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<script src="https://unpkg.com/htmx-ext-json-enc@2.0.1/json-enc.js"></script>
|
||||
<script src="https://unpkg.com/htmx-ext-response-targets@2.0.3" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous"></script>
|
||||
<script>htmx.config.responseTargetUnsetsError = false</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
</html>
|
||||
}
|
||||
|
||||
func Layout(component templ.Component, title string, req *http.Request) templ.Component {
|
||||
token, _ := utils.GetToken(req)
|
||||
return layout(component, title, token);
|
||||
}
|
|
@ -2,17 +2,18 @@ package templates
|
|||
|
||||
templ Login() {
|
||||
<div class="container">
|
||||
<h2>Connection</h2>
|
||||
<form hx-post="/api/login" hx-target-error="#loginError" id="loginForm">
|
||||
<div hx-trigger="changed" id="loginError"></div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email Address</label>
|
||||
<input name="email" class="form-control" type="email" id="email">
|
||||
<input name="email" class="form-control" type="email" id="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input name="password" class="form-control" type="password" id="password">
|
||||
<input name="password" class="form-control" type="password" id="password" required>
|
||||
</div>
|
||||
<button class="btn btn-primary">Connect</button>
|
||||
<button class="btn btn-primary" type="submit">Connect</button>
|
||||
</form>
|
||||
|
||||
<script type="application/javascript">
|
||||
|
|
129
templates/project.templ
Normal file
129
templates/project.templ
Normal file
|
@ -0,0 +1,129 @@
|
|||
package templates
|
||||
|
||||
import "sonarqube-badge/store"
|
||||
import "fmt"
|
||||
|
||||
templ ProjectList(projects []store.Project) {
|
||||
<ul>
|
||||
for _, item := range projects {
|
||||
<li>
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/project/%d", item.ID))}>
|
||||
if item.ProjectName != "" {
|
||||
{ item.ProjectName }
|
||||
} else {
|
||||
Untitled Project
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
templ Project(project store.Project) {
|
||||
@templ.JSONScript("projectID", project.ID)
|
||||
<div class="container">
|
||||
<div class="my-4">
|
||||
if project.ProjectName != "" {
|
||||
<h2>{project.ProjectName}</h2>
|
||||
} else {
|
||||
<h2>Untitled Project</h2>
|
||||
}
|
||||
</div>
|
||||
|
||||
@EditProject(project)
|
||||
|
||||
@BadgeMaker(project)
|
||||
|
||||
@DangerZone(project)
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
templ EditProject(project store.Project) {
|
||||
<div class="container mb-4">
|
||||
<form id="editForm" hx-post={fmt.Sprintf("/api/project/%d", project.ID)} hx-ext="json-enc">
|
||||
<h3>Project Edition</h3>
|
||||
<div class="mb-2">
|
||||
<label class="form-label" for="projectName">Project Name</label>
|
||||
<input class="form-control" name="name" id="projectName" value={project.ProjectName}>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label" for="projectToken">Project Token</label>
|
||||
<input class="form-control" name="token" id="projectToken" value={project.Token} type="password">
|
||||
</div>
|
||||
<button class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('editForm').addEventListener('htmx:afterRequest', function (ev) {
|
||||
if (ev.detail.successful) {
|
||||
location.reload()
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
templ BadgeMaker(project store.Project) {
|
||||
<div class="container mb-4">
|
||||
<div>
|
||||
<h3>Badge maker</h3>
|
||||
<label class="form-label" for="badgeType">Type of badge</label>
|
||||
<select class="form-control"
|
||||
id="badgeType"
|
||||
name="type"
|
||||
hx-target="#image"
|
||||
hx-target-error="#image"
|
||||
hx-post={fmt.Sprintf("/project/badge/%d", project.ID)}>
|
||||
<option value="coverage">Coverage</option>
|
||||
<option value="duplicated_lines_density">Duplicated Lines(%)</option>
|
||||
<option value="ncloc">Lines of Code</option>
|
||||
<option selected="selected" value="alert_status">Quality Gate Status</option>
|
||||
<option value="security_hotspots">Security Hotspots</option>
|
||||
<option value="software_quality_reliability_issues">Reliability Issues</option>
|
||||
<option value="software_quality_maintainability_issues">Maintainability Issues</option>
|
||||
<option value="software_quality_reliability_issues">Reliability Issues</option>
|
||||
<option value="software_quality_security_issues">Security Issues</option>
|
||||
<option value="software_quality_maintainability_rating">Maintainability Rating</option>
|
||||
<option value="software_quality_reliability_rating">Reliability Rating</option>
|
||||
<option value="software_quality_security_rating">Security Rating</option>
|
||||
<option value="software_quality_maintainability_remediation_effort">Technical Debt</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<label class="form-label" for="imageURL">Image URL</label>
|
||||
<div class="mb-2" id="image" hx-trigger="load" hx-get={fmt.Sprintf("/project/badge/%d/alert_status", project.ID)}>
|
||||
</div>
|
||||
<input class="form-control disabled" id="imageURL" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const projectId = parseInt(document.getElementById("projectID").textContent);
|
||||
const badgeType = document.getElementById("badgeType");
|
||||
const imageUrl = document.getElementById("imageURL");
|
||||
function resetImage() {
|
||||
imageUrl.value = `${location.origin}/project/badge/${projectId}/${badgeType.value}`
|
||||
}
|
||||
resetImage()
|
||||
badgeType.addEventListener('htmx:afterRequest', function (ev) {
|
||||
if (ev.detail.successful){
|
||||
resetImage()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
}
|
||||
|
||||
templ DangerZone(project store.Project) {
|
||||
<div class="container">
|
||||
<h3>Danger Zone</h3>
|
||||
<button class="btn btn-danger" id="deleteProject" hx-delete={fmt.Sprintf("/api/project/%d", project.ID)} hx-confirm="Are you sure to delete this project?">Delete Project</button>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('deleteProject').addEventListener('htmx:afterRequest', function (ev) {
|
||||
if (ev.detail.successful) {
|
||||
location.href = "/"
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
56
templates/user.templ
Normal file
56
templates/user.templ
Normal file
|
@ -0,0 +1,56 @@
|
|||
package templates
|
||||
|
||||
import "sonarqube-badge/store"
|
||||
|
||||
templ User(user store.User) {
|
||||
<div class="container">
|
||||
<h2 class="my-4">Profile</h2>
|
||||
<div class="mb-4">
|
||||
<form id="nameForm" hx-post="/api/user/username">
|
||||
<label for="name" class="form-label">Username</label>
|
||||
<input id="name" class="form-control" name="username" value={user.Username}>
|
||||
<button class="btn btn-primary mt-2">Change Username</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<form id="emailForm" hx-post="/api/user/email">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input id="email" class="form-control" name="email" value={user.Email}>
|
||||
<button class="btn btn-primary mt-2">Change Email</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<form id="passwordForm" hx-post="/api/user/password">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input id="password" class="form-control" name="password" type="password" value>
|
||||
<label for="verifyPassword" class="form-label">Verify Password</label>
|
||||
<input id="verifyPassword" class="form-control" name="verify_password" type="password" value>
|
||||
<button class="btn btn-primary mt-2">Change Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const passwordField = document.getElementById("password");
|
||||
const verifPasswordField = document.getElementById("verifyPassword")
|
||||
document.getElementById("nameForm").addEventListener("htmx:afterRequest", (ev) => {
|
||||
if (ev.detail.successful) {
|
||||
alert("Successfully changed username")
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById("emailForm").addEventListener("htmx:afterRequest", (ev) => {
|
||||
if (ev.detail.successful) {
|
||||
alert("Successfully changed password")
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById("passwordForm").addEventListener("htmx:afterRequest", (ev) => {
|
||||
if (ev.detail.successful) {
|
||||
passwordField.value = ""
|
||||
verifPasswordField.value = ""
|
||||
alert("Successfully changed password")
|
||||
}
|
||||
})
|
||||
</script>
|
||||
}
|
Loading…
Reference in a new issue