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
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ COPY nginx.conf /etc/nginx/nginx.conf
# Copy supervisord configuration
COPY supervisord.conf /etc/supervisord.conf

# Set environment variables with default values that can be overridden
ENV BRAND_TITLE="GoShort - URL Shortener" \
BRAND_DESCRIPTION="GoShort is a powerful and user-friendly URL shortener. Simplify, manage, and track your links with ease." \
BRAND_KEYWORDS="URL shortener, GoShort, link management, shorten URLs, track links" \
BRAND_AUTHOR="GoShort Team" \
BRAND_THEME_COLOR="#4caf50" \
BRAND_LOGO_TEXT="GoShort" \
BRAND_PRIMARY_COLOR="#3b82f6" \
BRAND_SECONDARY_COLOR="#10b981" \
BRAND_HEADER_TITLE="GoShort - URL Shortener" \
BRAND_FOOTER_TEXT="View the project on" \
BRAND_FOOTER_LINK="https://github.com/kek-Sec/GoShort"

# Expose ports
EXPOSE 80 8080

Expand Down
56 changes: 52 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
### Demo: [https://x.yup.gr](http://x.yup.gr)
### DockerHub Image: [petrakisg/goshort](https://hub.docker.com/r/petrakisg/goshort)

GoShort is a fast and customizable URL shortener built with Go , Svelte and TailwindCSS. It is designed to be self-hosted and easy to deploy.
GoShort is a fast and customizable URL shortener built with Go, Svelte and TailwindCSS. It is designed to be self-hosted and easy to deploy.

![GoShort](web/static/banner.png)

Expand All @@ -17,9 +17,10 @@ GoShort is a fast and customizable URL shortener built with Go , Svelte and Tail

