Rewritten in go

This commit is contained in:
MarcUs7i 2025-04-02 08:37:01 +02:00
parent b131756235
commit b17e528180
23 changed files with 989 additions and 18 deletions

View file

@ -0,0 +1,126 @@
package handlers
import (
"context"
"net/http"
"github.com/gorilla/mux"
"github.com/marcus7i/ulinkshortener/internal/database"
"github.com/marcus7i/ulinkshortener/internal/models"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
func (h *Handler) GetAnalytics(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
accountID := vars["accountID"]
ctx := context.Background()
var user models.User
err := h.DB.Collection(database.UsersCollection).FindOne(ctx, bson.M{"account_id": accountID}).Decode(&user)
if err != nil {
respondWithError(w, http.StatusUnauthorized, "Invalid account")
return
}
cursor, err := h.DB.Collection(database.LinksCollection).Find(ctx, bson.M{"account_id": accountID})
if err != nil {
respondWithError(w, http.StatusInternalServerError, "Failed to retrieve links")
return
}
defer cursor.Close(ctx)
var links []models.Link
if err = cursor.All(ctx, &links); err != nil {
respondWithError(w, http.StatusInternalServerError, "Failed to process links")
return
}
cursor, err = h.DB.Collection(database.AnalyticsCollection).Find(ctx, bson.M{"account_id": accountID})
if err != nil {
respondWithError(w, http.StatusInternalServerError, "Failed to retrieve analytics")
return
}
defer cursor.Close(ctx)
var analytics []map[string]interface{}
if err = cursor.All(ctx, &analytics); err != nil {
respondWithError(w, http.StatusInternalServerError, "Failed to process analytics")
return
}
respondWithJSON(w, http.StatusOK, map[string]interface{}{
"links": links,
"analytics": analytics,
})
}
func (h *Handler) GetStatsData(ctx context.Context) (models.Stats, error) {
totalLinks, err := h.DB.Collection(database.LinksCollection).CountDocuments(ctx, bson.M{})
if err != nil {
totalLinks = 0
}
totalClicks, err := h.DB.Collection(database.AnalyticsCollection).CountDocuments(ctx, bson.M{})
if err != nil {
totalClicks = 0
}
ipVersionsPipeline := mongo.Pipeline{
{{Key: "$group", Value: bson.D{{Key: "_id", Value: "$ip_version"}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}},
{{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}},
}
ipVersionsCursor, err := h.DB.Collection(database.AnalyticsCollection).Aggregate(ctx, ipVersionsPipeline)
var ipVersions []models.StatItem
if err == nil {
ipVersionsCursor.All(ctx, &ipVersions)
}
osPipeline := mongo.Pipeline{
{{Key: "$group", Value: bson.D{{Key: "_id", Value: "$platform"}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}},
{{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}},
{{Key: "$limit", Value: 10}},
}
osCursor, err := h.DB.Collection(database.AnalyticsCollection).Aggregate(ctx, osPipeline)
var osStats []models.StatItem
if err == nil {
osCursor.All(ctx, &osStats)
}
countryPipeline := mongo.Pipeline{
{{Key: "$group", Value: bson.D{{Key: "_id", Value: "$country"}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}},
{{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}},
{{Key: "$limit", Value: 10}},
}
countryCursor, err := h.DB.Collection(database.AnalyticsCollection).Aggregate(ctx, countryPipeline)
var countryStats []models.StatItem
if err == nil {
countryCursor.All(ctx, &countryStats)
}
ispPipeline := mongo.Pipeline{
{{Key: "$group", Value: bson.D{{Key: "_id", Value: "$isp"}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}},
{{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}},
{{Key: "$limit", Value: 10}},
}
ispCursor, err := h.DB.Collection(database.AnalyticsCollection).Aggregate(ctx, ispPipeline)
var ispStats []models.StatItem
if err == nil {
ispCursor.All(ctx, &ispStats)
}
stats := models.Stats{
TotalLinks: totalLinks,
TotalClicks: totalClicks,
ChartData: models.ChartData{
IPVersions: ipVersions,
OSStats: osStats,
CountryStats: countryStats,
ISPStats: ispStats,
},
LoggedIn: false,
}
return stats, nil
}

View file

@ -0,0 +1,95 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/marcus7i/ulinkshortener/internal/database"
"github.com/marcus7i/ulinkshortener/internal/models"
"github.com/marcus7i/ulinkshortener/internal/utils"
"go.mongodb.org/mongo-driver/bson"
)
type LoginRequest struct {
AccountID string `json:"account_id"`
}
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
accountID := utils.GenerateAccountID()
for {
var user models.User
err := h.DB.Collection(database.UsersCollection).FindOne(ctx, bson.M{"account_id": accountID}).Decode(&user)
if err != nil {
break
}
accountID = utils.GenerateAccountID()
}
_, err := h.DB.Collection(database.UsersCollection).InsertOne(ctx, models.User{
AccountID: accountID,
CreatedAt: time.Now(),
})
if err != nil {
respondWithError(w, http.StatusInternalServerError, "Failed to create account")
return
}
http.SetCookie(w, &http.Cookie{
Name: "account_id",
Value: accountID,
Path: "/",
MaxAge: 31536000, // 1 year
HttpOnly: true,
Secure: r.TLS != nil,
SameSite: http.SameSiteLaxMode,
})
respondWithJSON(w, http.StatusOK, map[string]string{"account_id": accountID})
}
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid request")
return
}
ctx := context.Background()
var user models.User
err := h.DB.Collection(database.UsersCollection).FindOne(ctx, bson.M{"account_id": req.AccountID}).Decode(&user)
if err != nil {
respondWithError(w, http.StatusUnauthorized, "Invalid account ID")
return
}
http.SetCookie(w, &http.Cookie{
Name: "account_id",
Value: req.AccountID,
Path: "/",
MaxAge: 31536000, // 1 year
HttpOnly: true,
Secure: r.TLS != nil,
SameSite: http.SameSiteLaxMode,
})
respondWithJSON(w, http.StatusOK, map[string]bool{"success": true})
}
func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "account_id",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: r.TLS != nil,
SameSite: http.SameSiteLaxMode,
})
respondWithJSON(w, http.StatusOK, map[string]bool{"success": true})
}

