feat(struct) moving into src directory
This commit is contained in:
43
src/go.mod
Normal file
43
src/go.mod
Normal file
@@ -0,0 +1,43 @@
|
||||
module itsmrval/ltsNinja
|
||||
|
||||
go 1.22.5
|
||||
|
||||
require (
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
|
||||
github.com/bytedance/sonic v1.11.9 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/contrib v0.0.0-20240508051311-c1c6bf0061b0 // indirect
|
||||
github.com/gin-gonic/gin v1.10.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.22.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.3.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
102
src/go.sum
Normal file
102
src/go.sum
Normal file
@@ -0,0 +1,102 @@
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg=
|
||||
github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/contrib v0.0.0-20240508051311-c1c6bf0061b0 h1:EUFmvQ8ffefnSAmaUZd9HZYZSw9w/bFjp3FiNaJ5WmE=
|
||||
github.com/gin-gonic/contrib v0.0.0-20240508051311-c1c6bf0061b0/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
||||
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg=
|
||||
github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
177
src/handlers.go
Normal file
177
src/handlers.go
Normal file
@@ -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)
|
||||
}
|
||||
79
src/index.go
Normal file
79
src/index.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/github"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
var templatesFS embed.FS
|
||||
|
||||
//go:embed static/*
|
||||
var staticFS embed.FS
|
||||
|
||||
func initialize() (*App, error) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Println("No .env file found")
|
||||
}
|
||||
|
||||
db, err := initDB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize database: %w", err)
|
||||
}
|
||||
oauthConfig := initGithubOAuth()
|
||||
|
||||
router := gin.Default()
|
||||
tmpl := template.Must(template.ParseFS(templatesFS, "templates/*"))
|
||||
router.SetHTMLTemplate(tmpl)
|
||||
|
||||
return &App{
|
||||
DB: db,
|
||||
GithubOAuthConfig: oauthConfig,
|
||||
Router: router,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func initDB() (*sql.DB, error) {
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
if dbPath == "" {
|
||||
return nil, fmt.Errorf("DB_PATH not set in environment")
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
if err = db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func initGithubOAuth() *oauth2.Config {
|
||||
return &oauth2.Config{
|
||||
ClientID: os.Getenv("GITHUB_CLIENT_ID"),
|
||||
ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
|
||||
RedirectURL: os.Getenv("GITHUB_REDIRECT_URL"),
|
||||
Scopes: []string{"user:email"},
|
||||
Endpoint: github.Endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) Run() error {
|
||||
port := os.Getenv("PORT")
|
||||
log.Println("Server running on port :" + port)
|
||||
return app.Router.Run(":" + port)
|
||||
}
|
||||
22
src/main.go
Normal file
22
src/main.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
22
src/models.go
Normal file
22
src/models.go
Normal file
@@ -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
|
||||
}
|
||||
24
src/routes.go
Normal file
24
src/routes.go
Normal file
@@ -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)
|
||||
|
||||
}
|
||||
40
src/static/img/hero.svg
Normal file
40
src/static/img/hero.svg
Normal file
@@ -0,0 +1,40 @@
|
||||
<svg width="1572" height="795" viewBox="0 0 1572 795" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_340_660" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="1572" height="795">
|
||||
<rect width="1572" height="795" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_340_660)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M862.574 -54.9121V0.955067L808.953 0.955063L808.953 1.95506L862.574 1.95507V74.6094H808.953L808.953 75.6094H862.574V148.264H808.953L808.953 149.264H862.574V221.918H808.953L808.953 222.918H862.574V295.572H808.953L808.953 296.572H862.574V369.227H808.953L808.953 370.227H862.574V442.881H808.953L808.953 443.881H862.574V516.535H808.953L808.953 517.535H862.574V573.404H863.574V517.535H936.23V573.404H937.23V517.535H1009.88V573.404H1010.88V517.535H1083.53V573.404H1084.53V517.535L1157.19 517.535V573.404H1158.19V517.535H1230.85V573.404H1231.85V517.535H1304.5V573.404H1305.5V517.535H1378.15V573.404H1379.15V517.535H1437.27V516.535H1379.15V443.881H1437.27V442.881H1379.15V370.227H1437.27V369.227H1379.15V296.572H1437.27V295.572H1379.15V222.918H1437.27V221.918H1379.15V149.264H1437.27V148.264H1379.15V75.6094H1437.27V74.6094H1379.15V1.95509L1437.27 1.95509V0.95509L1379.15 0.955086V-54.9121H1378.15V0.955086H1305.5V-54.9121H1304.5V0.955086L1231.85 0.955082V-54.9121H1230.85V0.955082L1158.19 0.955078V-54.9121H1157.19V0.955078L1084.53 0.955074V-54.9121H1083.53V0.955074L1010.88 0.95507V-54.9121H1009.88V0.95507L937.23 0.955067V-54.9121H936.23V0.955067H863.574V-54.9121H862.574ZM1378.15 516.535V443.881H1305.5V516.535H1378.15ZM1304.5 516.535V443.881H1231.85V516.535H1304.5ZM1230.85 516.535V443.881H1158.19V516.535H1230.85ZM1157.19 516.535V443.881H1084.53V516.535L1157.19 516.535ZM1083.53 516.535V443.881H1010.88V516.535H1083.53ZM1009.88 516.535V443.881H937.23V516.535H1009.88ZM936.23 516.535V443.881H863.574V516.535H936.23ZM936.23 442.881H863.574V370.227H936.23V442.881ZM1009.88 442.881H937.23V370.227H1009.88V442.881ZM1083.53 442.881H1010.88V370.227H1083.53V442.881ZM1157.19 442.881H1084.53V370.227H1157.19V442.881ZM1230.85 442.881H1158.19V370.227H1230.85V442.881ZM1304.5 442.881H1231.85V370.227H1304.5V442.881ZM1378.15 442.881H1305.5V370.227H1378.15V442.881ZM1378.15 369.227V296.572H1305.5V369.227H1378.15ZM1304.5 369.227V296.572H1231.85V369.227H1304.5ZM1230.85 369.227V296.572H1158.19V369.227H1230.85ZM1157.19 369.227V296.572H1084.53V369.227H1157.19ZM1083.53 369.227V296.572H1010.88V369.227H1083.53ZM1009.88 369.227V296.572H937.23V369.227H1009.88ZM936.23 369.227V296.572H863.574V369.227H936.23ZM936.23 295.572H863.574V222.918H936.23V295.572ZM1009.88 295.572H937.23V222.918H1009.88V295.572ZM1083.53 295.572H1010.88V222.918H1083.53V295.572ZM1157.19 295.572H1084.53V222.918H1157.19V295.572ZM1230.85 295.572H1158.19V222.918H1230.85V295.572ZM1304.5 295.572H1231.85V222.918H1304.5V295.572ZM1378.15 295.572H1305.5V222.918H1378.15V295.572ZM1378.15 221.918V149.264H1305.5V221.918H1378.15ZM1304.5 221.918V149.264L1231.85 149.264V221.918H1304.5ZM1230.85 221.918V149.264H1158.19V221.918H1230.85ZM1157.19 221.918V149.264H1084.53V221.918H1157.19ZM1083.53 221.918V149.264H1010.88V221.918H1083.53ZM1009.88 221.918V149.264L937.23 149.264V221.918H1009.88ZM936.23 221.918V149.264H863.574V221.918H936.23ZM936.23 148.264H863.574V75.6094H936.23V148.264ZM1009.88 148.264L937.23 148.264V75.6094L1009.88 75.6094V148.264ZM1083.53 148.264H1010.88V75.6094H1083.53V148.264ZM1157.19 148.264H1084.53V75.6094H1157.19V148.264ZM1230.85 148.264H1158.19V75.6094H1230.85V148.264ZM1304.5 148.264L1231.85 148.264V75.6094L1304.5 75.6094V148.264ZM1378.15 148.264H1305.5V75.6094H1378.15V148.264ZM1378.15 74.6094V1.95509H1305.5V74.6094H1378.15ZM1304.5 74.6094V1.95509L1231.85 1.95508V74.6094L1304.5 74.6094ZM1230.85 74.6094V1.95508L1158.19 1.95508V74.6094H1230.85ZM1157.19 74.6094V1.95508L1084.53 1.95507V74.6094H1157.19ZM1083.53 74.6094V1.95507L1010.88 1.95507V74.6094H1083.53ZM1009.88 74.6094V1.95507L937.23 1.95507V74.6094L1009.88 74.6094ZM936.23 74.6094V1.95507H863.574V74.6094H936.23Z" fill="url(#paint0_radial_340_660)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M492.574 251.088V306.955L438.953 306.955L438.953 307.955L492.574 307.955V380.609H438.953L438.953 381.609H492.574V454.264H438.953L438.953 455.264H492.574V527.918H438.953L438.953 528.918H492.574V601.572H438.953L438.953 602.572H492.574V675.227H438.953L438.953 676.227H492.574V748.881H438.953L438.953 749.881H492.574V822.535H438.953L438.953 823.535H492.574V879.404H493.574V823.535H566.23V879.404H567.23V823.535H639.878V879.404H640.878V823.535H713.535V879.404H714.535V823.535L787.191 823.535V879.404H788.191V823.535H860.847V879.404H861.847V823.535H934.496V879.404H935.496V823.535H1008.15V879.404H1009.15V823.535H1067.27V822.535H1009.15V749.881H1067.27V748.881H1009.15V676.227H1067.27V675.227H1009.15V602.572H1067.27V601.572H1009.15V528.918H1067.27V527.918H1009.15V455.264H1067.27V454.264H1009.15V381.609H1067.27V380.609H1009.15V307.955L1067.27 307.955V306.955L1009.15 306.955V251.088H1008.15V306.955H935.496V251.088H934.496V306.955L861.847 306.955V251.088H860.847V306.955L788.191 306.955V251.088H787.191V306.955L714.535 306.955V251.088H713.535V306.955L640.878 306.955V251.088H639.878V306.955L567.23 306.955V251.088H566.23V306.955H493.574V251.088H492.574ZM1008.15 822.535V749.881H935.496V822.535H1008.15ZM934.496 822.535V749.881H861.847V822.535H934.496ZM860.847 822.535V749.881H788.191V822.535H860.847ZM787.191 822.535V749.881H714.535V822.535L787.191 822.535ZM713.535 822.535V749.881H640.878V822.535H713.535ZM639.878 822.535V749.881H567.23V822.535H639.878ZM566.23 822.535V749.881H493.574V822.535H566.23ZM566.23 748.881H493.574V676.227H566.23V748.881ZM639.878 748.881H567.23V676.227H639.878V748.881ZM713.535 748.881H640.878V676.227H713.535V748.881ZM787.191 748.881H714.535V676.227H787.191V748.881ZM860.847 748.881H788.191V676.227H860.847V748.881ZM934.496 748.881H861.847V676.227H934.496V748.881ZM1008.15 748.881H935.496V676.227H1008.15V748.881ZM1008.15 675.227V602.572H935.496V675.227H1008.15ZM934.496 675.227V602.572H861.847V675.227H934.496ZM860.847 675.227V602.572H788.191V675.227H860.847ZM787.191 675.227V602.572H714.535V675.227H787.191ZM713.535 675.227V602.572H640.878V675.227H713.535ZM639.878 675.227V602.572H567.23V675.227H639.878ZM566.23 675.227V602.572H493.574V675.227H566.23ZM566.23 601.572H493.574V528.918H566.23V601.572ZM639.878 601.572H567.23V528.918H639.878V601.572ZM713.535 601.572H640.878V528.918H713.535V601.572ZM787.191 601.572H714.535V528.918H787.191V601.572ZM860.847 601.572H788.191V528.918H860.847V601.572ZM934.496 601.572H861.847V528.918H934.496V601.572ZM1008.15 601.572H935.496V528.918H1008.15V601.572ZM1008.15 527.918V455.264H935.496V527.918H1008.15ZM934.496 527.918V455.264L861.847 455.264V527.918H934.496ZM860.847 527.918V455.264H788.191V527.918H860.847ZM787.191 527.918V455.264H714.535V527.918H787.191ZM713.535 527.918V455.264H640.878V527.918H713.535ZM639.878 527.918V455.264L567.23 455.264V527.918H639.878ZM566.23 527.918V455.264H493.574V527.918H566.23ZM566.23 454.264H493.574V381.609H566.23V454.264ZM639.878 454.264L567.23 454.264V381.609L639.878 381.609V454.264ZM713.535 454.264H640.878V381.609H713.535V454.264ZM787.191 454.264H714.535V381.609H787.191V454.264ZM860.847 454.264H788.191V381.609H860.847V454.264ZM934.496 454.264L861.847 454.264V381.609L934.496 381.609V454.264ZM1008.15 454.264H935.496V381.609H1008.15V454.264ZM1008.15 380.609V307.955H935.496V380.609H1008.15ZM934.496 380.609V307.955L861.847 307.955V380.609L934.496 380.609ZM860.847 380.609V307.955L788.191 307.955V380.609H860.847ZM787.191 380.609V307.955L714.535 307.955V380.609H787.191ZM713.535 380.609V307.955L640.878 307.955V380.609H713.535ZM639.878 380.609V307.955L567.23 307.955V380.609L639.878 380.609ZM566.23 380.609V307.955H493.574V380.609H566.23Z" fill="url(#paint1_radial_340_660)"/>
|
||||
<g opacity="0.4" filter="url(#filter0_f_340_660)">
|
||||
<ellipse cx="1097.67" cy="90.2204" rx="278.006" ry="262.262" transform="rotate(-157.304 1097.67 90.2204)" fill="url(#paint2_linear_340_660)" fill-opacity="0.23"/>
|
||||
</g>
|
||||
<g opacity="0.4" filter="url(#filter1_f_340_660)">
|
||||
<ellipse cx="645.778" cy="-170.165" rx="362.917" ry="211.851" transform="rotate(46.3553 645.778 -170.165)" fill="#8244FF"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_340_660" x="627.875" y="-368.511" width="939.586" height="917.463" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="97" result="effect1_foregroundBlur_340_660"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_340_660" x="48.0703" y="-774.799" width="1195.42" height="1209.27" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="152" result="effect1_foregroundBlur_340_660"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_340_660" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(1123.11 259.246) rotate(90) scale(238.243)">
|
||||
<stop stop-color="#8244FF" stop-opacity="0.29"/>
|
||||
<stop offset="1" stop-color="#8244FF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_340_660" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(753.111 565.246) rotate(90) scale(238.243)">
|
||||
<stop stop-color="#8244FF" stop-opacity="0.29"/>
|
||||
<stop offset="1" stop-color="#8244FF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint2_linear_340_660" x1="1356.37" y1="-139.487" x2="1240.56" y2="384.088" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F926AE"/>
|
||||
<stop offset="0.21875" stop-color="#F926AE"/>
|
||||
<stop offset="1" stop-color="#F926AE" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.6 KiB |
10
src/static/img/logo.svg
Normal file
10
src/static/img/logo.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -64 640 640" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#9333EA"/>
|
||||
<stop offset="80%" style="stop-color:#DB2777"/>
|
||||
<stop offset="100%" style="stop-color:#2563EB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#logoGradient)" d="M312 8C175 8 64 119 64 256s111 248 248 248 248-111 248-248c0-25.38-3.82-49.86-10.9-72.91l63.92 52.97L640 173.49l-98.2-10.89c-.65-1.6-1.31-3.19-2-4.77l94.89-40.43L595 61.64l-60.41 84.91C494.17 64.46 409.7 8 312 8zM191.99 256c0-9.3 4.1-17.5 10.5-23.4l-31-9.3c-8.5-2.5-13.3-11.5-10.7-19.9 2.5-8.5 11.4-13.2 19.9-10.7l80 24c8.5 2.5 13.3 11.5 10.7 19.9-2.1 6.9-8.4 11.4-15.3 11.4-.5 0-1.1-.2-1.7-.2.7 2.7 1.7 5.3 1.7 8.2 0 17.7-14.3 32-32 32s-32.1-14.3-32.1-32zm252.61-32.7l-31 9.3c6.3 5.8 10.5 14.1 10.5 23.4 0 17.7-14.3 32-32 32s-32-14.3-32-32c0-2.9.9-5.6 1.7-8.2-.6.1-1.1.2-1.7.2-6.9 0-13.2-4.5-15.3-11.4-2.5-8.5 2.3-17.4 10.7-19.9l80-24c8.4-2.5 17.4 2.3 19.9 10.7 2.5 8.5-2.3 17.4-10.8 19.9zm-265.19-52.28h262.56c42.35 0 54.97 49.74 53.8 83.99-1.18 34.83-41.79 72.53-72.23 72.53-61.58 0-73.62-40.25-112.85-40.28-39.23.03-51.27 40.28-112.85 40.28-30.44 0-71.05-37.7-72.23-72.53-1.17-34.25 11.45-83.99 53.8-83.99z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
130
src/templates/dashboard.html
Normal file
130
src/templates/dashboard.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ltsNinja - Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||
<link rel="icon" href="/static/img/logo.svg">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
'rainbow': '0 0 10px rgba(255, 0, 0, 0.2), 0 0 20px rgba(255, 165, 0, 0.2), 0 0 30px rgba(255, 255, 0, 0.2), 0 0 40px rgba(0, 255, 0, 0.2), 0 0 50px rgba(0, 0, 255, 0.2), 0 0 60px rgba(75, 0, 130, 0.2), 0 0 70px rgba(238, 130, 238, 0.2)',
|
||||
'color': '0 0 15px rgba(99, 102, 241, 0.4)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen flex flex-col items-center justify-center p-4 bg-[url('/static/img/hero.svg')] bg-cover">
|
||||
<img src="/static/img/logo.svg" width="128" height="128" class="mb-4" alt="Links Ninja">
|
||||
|
||||
<div class="bg-white/0 ring-1 ring-black/5 rounded-3xl p-8 w-full max-w-4xl backdrop-blur-md">
|
||||
<h2 class="text-3xl font-bold mb-6 text-center text-gray-800">👋 Welcome on your dashboard</h2>
|
||||
<div id="error" class="hidden p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||
<span class="font-medium">⚠️ An error occured!</span>
|
||||
<span id="error-content"></span>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl p-6 shadow mb-6 overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="bg-gray-100">
|
||||
<th class="px-4 py-2 text-left">Original URL</th>
|
||||
<th class="px-4 py-2 text-left">Short URL</th>
|
||||
<th class="px-4 py-2 text-left">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .links}}
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">{{.OriginalURL}}</td>
|
||||
<td class="px-4 py-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="text" id="new-name-{{.ID}}" value="{{.ShortURL}}" placeholder="Short URL" class="px-2 py-1 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-300">
|
||||
<button onclick="updateLink('{{.ID}}')" class="px-3 py-1 bg-gradient-to-r from-green-400 to-blue-400 text-white rounded-full hover:from-green-500 hover:to-blue-500 focus:outline-none focus:ring-2 focus:ring-green-300 transition duration-300 ease-in-out transform hover:scale-105">
|
||||
<i class="fa fa-pen"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td class="px py-2">
|
||||
<button onclick="deleteLink('{{.ID}}')" class="px-3 py-1 bg-gradient-to-r from-red-400 to-pink-400 text-white rounded-full hover:from-red-500 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-red-300 transition duration-300 ease-in-out transform hover:scale-105">
|
||||
<i class="fa fa-trash"></i> Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mb-4">
|
||||
<small>Account ID: {{.userId }}</small>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/" class="px-6 py-2 bg-gradient-to-r from-purple-400 to-indigo-400 text-white rounded-full hover:from-purple-500 hover:to-indigo-500 focus:outline-none focus:ring-2 focus:ring-purple-300 transition duration-300 ease-in-out transform hover:scale-105">
|
||||
Return to home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="w-full text-center py-4 mt-8">
|
||||
<p class="text-sm text-gray-600">
|
||||
© 2024 ltsNinja. Made with ❤️ -
|
||||
<a href="https://github.com/itsmrval/ltsninja" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-700 transition duration-300">
|
||||
Source code
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const errorBox = document.getElementById('error');
|
||||
const errorContent = document.getElementById('error-content');
|
||||
const resultBox = document.getElementById('result');
|
||||
const resultLink = document.getElementById('result-link');
|
||||
|
||||
function deleteLink(id) {
|
||||
fetch('/dashboard', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ id: id }),
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
errorContent.textContent = 'Failed to delete the link.';
|
||||
errorBox.classList.remove('hidden');
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateLink(id) {
|
||||
const newName = document.getElementById(`new-name-${id}`).value;
|
||||
fetch('/dashboard', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ id: id, new_name: newName }),
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
errorContent.textContent = 'Failed to update the link.';
|
||||
errorBox.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
124
src/templates/index.html
Normal file
124
src/templates/index.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ltsNinja - Raccourcisseur d'URL</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||
<link rel="icon" href="/static/img/logo.svg">
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
'rainbow': '0 0 10px rgba(255, 0, 0, 0.2), 0 0 20px rgba(255, 165, 0, 0.2), 0 0 30px rgba(255, 255, 0, 0.2), 0 0 40px rgba(0, 255, 0, 0.2), 0 0 50px rgba(0, 0, 255, 0.2), 0 0 60px rgba(75, 0, 130, 0.2), 0 0 70px rgba(238, 130, 238, 0.2)',
|
||||
'color': '0 0 15px rgba(99, 102, 241, 0.4)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen flex flex-col items-center justify-center p-4 bg-[url('/static/img/hero.svg')] bg-cover">
|
||||
<img src="/static/img/logo.svg" width="128" height="128" class="mb-4" alt="Links Ninja">
|
||||
|
||||
|
||||
<div class="bg-white/0 ring-1 ring-black/5 rounded-3xl p-8 w-full max-w-2xl backdrop-blur-md">
|
||||
<div class="bg-white rounded-2xl p-6 shadow mb-6">
|
||||
<form id="shorten-form" class="space-y-4">
|
||||
<input type="url" id="url" name="url" placeholder="📋 Paste here your long link" required
|
||||
class="w-full px-4 py-2 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-300">
|
||||
|
||||
<div class="flex space-x-4">
|
||||
<input type="text" id="custom-name" name="custom_name" placeholder="{{if .loggedIn}} my-custom-name {{else}} Login to enable custom names ✨ {{end}}" {{if not .loggedIn}}disabled{{end}}
|
||||
class="flex-grow px-4 py-2 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-300" >
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-gradient-to-r from-pink-400 to-red-400 text-white rounded-full shadow hover:from-pink-500 hover:to-red-500 focus:outline-none focus:ring-2 focus:ring-red-300 transition duration-300 ease-in-out transform hover:scale-105">
|
||||
Generate 📬
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="result" class="hidden p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400" role="alert">
|
||||
<span class="font-medium">📨 URL Generated!</span>
|
||||
<span id="result-link"></span>
|
||||
<button id="copy-btn" class="ml-2 px-2 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-300">Copy</button>
|
||||
</div>
|
||||
<div id="error" class="hidden p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||
<span class="font-medium">⚠️ An error occured!</span>
|
||||
<span id="error-content"></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-between items-center">
|
||||
{{if .loggedIn}}
|
||||
<a href="/dashboard"
|
||||
class="px-6 py-2 bg-blue-400 text-white rounded-full focus:outline-none focus:ring-2 focus:ring-green-300 transition duration-300 ease-in-out transform hover:scale-105">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/logout" class="px-6 py-2 bg-purple-400 text-white rounded-full focus:outline-none focus:ring-2 focus:ring-purple-300 transition duration-300 ease-in-out transform hover:scale-105">
|
||||
Logout
|
||||
</a>
|
||||
{{else}}
|
||||
<a href="/login"
|
||||
class="px-6 py-2 bg-blue-400 text-white rounded-full focus:outline-none focus:ring-2 focus:ring-blue-300 transition duration-300 ease-in-out transform hover:scale-105">
|
||||
<i class="fab fa-github"></i> Login using GitHub
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="w-full text-center py-4 mt-8">
|
||||
<p class="text-sm text-gray-600">
|
||||
© 2024 ltsNinja. Made with ❤️ -
|
||||
<a href="https://github.com/itsmrval/ltsninja" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-700 transition duration-300">
|
||||
Source code
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const errorBox = document.getElementById('error');
|
||||
const errorContent = document.getElementById('error-content');
|
||||
const resultBox = document.getElementById('result');
|
||||
const resultLink = document.getElementById('result-link');
|
||||
document.getElementById('shorten-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
fetch('/', {
|
||||
method: 'POST',
|
||||
body: new FormData(this)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.reset();
|
||||
errorBox.classList.add('hidden');
|
||||
resultBox.classList.add('hidden');
|
||||
if (data.error) {
|
||||
errorContent.textContent = data.error;
|
||||
errorBox.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const shortURL = window.location.origin + '/' + data.shortURL;
|
||||
resultLink.textContent = shortURL;
|
||||
resultLink.setAttribute('data-url', shortURL);
|
||||
resultBox.classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('copy-btn').addEventListener('click', function() {
|
||||
const resultLink = document.getElementById('result-link');
|
||||
const url = resultLink.getAttribute('data-url');
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
this.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
this.textContent = 'Copy';
|
||||
}, 1000);
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
67
src/utils.go
Normal file
67
src/utils.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user