diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..8d2d188 --- /dev/null +++ b/handlers.go @@ -0,0 +1,177 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func (app *App) homePage(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{ + "loggedIn": isLoggedIn(c), + }) +} + +func (app *App) shortenURL(c *gin.Context) { + originalURL := c.PostForm("url") + customName := c.PostForm("custom_name") + + if !isValidURL(originalURL) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid URL"}) + return + } + + shortURL := customName + if shortURL == "" { + shortURL = generateShortURL() + } + + userID := getUserID(c) + + id := uuid.New().String() + + _, err := app.DB.Exec("INSERT INTO links (id, original_url, short_url, user_id) VALUES (?, ?, ?, ?)", + id, originalURL, shortURL, userID) + if err != nil { + log.Printf("Error inserting link: %v", err) + if err.Error() == "UNIQUE constraint failed: links.short_url" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Short URL already exists"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"shortURL": shortURL}) +} + +func (app *App) loginGithub(c *gin.Context) { + url := app.GithubOAuthConfig.AuthCodeURL("state") + c.Redirect(http.StatusFound, url) +} + +func (app *App) githubCallback(c *gin.Context) { + code := c.Query("code") + token, err := app.GithubOAuthConfig.Exchange(c, code) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"}) + return + } + + client := app.GithubOAuthConfig.Client(c, token) + resp, err := client.Get("https://api.github.com/user") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"}) + return + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response body"}) + return + } + + var githubUser struct { + ID int64 `json:"id"` + } + if err := json.Unmarshal(body, &githubUser); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse user info"}) + return + } + + userID := fmt.Sprintf("%d", githubUser.ID) + c.SetCookie("user_id", userID, 3600, "/", "", false, true) + c.Redirect(http.StatusFound, "/") +} + +func (app *App) logout(c *gin.Context) { + c.SetCookie("user_id", "", -1, "/", "", false, true) + c.Redirect(http.StatusFound, "/") +} + +func (app *App) dashboard(c *gin.Context) { + userID := getUserID(c) + if userID == "" { + c.Redirect(http.StatusFound, "/") + return + } + + links, err := app.getUserLinks(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) + return + } + + c.HTML(http.StatusOK, "dashboard.html", gin.H{"links": links, "userId": userID}) +} + +func (app *App) deleteLink(c *gin.Context) { + var payload struct { + ID string `json:"id"` + } + + if err := c.BindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"}) + return + } + + userID := getUserID(c) + + _, err := app.DB.Exec("DELETE FROM links WHERE id = ? AND user_id = ?", payload.ID, userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true, "id": payload.ID}) +} + +func (app *App) updateLink(c *gin.Context) { + var payload struct { + ID string `json:"id"` + NewName string `json:"new_name"` + } + + if err := c.BindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"}) + return + } + + userID := getUserID(c) + + _, err := app.DB.Exec("UPDATE links SET short_url = ? WHERE id = ? AND user_id = ?", payload.NewName, payload.ID, userID) + if err != nil { + if err.Error() == "UNIQUE constraint failed: links.short_url" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Short URL already exists"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true}) +} + +func (app *App) redirectToOriginal(c *gin.Context) { + shortURL := c.Param("shortURL") + + var originalURL string + err := app.DB.QueryRow("SELECT original_url FROM links WHERE short_url = ?", shortURL).Scan(&originalURL) + if err != nil { + if err == sql.ErrNoRows { + c.String(http.StatusNotFound, "Short URL not found") + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + return + } + + c.Redirect(http.StatusFound, originalURL) +} diff --git a/index.go b/index.go index 4426dff..83ccad4 100644 --- a/index.go +++ b/index.go @@ -3,17 +3,12 @@ package main import ( "database/sql" "embed" - "encoding/json" "fmt" "html/template" - "io/fs" - "io/ioutil" "log" - "net/http" "os" "github.com/gin-gonic/gin" - "github.com/google/uuid" "github.com/joho/godotenv" _ "github.com/mattn/go-sqlite3" "golang.org/x/oauth2" @@ -26,19 +21,6 @@ var templatesFS embed.FS //go:embed static/* var staticFS embed.FS -type Link struct { - ID string - OriginalURL string - ShortURL string - UserID string -} - -type App struct { - DB *sql.DB - GithubOAuthConfig *oauth2.Config - Router *gin.Engine -} - func initialize() (*App, error) { gin.SetMode(gin.ReleaseMode) if err := godotenv.Load(); err != nil { @@ -90,230 +72,8 @@ func initGithubOAuth() *oauth2.Config { } } -func (app *App) SetupRoutes() { - staticContent, _ := fs.Sub(staticFS, "static") - app.Router.StaticFS("/static", http.FS(staticContent)) - app.Router.GET("/", app.homePage) - app.Router.POST("/", app.shortenURL) - app.Router.GET("/login", app.loginGithub) - app.Router.GET("/callback", app.githubCallback) - app.Router.GET("/logout", app.logout) - app.Router.GET("/dashboard", app.dashboard) - app.Router.DELETE("/dashboard", app.deleteLink) - app.Router.PUT("/dashboard", app.updateLink) - app.Router.GET("/:shortURL", app.redirectToOriginal) -} - func (app *App) Run() error { port := os.Getenv("PORT") log.Println("Server running on port :" + port) return app.Router.Run(":" + port) } - -func (app *App) createTable() error { - _, err := app.DB.Exec(`CREATE TABLE IF NOT EXISTS links ( - id TEXT PRIMARY KEY NOT NULL UNIQUE, - original_url TEXT NOT NULL, - short_url TEXT NOT NULL UNIQUE, - user_id TEXT - )`) - return err -} - -func (app *App) logout(c *gin.Context) { - c.SetCookie("user_id", "", -1, "/", "", false, true) - c.Redirect(http.StatusFound, "/") -} - -func (app *App) homePage(c *gin.Context) { - c.HTML(http.StatusOK, "index.html", gin.H{ - "loggedIn": isLoggedIn(c), - }) -} - -func (app *App) shortenURL(c *gin.Context) { - originalURL := c.PostForm("url") - customName := c.PostForm("custom_name") - - shortURL := customName - if shortURL == "" { - shortURL = uuid.New().String()[:8] - } - - userID := getUserID(c) - - id := uuid.New().String() - - _, err := app.DB.Exec("INSERT INTO links (id, original_url, short_url, user_id) VALUES (?, ?, ?, ?)", - id, originalURL, shortURL, userID) - if err != nil { - log.Printf("Error inserting link: %v", err) - if err.Error() == "UNIQUE constraint failed: links.short_url" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Short URL already exists"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"shortURL": shortURL}) -} - -func (app *App) loginGithub(c *gin.Context) { - url := app.GithubOAuthConfig.AuthCodeURL("state") - c.Redirect(http.StatusFound, url) -} - -func (app *App) githubCallback(c *gin.Context) { - code := c.Query("code") - token, err := app.GithubOAuthConfig.Exchange(c, code) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"}) - return - } - - client := app.GithubOAuthConfig.Client(c, token) - resp, err := client.Get("https://api.github.com/user") - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"}) - return - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response body"}) - return - } - - var githubUser struct { - ID int64 `json:"id"` - } - if err := json.Unmarshal(body, &githubUser); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse user info"}) - return - } - - userID := fmt.Sprintf("%d", githubUser.ID) - c.SetCookie("user_id", userID, 3600, "/", "", false, true) - c.Redirect(http.StatusFound, "/") -} - -func (app *App) dashboard(c *gin.Context) { - userID := getUserID(c) - if userID == "" { - c.Redirect(http.StatusFound, "/") - return - } - - rows, err := app.DB.Query("SELECT id, original_url, short_url FROM links WHERE user_id = ?", userID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) - return - } - defer rows.Close() - - var links []Link - for rows.Next() { - var link Link - err := rows.Scan(&link.ID, &link.OriginalURL, &link.ShortURL) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) - return - } - links = append(links, link) - } - - c.HTML(http.StatusOK, "dashboard.html", gin.H{"links": links, "userId": userID}) -} - -func (app *App) deleteLink(c *gin.Context) { - var payload struct { - ID string `json:"id"` - } - - if err := c.BindJSON(&payload); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"}) - return - } - - userID := getUserID(c) - - _, err := app.DB.Exec("DELETE FROM links WHERE id = ? AND user_id = ?", payload.ID, userID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"success": true, "id": payload.ID}) -} - -func (app *App) updateLink(c *gin.Context) { - var payload struct { - ID string `json:"id"` - NewName string `json:"new_name"` - } - - if err := c.BindJSON(&payload); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"}) - return - } - - userID := getUserID(c) - - _, err := app.DB.Exec("UPDATE links SET short_url = ? WHERE id = ? AND user_id = ?", payload.NewName, payload.ID, userID) - if err != nil { - if err.Error() == "UNIQUE constraint failed: links.short_url" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Short URL already exists"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"success": true}) -} - -func (app *App) redirectToOriginal(c *gin.Context) { - shortURL := c.Param("shortURL") - - var originalURL string - err := app.DB.QueryRow("SELECT original_url FROM links WHERE short_url = ?", shortURL).Scan(&originalURL) - if err != nil { - if err == sql.ErrNoRows { - c.String(http.StatusNotFound, "Short URL not found") - } else { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - } - return - } - - c.Redirect(http.StatusFound, originalURL) -} - -func isLoggedIn(c *gin.Context) bool { - _, err := c.Cookie("user_id") - return err == nil -} - -func getUserID(c *gin.Context) string { - userID, _ := c.Cookie("user_id") - return userID -} - -func main() { - app, err := initialize() - if err != nil { - log.Fatalf("Failed to initialize app: %v", err) - } - - if err := app.createTable(); err != nil { - log.Fatalf("Failed to create table: %v", err) - } - - app.SetupRoutes() - - if err := app.Run(); err != nil { - log.Fatalf("Failed to run app: %v", err) - } -} diff --git a/ltsNinja b/ltsNinja new file mode 100755 index 0000000..26c3349 Binary files /dev/null and b/ltsNinja differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..ece99bf --- /dev/null +++ b/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "log" +) + +func main() { + app, err := initialize() + if err != nil { + log.Fatalf("Failed to initialize app: %v", err) + } + + if err := app.createTable(); err != nil { + log.Fatalf("Failed to create table: %v", err) + } + + app.SetupRoutes() + + if err := app.Run(); err != nil { + log.Fatalf("Failed to run app: %v", err) + } +} diff --git a/models.go b/models.go new file mode 100644 index 0000000..ca16a07 --- /dev/null +++ b/models.go @@ -0,0 +1,22 @@ +package main + +import ( + "database/sql" + + "github.com/gin-gonic/gin" + "golang.org/x/oauth2" +) + +type App struct { + DB *sql.DB + GithubOAuthConfig *oauth2.Config + Router *gin.Engine +} + +type Link struct { + ID string + OriginalURL string + ShortURL string + UserID string + CreatedAt string +} diff --git a/routes.go b/routes.go new file mode 100644 index 0000000..d16aed1 --- /dev/null +++ b/routes.go @@ -0,0 +1,24 @@ +package main + +import ( + "io/fs" + "net/http" +) + +func (app *App) SetupRoutes() { + staticContent, _ := fs.Sub(staticFS, "static") + app.Router.StaticFS("/static", http.FS(staticContent)) + + app.Router.GET("/", app.homePage) + app.Router.POST("/", app.shortenURL) + + app.Router.GET("/login", app.loginGithub) + app.Router.GET("/callback", app.githubCallback) + app.Router.GET("/logout", app.logout) + + app.Router.GET("/dashboard", app.dashboard) + app.Router.DELETE("/dashboard", app.deleteLink) + app.Router.PUT("/dashboard", app.updateLink) + app.Router.GET("/:shortURL", app.redirectToOriginal) + +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..be4bad1 --- /dev/null +++ b/utils.go @@ -0,0 +1,67 @@ +package main + +import ( + "math/rand" + "net/url" + "time" + + "github.com/gin-gonic/gin" +) + +func isLoggedIn(c *gin.Context) bool { + _, err := c.Cookie("user_id") + return err == nil +} + +func getUserID(c *gin.Context) string { + userID, _ := c.Cookie("user_id") + return userID +} + +func isValidURL(str string) bool { + u, err := url.Parse(str) + return err == nil && u.Scheme != "" && u.Host != "" +} + +func generateShortURL() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + const length = 8 + + rand.Seed(time.Now().UnixNano()) + result := make([]byte, length) + for i := range result { + result[i] = charset[rand.Intn(len(charset))] + } + return string(result) +} + +func (app *App) getUserLinks(userID string) ([]Link, error) { + rows, err := app.DB.Query("SELECT id, original_url, short_url FROM links WHERE user_id = ?", userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var links []Link + for rows.Next() { + var link Link + err := rows.Scan(&link.ID, &link.OriginalURL, &link.ShortURL) + if err != nil { + return nil, err + } + links = append(links, link) + } + + return links, nil +} + +func (app *App) createTable() error { + _, err := app.DB.Exec(`CREATE TABLE IF NOT EXISTS links ( + id TEXT PRIMARY KEY NOT NULL UNIQUE, + original_url TEXT NOT NULL, + short_url TEXT NOT NULL UNIQUE, + user_id TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )`) + return err +}