View file

@ -0,0 +1,38 @@
package handlers
import (
"encoding/json"
"net/http"
"github.com/marcus7i/ulinkshortener/internal/database"
)
type Handler struct {
DB *database.MongoDB
}
func NewHandler(db *database.MongoDB) *Handler {
return &Handler{
DB: db,
}
}
func respondWithError(w http.ResponseWriter, code int, message string) {
respondWithJSON(w, code, map[string]string{"error": message})
}
func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
response, _ := json.Marshal(payload)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(response)
}
func getAccountIDFromCookie(r *http.Request) string {
cookie, err := r.Cookie("account_id")
if err != nil {
return ""
}
return cookie.Value
}

View file

@ -0,0 +1,132 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/marcus7i/ulinkshortener/internal/database"
"github.com/marcus7i/ulinkshortener/internal/models"
"github.com/marcus7i/ulinkshortener/internal/utils"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type CreateLinkRequest struct {
AccountID string `json:"account_id"`
URL string `json:"url"`
}
type CreateLinkResponse struct {
ShortURL string `json:"short_url"`
}
func (h *Handler) CreateLink(w http.ResponseWriter, r *http.Request) {
var req CreateLinkRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid request")
return
}
ctx := context.Background()
user := h.DB.Collection(database.UsersCollection).FindOne(ctx, bson.M{"account_id": req.AccountID})
if user.Err() != nil {
respondWithError(w, http.StatusUnauthorized, "Invalid account")
return
}
if !utils.IsValidURL(req.URL) {
respondWithError(w, http.StatusBadRequest, "Invalid URL. Please provide a valid URL with scheme (e.g., http:// or https://)")
return
}
shortID := utils.GenerateShortID()
link := models.Link{
ShortID: shortID,
TargetURL: req.URL,
AccountID: req.AccountID,
CreatedAt: time.Now(),
}
_, err := h.DB.Collection(database.LinksCollection).InsertOne(ctx, link)
if err != nil {
respondWithError(w, http.StatusInternalServerError, "Failed to create link")
return
}
respondWithJSON(w, http.StatusOK, CreateLinkResponse{
ShortURL: fmt.Sprintf("/l/%s", shortID),
})
}
func (h *Handler) RedirectLink(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
shortID := vars["shortID"]
ctx := context.Background()
var link models.Link
err := h.DB.Collection(database.LinksCollection).FindOne(ctx, bson.M{"short_id": shortID}).Decode(&link)
if err != nil {
if err == mongo.ErrNoDocuments {
http.Error(w, "Link not found", http.StatusNotFound)
} else {
http.Error(w, "Server error", http.StatusInternalServerError)
}
return
}
clientInfo := utils.GetClientInfo(r)
clientInfo["link_id"] = shortID
clientInfo["account_id"] = link.AccountID
_, err = h.DB.Collection(database.AnalyticsCollection).InsertOne(ctx, clientInfo)
if err != nil {
fmt.Printf("Failed to log analytics: %v\n", err)
}
http.Redirect(w, r, link.TargetURL, http.StatusFound)
}
func (h *Handler) DeleteLink(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
shortID := vars["shortID"]
accountID := getAccountIDFromCookie(r)
if accountID == "" {
respondWithError(w, http.StatusUnauthorized, "Not logged in")
return
}
ctx := context.Background()
var link models.Link
err := h.DB.Collection(database.LinksCollection).FindOne(ctx, bson.M{
"short_id": shortID,
"account_id": accountID,
}).Decode(&link)
if err != nil {
if err == mongo.ErrNoDocuments {
respondWithError(w, http.StatusNotFound, "Link not found or unauthorized")
} else {
respondWithError(w, http.StatusInternalServerError, "Server error")
}
return
}
_, err = h.DB.Collection(database.LinksCollection).DeleteOne(ctx, bson.M{"short_id": shortID})
if err != nil {
respondWithError(w, http.StatusInternalServerError, "Failed to delete link")
return
}
_, err = h.DB.Collection(database.AnalyticsCollection).DeleteMany(ctx, bson.M{"link_id": shortID})
if err != nil {
fmt.Printf("Failed to delete analytics: %v\n", err)
}
respondWithJSON(w, http.StatusOK, map[string]bool{"success": true})
}