1. [Features](#-features)
2. [Installation](#-installation)
3. [Contributing](#-contributing)
4. [License](#-license)
5. [Security](#-security)
3. [Customization](#-customization)
4. [Contributing](#-contributing)
5. [License](#-license)
6. [Security](#-security)
---

## 🚀 **Features**
Expand All @@ -29,6 +30,7 @@ GoShort is a fast and customizable URL shortener built with Go , Svelte and Tail
- **Self-hosted**: You own your data and can deploy GoShort on your own server.
- **Custom URLs**: You can set custom URLs for your short links.
- **Expiration**: You can set expiration for your short links.
- **White-labeling**: Customize branding elements without rebuilding the Docker image.

---

Expand All @@ -38,6 +40,52 @@ GoShort is a fast and customizable URL shortener built with Go , Svelte and Tail

---

## 🎨 **Customization**

GoShort supports customizing the branding and appearance through environment variables, making it easy to white-label without rebuilding the Docker image.

### Available Customization Options

You can customize the following aspects of the UI by setting these environment variables:

| Environment Variable | Description | Default Value |
|---------------------|-------------|---------------|
| BRAND_TITLE | Browser tab title | GoShort - URL Shortener |
| BRAND_DESCRIPTION | Meta description for SEO | GoShort is a powerful and user-friendly URL shortener... |
| BRAND_KEYWORDS | Meta keywords for SEO | URL shortener, GoShort, link management... |
| BRAND_AUTHOR | Author meta tag | GoShort Team |
| BRAND_THEME_COLOR | Browser theme color | #4caf50 |
| BRAND_LOGO_TEXT | Text logo displayed in the header | GoShort |
| BRAND_PRIMARY_COLOR | Main accent color (buttons, links) | #3b82f6 |
| BRAND_SECONDARY_COLOR | Secondary accent color | #10b981 |
| BRAND_HEADER_TITLE | Main heading on the page | GoShort - URL Shortener |
| BRAND_FOOTER_TEXT | Text shown in the footer | View the project on |
| BRAND_FOOTER_LINK | URL for the footer link | https://github.com/kek-Sec/GoShort |

### Usage Example

Here's how to customize the branding in your docker-compose file:

```yaml
services:
goshort:
image: petrakisg/goshort:1.0.1
environment:
# Database configuration
DATABASE_URL: postgres://user:password@db:5432/goshort

# Branding customization
BRAND_TITLE: "MyCompany URL Shortener"
BRAND_LOGO_TEXT: "MyShort"
BRAND_PRIMARY_COLOR: "#ff5722"
BRAND_SECONDARY_COLOR: "#2196f3"
BRAND_HEADER_TITLE: "MyCompany Link Shortener"
BRAND_FOOTER_TEXT: "Powered by"
BRAND_FOOTER_LINK: "https://mycompany.com"
```

---

## 🤝 **Contributing**

1. Fork the repository.
Expand Down
4 changes: 3 additions & 1 deletion cmd/server/router.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package main

import (
"GoShort/internal/api/v1"
v1 "GoShort/internal/api/v1"

"github.com/gorilla/mux"
)

Expand All @@ -12,6 +13,7 @@ func setupRouter() *mux.Router {
// V1 Routes
apiV1 := router.PathPrefix("/v1").Subrouter()
apiV1.HandleFunc("/shorten", v1.ShortenURL).Methods("POST")
apiV1.HandleFunc("/config", v1.GetConfig).Methods("GET") // Add config endpoint

// Redirect Route (catch-all)
router.HandleFunc("/{shortURL}", v1.RedirectURL).Methods("GET")
Expand Down
11 changes: 11 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ services:
container_name: goshort_app
environment:
DATABASE_URL: postgres://goshort:goshort_password@goshort-db:5432/goshort?sslmode=disable
# Branding customization (uncomment and modify as needed)
# BRAND_TITLE: "MyShort - URL Shortener"
# BRAND_DESCRIPTION: "A fast and customizable URL shortener for your organization"
# BRAND_AUTHOR: "Your Company Name"
# BRAND_THEME_COLOR: "#2563eb"
# BRAND_LOGO_TEXT: "MyShort"
# BRAND_PRIMARY_COLOR: "#2563eb"
# BRAND_SECONDARY_COLOR: "#10b981"
# BRAND_HEADER_TITLE: "MyShort - Simplify Your URLs"
# BRAND_FOOTER_TEXT: "Powered by"
# BRAND_FOOTER_LINK: "https://yourcompany.com"
depends_on:
goshort-db:
condition: service_healthy
Expand Down
7 changes: 7 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ services:
- "8081:80" # Frontend
environment:
DATABASE_URL: postgres://goshort:goshort_password@database:5432/goshort?sslmode=disable
# Branding customization (optional)
BRAND_TITLE: "GoShort - URL Shortener"
BRAND_DESCRIPTION: "A fast and customizable URL shortener"
BRAND_THEME_COLOR: "#4caf50"
BRAND_PRIMARY_COLOR: "#3b82f6"
BRAND_SECONDARY_COLOR: "#10b981"
BRAND_HEADER_TITLE: "GoShort - URL Shortener"
depends_on:
database:
condition: service_healthy
Expand Down
57 changes: 57 additions & 0 deletions internal/api/v1/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package v1

import (
"encoding/json"
"net/http"
"os"
)

// Config represents the frontend configuration
type Config struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Keywords string `json:"keywords,omitempty"`
Author string `json:"author,omitempty"`
ThemeColor string `json:"themeColor,omitempty"`
LogoText string `json:"logoText,omitempty"`
PrimaryColor string `json:"primaryColor,omitempty"`
SecondaryColor string `json:"secondaryColor,omitempty"`
HeaderTitle string `json:"headerTitle,omitempty"`
FooterText string `json:"footerText,omitempty"`
FooterLink string `json:"footerLink,omitempty"`
}

// GetConfig returns the frontend configuration
func GetConfig(w http.ResponseWriter, r *http.Request) {
config := Config{}

// Load config from environment variables
envVars := map[string]*string{
"BRAND_TITLE": &config.Title,
"BRAND_DESCRIPTION": &config.Description,
"BRAND_KEYWORDS": &config.Keywords,
"BRAND_AUTHOR": &config.Author,
"BRAND_THEME_COLOR": &config.ThemeColor,
"BRAND_LOGO_TEXT": &config.LogoText,
"BRAND_PRIMARY_COLOR": &config.PrimaryColor,
"BRAND_SECONDARY_COLOR": &config.SecondaryColor,
"BRAND_HEADER_TITLE": &config.HeaderTitle,
"BRAND_FOOTER_TEXT": &config.FooterText,
"BRAND_FOOTER_LINK": &config.FooterLink,
}

for envVar, field := range envVars {
if value := os.Getenv(envVar); value != "" {
*field = value
}
}

// Set response headers
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") // Prevent caching

// Encode config as JSON and send response with error handling
if err := json.NewEncoder(w).Encode(config); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
}
}
170 changes: 170 additions & 0 deletions internal/api/v1/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package v1

import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetConfig(t *testing.T) {
// Test cases
tests := []struct {
name string
envVars map[string]string
expectedKey string
expectedVal string
}{
{
name: "Default Config Returns Empty When No Env Vars",
// No env vars set
expectedKey: "",
expectedVal: "",
},
{
name: "Returns Title From Environment",
envVars: map[string]string{
"BRAND_TITLE": "Custom Title",
},
expectedKey: "title",
expectedVal: "Custom Title",
},
{
name: "Returns Primary Color From Environment",
envVars: map[string]string{
"BRAND_PRIMARY_COLOR": "#ff0000",
},
expectedKey: "primaryColor",
expectedVal: "#ff0000",
},
{
name: "Returns Multiple Config Values",
envVars: map[string]string{
"BRAND_TITLE": "Custom Title",
"BRAND_DESCRIPTION": "Custom Description",
"BRAND_PRIMARY_COLOR": "#ff0000",
"BRAND_HEADER_TITLE": "Custom Header",
"BRAND_SECONDARY_COLOR": "#00ff00",
},
expectedKey: "headerTitle", // We'll check just this one
expectedVal: "Custom Header",
},
}

// Run tests
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear environment variables
os.Clearenv()

// Set environment variables for this test
for k, v := range tt.envVars {
os.Setenv(k, v)
}

// Create request
req, err := http.NewRequest("GET", "/v1/config", nil)
if err != nil {
t.Fatal(err)
}

// Create response recorder
rr := httptest.NewRecorder()
handler := http.HandlerFunc(GetConfig)

// Serve request
handler.ServeHTTP(rr, req)

// Check status code
assert.Equal(t, http.StatusOK, rr.Code)

// Check Content-Type
assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
assert.Equal(t, "no-cache, no-store, must-revalidate", rr.Header().Get("Cache-Control"))

// If we're not expecting any specific value, just verify it's valid JSON
if tt.expectedKey == "" {
var result map[string]interface{}
err = json.Unmarshal(rr.Body.Bytes(), &result)
assert.NoError(t, err, "Response should be valid JSON")
return
}

// Parse the response
var result Config
err = json.Unmarshal(rr.Body.Bytes(), &result)
assert.NoError(t, err)

// Check the specific field we're testing for
switch tt.expectedKey {
case "title":
assert.Equal(t, tt.expectedVal, result.Title)
case "description":
assert.Equal(t, tt.expectedVal, result.Description)
case "keywords":
assert.Equal(t, tt.expectedVal, result.Keywords)
case "author":
assert.Equal(t, tt.expectedVal, result.Author)
case "themeColor":
assert.Equal(t, tt.expectedVal, result.ThemeColor)
case "logoText":
assert.Equal(t, tt.expectedVal, result.LogoText)
case "primaryColor":
assert.Equal(t, tt.expectedVal, result.PrimaryColor)
case "secondaryColor":
assert.Equal(t, tt.expectedVal, result.SecondaryColor)
case "headerTitle":
assert.Equal(t, tt.expectedVal, result.HeaderTitle)
case "footerText":
assert.Equal(t, tt.expectedVal, result.FooterText)
case "footerLink":
assert.Equal(t, tt.expectedVal, result.FooterLink)
}
})
}
}

func TestGetConfigMultipleValues(t *testing.T) {
// Clear environment variables
os.Clearenv()

// Set multiple environment variables
os.Setenv("BRAND_TITLE", "Test Title")
os.Setenv("BRAND_PRIMARY_COLOR", "#ff0000")
os.Setenv("BRAND_SECONDARY_COLOR", "#00ff00")
os.Setenv("BRAND_FOOTER_TEXT", "Custom Footer")

// Create request
req, err := http.NewRequest("GET", "/v1/config", nil)
if err != nil {
t.Fatal(err)
}

// Create response recorder
rr := httptest.NewRecorder()
handler := http.HandlerFunc(GetConfig)

// Serve request
handler.ServeHTTP(rr, req)

// Check status code
assert.Equal(t, http.StatusOK, rr.Code)

// Parse the response
var result Config
err = json.Unmarshal(rr.Body.Bytes(), &result)
assert.NoError(t, err)

// Check all expected values
assert.Equal(t, "Test Title", result.Title)
assert.Equal(t, "#ff0000", result.PrimaryColor)
assert.Equal(t, "#00ff00", result.SecondaryColor)
assert.Equal(t, "Custom Footer", result.FooterText)

// Check that others are empty
assert.Empty(t, result.Description)
assert.Empty(t, result.Keywords)
}
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion version.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#Application version following https://semver.org/
version: 1.0.1
version: 1.0.2
Loading
Loading