diff --git a/.dockerignore b/.dockerignore index 3fa43d4..fc1c469 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,4 +5,6 @@ Dockerfile docker-compose.yml LICENSE README.md -assets/ \ No newline at end of file +assets/ +build/ +build.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8a6f179..99f2df6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ gopath/ -.idea/ \ No newline at end of file +.idea/ +build/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index d2a04bb..b81d623 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ CMD ["/inviter"] ENV GITHUB_ORG_NAME="" ENV GITHUB_TOKEN="" ENV GITHUB_GROUP_NAME="" -ENV INVITE_CODE="" +ENV INVITE_CODE_HASH="" ENV TLS_CERT="" ENV TLS_KEY="" ENV HTTP_PORT="80" diff --git a/README.md b/README.md index bd7a7c8..8c57b3f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # GitHub Inviter -GitHub Inviter is a web application that allows users to join a specific GitHub organization and team using an -invitation code. The application provides a simple web interface where users can enter their GitHub username and the -provided invitation code to get added to the organization's team. +GitHub Inviter is a web application that allows users to join a specific GitHub organization and a team (optional) using +an invitation code (optional). The application provides a simple web interface where users can enter their +GitHub username and the provided invitation code to get added to the organization's team. ## Features @@ -14,6 +14,7 @@ provided invitation code to get added to the organization's team. ## Screenshot ![Screenshot](/assets/images/index.png) +![Screenshot](/assets/images/success.png) ## Configuration @@ -23,13 +24,21 @@ The application is configured using environment variables. Here are the availabl |----------------------|---------------------------------------------------------|----------|---------| | `GITHUB_ORG_NAME` | The name of your GitHub organization | Yes | - | | `GITHUB_TOKEN` | GitHub personal access token with necessary permissions | Yes | - | -| `GITHUB_GROUP_NAME` | The name of the team in your organization | Yes | - | -| `INVITE_CODE` | The invitation code users need to provide | Yes | - | +| `GITHUB_GROUP_NAME` | The name of the team in your organization | No | - | +| `INVITE_CODE_HASH` | The invitation code users need to provide | No | - | | `HTTP_PORT` | The port on which the application will run | No | 80 | | `HTTPS_PORT` | The port on which the application will run (https) | No | 443 | | `TLS_CERT` | Path to the TLS certificate file | No | - | | `TLS_KEY` | Path to the TLS key file | No | - | +### How to generate the INVITE_CODE_HASH + +- On Linux: + - open a terminal + - write `echo -n 'invite code' | sha256sum` where *invite code* is your string **leave the '** +- On any other: + - go [here](https://emn178.github.io/online-tools/sha256.html) + ## Running with Docker Compose Here's an example `docker-compose.yml` file to run the GitHub Inviter: @@ -43,10 +52,10 @@ services: - GITHUB_ORG_NAME=your-org-name - GITHUB_TOKEN=your-github-token - GITHUB_GROUP_NAME=your-team-name - - INVITE_CODE=your-invite-code + - INVITE_CODE_HASH=your-invite-code - HTTP_PORT=80 - - HTTPS_PORT=443 # Uncomment the following lines if you want to use TLS + # - HTTPS_PORT=443 # - TLS_CERT=/path/to/your/cert.pem # - TLS_KEY=/path/to/your/key.pem ports: @@ -68,7 +77,9 @@ To run the application: docker-compose up -d ``` -The application will be available at `http://127.0.0.1:8080` (or `https://127.0.0.1:8080` if TLS is configured). +The application will be available at `http://127.0.0.1/` (or `https://127.0.0.1/` if TLS is configured) unless +otherwise +specified with environment variables. ## About the token diff --git a/assets/images/success.png b/assets/images/success.png new file mode 100644 index 0000000..ca8129a Binary files /dev/null and b/assets/images/success.png differ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..0f5c630 --- /dev/null +++ b/build.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Nome del file Go che desideri compilare +SOURCE_FILE="main.go" + +# Nome del binario risultante +OUTPUT_NAME="inviter" +OUTPUT_PATH="./build" + +# Funzione per compilare l'applicazione +build() { + local os=$1 + local arch=$2 + local output=$3 + + echo "Compilazione per $os/$arch..." + + # Impostiamo le variabili di ambiente per la compilazione + GOOS=$os GOARCH=$arch CGO_ENABLED=0 go build -ldflags="-s -w" -o "$OUTPUT_PATH/$output" $SOURCE_FILE + + if [ $? -ne 0 ]; then + echo "Errore durante la compilazione per $os/$arch" + exit 1 + fi + + echo "Compilazione completata: $output" +} + +# Compilazione per Windows (amd64) +build "windows" "amd64" "${OUTPUT_NAME}_windows_amd64.exe" + +# Compilazione per Linux (amd64) +build "linux" "amd64" "${OUTPUT_NAME}_linux_amd64" + +# Compilazione per Linux (arm64) +build "linux" "arm64" "${OUTPUT_NAME}_linux_arm64" + +# Compilazione per macOS (Intel) +build "darwin" "amd64" "${OUTPUT_NAME}_macos_amd64" + +# Compilazione per macOS (ARM - serie M) +build "darwin" "arm64" "${OUTPUT_NAME}_macos_arm64" + +echo "Tutte le compilazioni sono completate." diff --git a/config/config.go b/config/config.go index e4a30a5..d841990 100644 --- a/config/config.go +++ b/config/config.go @@ -11,8 +11,8 @@ import ( type AppConfig struct { OrgName string //mandatory Token string //mandatory - GroupName string //mandatory - InviteCode []byte //mandatory + GroupName string //optional + InviteCode []byte //optional HttpPort string //optional (default 80) HttpsPort string //optional (default 443) TlsCert string //optional @@ -21,7 +21,15 @@ type AppConfig struct { var conf AppConfig -func Load() bool { +type EnabledFeatures struct { + Code bool + Tls bool + Group bool +} + +var features EnabledFeatures + +func Load() { // Check for mandatory environment variables orgName := strings.Trim(os.Getenv("GITHUB_ORG_NAME"), " ") if len(orgName) == 0 { @@ -33,14 +41,14 @@ func Load() bool { log.Fatal("GITHUB_TOKEN environment variable must be set") } - inviteCode := strings.Trim(os.Getenv("INVITE_CODE"), " ") - if len(inviteCode) == 0 { - log.Fatal("INVITE_CODE environment variable must be set") + groupName := strings.Trim(os.Getenv("GITHUB_GROUP_NAME"), " ") + if len(groupName) != 0 { + features.Group = true } - groupName := strings.Trim(os.Getenv("GITHUB_GROUP_NAME"), " ") - if len(groupName) == 0 { - log.Fatal("GROUP_NAME environment variable must be set") + inviteCode := strings.Trim(os.Getenv("INVITE_CODE_HASH"), " ") + if len(inviteCode) != 0 { + features.Code = true } // Set the optional environment variables, using defaults if not set @@ -57,7 +65,7 @@ func Load() bool { OrgName: orgName, Token: token, GroupName: strings.ToLower(groupName), - InviteCode: hash.CalculateHash(inviteCode), + InviteCode: hash.HexToByteArray(inviteCode), HttpPort: httpPort, HttpsPort: httpsPort, TlsCert: strings.Trim(os.Getenv("TLS_CERT"), " "), @@ -72,10 +80,8 @@ func Load() bool { log.Fatalf("Key file: %s does not exist", conf.TlsKey) } - return true + features.Tls = true } - - return false } func OrgName() string { @@ -109,3 +115,15 @@ func TlsCert() string { func TlsKey() string { return conf.TlsKey } + +func IsTlsEnable() bool { + return features.Tls +} + +func IsCodeRequired() bool { + return features.Code +} + +func IsGroupEnable() bool { + return features.Group +} diff --git a/docker-compose.yml b/docker-compose.yml index df60732..2b1ac08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: - GITHUB_ORG_NAME=your-org-name - GITHUB_TOKEN=your-github-token - GITHUB_GROUP_NAME=your-team-name - - INVITE_CODE=your-invite-code + - INVITE_CODE_HASH=your-invite-code - HTTP_PORT=80 - HTTPS_PORT=443 # Uncomment the following lines if you want to use TLS diff --git a/github/github.go b/github/github.go index 8acb0bb..ddb1a6d 100644 --- a/github/github.go +++ b/github/github.go @@ -42,6 +42,37 @@ func AddUserToGroup(username string) error { return nil } +func AddUserToOrg(username string) error { + url := fmt.Sprintf("%s/orgs/%s/memberships/%s", baseUrl, config.OrgName(), username) + req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(`{"role":"member"}`))) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+config.Token()) + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode == http.StatusUnauthorized { + return fmt.Errorf("the token does not have the required permissions") + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to add user to group: %s", string(body)) + } + + return nil +} + func GetOrgLogoUrl(orgName string) (string, error) { url := fmt.Sprintf("%s/orgs/%s", baseUrl, orgName) req, err := http.NewRequest("GET", url, nil) diff --git a/handlers/handlers.go b/handlers/handlers.go index 7ae49de..dff61e8 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -21,7 +21,14 @@ var ( func InitCache() error { cacheMutex.RLock() var err error - templateCache, err = template.ParseFiles("./templates/index.html") + + // choose the template based on whether the invitation code is needed + if config.IsCodeRequired() { + templateCache, err = template.ParseFiles("./templates/index-code.html") + } else { + templateCache, err = template.ParseFiles("./templates/index.html") + } + if err != nil { cacheMutex.RUnlock() return err @@ -43,12 +50,17 @@ func MainPage(w http.ResponseWriter, _ *http.Request) { cachedLogo := logoCache cacheMutex.RUnlock() + name := config.OrgName() + if config.IsGroupEnable() { + name += "'s " // display org's team + } + data := struct { OrgName string LogoURL string TeamName string }{ - OrgName: config.OrgName(), + OrgName: name, LogoURL: cachedLogo, TeamName: config.GroupName(), } @@ -70,21 +82,32 @@ func Submit(w http.ResponseWriter, r *http.Request) { http.Error(w, "Username is required", http.StatusBadRequest) return } - inviteCode := strings.Trim(r.FormValue("inviteCode"), " ") - if inviteCode == "" { - http.Error(w, "Invite code is required", http.StatusBadRequest) - return - } - if !hash.Compare(inviteCode, config.InviteCode()) { - http.Error(w, "Invalid username or invitation code", http.StatusUnauthorized) - log.Printf("User: %s, tried to access with code: %s", username, inviteCode) - return + + if config.IsCodeRequired() { + inviteCode := strings.Trim(r.FormValue("inviteCode"), " ") + if inviteCode == "" { + http.Error(w, "Invite code is required", http.StatusBadRequest) + return + } + if !hash.Compare(inviteCode, config.InviteCode()) { + http.Error(w, "Invalid username or invitation code", http.StatusUnauthorized) + log.Printf("User: %s, tried to access with code: %s", username, inviteCode) + return + } } - err := github.AddUserToGroup(username) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to add user to group: %v", err), http.StatusInternalServerError) - return + if config.IsGroupEnable() { + err := github.AddUserToGroup(username) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to add user to group: %v", err), http.StatusInternalServerError) + return + } + } else { + err := github.AddUserToOrg(username) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to add user to org: %v", err), http.StatusInternalServerError) + return + } } http.Redirect(w, r, "/success", http.StatusSeeOther) diff --git a/hash/hash.go b/hash/hash.go index f17e52e..5671292 100644 --- a/hash/hash.go +++ b/hash/hash.go @@ -3,14 +3,30 @@ package hash import ( "bytes" "crypto/sha256" + "encoding/hex" + "log" ) -func CalculateHash(password string) []byte { - h := sha256.New() - h.Write([]byte(password)) - return h.Sum(nil) +func CalculateHash(code string) []byte { + hash := sha256.Sum256([]byte(code)) + return hash[:] } -func Compare(password string, hash []byte) bool { - return bytes.Equal(hash, CalculateHash(password)) +func Compare(code string, hash []byte) bool { + return bytes.Equal(hash, CalculateHash(code)) +} + +func HexToByteArray(hexString string) []byte { + // Remove "0x" prefix if present + if len(hexString) >= 2 && hexString[:2] == "0x" { + hexString = hexString[2:] + } + + // Decode hex string to byte slice + byteArray, err := hex.DecodeString(hexString) + if err != nil { + log.Fatalf("failed to decode hex string: %v", err) + } + + return byteArray } diff --git a/main.go b/main.go index 9287b86..6f78bff 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,7 @@ import ( func main() { // Load configuration - tlsEnable := config.Load() + config.Load() // Handle form submission http.HandleFunc("/submit", handlers.Submit) @@ -29,7 +29,7 @@ func main() { log.Fatalf("Error initializing cache: %v", err) } - if tlsEnable { + if config.IsTlsEnable() { go func() { // Start HTTP server that redirects all traffic to HTTPS log.Println("Starting HTTP to HTTPS redirect") diff --git a/templates/index-code.html b/templates/index-code.html new file mode 100644 index 0000000..a0da9b5 --- /dev/null +++ b/templates/index-code.html @@ -0,0 +1,76 @@ + + +
+ + +