39
internal/api/router.go Normal file
View file

@ -0,0 +1,39 @@
package api
import (
"net/http"
"github.com/gorilla/mux"
"github.com/marcus7i/ulinkshortener/internal/api/handlers"
"github.com/marcus7i/ulinkshortener/internal/database"
)
func SetupRouter(db *database.MongoDB) *mux.Router {
r := mux.NewRouter()
h := handlers.NewHandler(db)
fs := http.FileServer(http.Dir("./web/static"))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs))
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/static/css/style.css" {
w.Header().Set("Content-Type", "text/css")
} else if r.URL.Path == "/static/js/script.js" {
w.Header().Set("Content-Type", "application/javascript")
}
next.ServeHTTP(w, r)
})
})
r.HandleFunc("/register", h.Register).Methods("POST")
r.HandleFunc("/login", h.Login).Methods("POST")
r.HandleFunc("/logout", h.Logout).Methods("POST")
r.HandleFunc("/create", h.CreateLink).Methods("POST")
r.HandleFunc("/l/{shortID}", h.RedirectLink).Methods("GET")
r.HandleFunc("/analytics/{accountID}", h.GetAnalytics).Methods("GET")
r.HandleFunc("/delete/{shortID}", h.DeleteLink).Methods("DELETE")
return r
}