mirror of
https://github.com/Kizuren/uLinkShortener.git
synced 2026-01-06 06:27:53 +01:00
Rewritten in go
This commit is contained in:
parent
b131756235
commit
b17e528180
23 changed files with 989 additions and 18 deletions
126
internal/api/handlers/analytics.go
Normal file
126
internal/api/handlers/analytics.go
Normal 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
|
||||
}
|
||||
95
internal/api/handlers/auth.go
Normal file
95
internal/api/handlers/auth.go
Normal 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})
|
||||
}
|
||||
38
internal/api/handlers/handlers.go
Normal file
38
internal/api/handlers/handlers.go
Normal 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
|
||||
}
|
||||
132
internal/api/handlers/url.go
Normal file
132
internal/api/handlers/url.go
Normal 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
39
internal/api/router.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue