Skip to content

Commit

Permalink
added favicon for sites, list all sites api and page, disabled cors f…
Browse files Browse the repository at this point in the history
…or api, minor fixes
  • Loading branch information
Alexander-D-Karpov committed Jul 16, 2024
1 parent 14d686e commit d22425e
Show file tree
Hide file tree
Showing 15 changed files with 397 additions and 32 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.env
*.log
.idea
media/
21 changes: 20 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"path/filepath"
"webring"
"webring/internal/public"

"webring/internal/api"
"webring/internal/dashboard"
Expand Down Expand Up @@ -91,14 +92,32 @@ func main() {
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(staticFiles))))

// Parse templates
t, err := template.ParseFS(webring.Files, "internal/dashboard/templates/*.html")
t, err := template.ParseFS(webring.Files, "internal/dashboard/templates/*.html", "internal/public/templates/*.html")
if err != nil {
log.Fatalf("Error parsing templates: %v", err)
}

// Initialize dashboard templates
dashboard.InitTemplates(t)

// Initialize public templates
public.InitTemplates(t)

mediaFolder := os.Getenv("MEDIA_FOLDER")
if mediaFolder == "" {
mediaFolder = "media"
}
err = os.MkdirAll(mediaFolder, os.ModePerm)
if err != nil {
return
}

// Serve media files
r.PathPrefix("/media/").Handler(http.StripPrefix("/media/", http.FileServer(http.Dir(mediaFolder))))

// Register public handlers
public.RegisterHandlers(r, db)

port := os.Getenv("PORT")
if port == "" {
fmt.Println("PORT environment variable not set. Defaulting to 8080")
Expand Down
2 changes: 1 addition & 1 deletion embedded.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import (
"embed"
)

//go:embed static internal/dashboard/templates
//go:embed static internal/dashboard/templates internal/public/templates
var Files embed.FS
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ module webring
go 1.22.4

require (
github.com/PuerkitoBio/goquery v1.9.2
github.com/gorilla/mux v1.8.1
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
)

require (
github.com/andybalholm/cascadia v1.3.2 // indirect
golang.org/x/net v0.24.0 // indirect
)
40 changes: 40 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
97 changes: 74 additions & 23 deletions internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@ import (
"fmt"
"log"
"net/http"

"webring/internal/api/middleware"
"webring/internal/models"

"github.com/gorilla/mux"
)

func RegisterHandlers(r *mux.Router, db *sql.DB) {
r.HandleFunc("/{id}/prev/", previousSiteHandler(db)).Methods("GET")
r.HandleFunc("/{id}/next/", nextSiteHandler(db)).Methods("GET")
r.HandleFunc("/{id}/prev", previousSiteRedirectHandler(db)).Methods("GET")
r.HandleFunc("/{id}/next", nextSiteRedirectHandler(db)).Methods("GET")
r.HandleFunc("/{id}/data", siteDataHandler(db)).Methods("GET")
r.HandleFunc("/{id}/random/", randomSiteHandler(db)).Methods("GET")
r.HandleFunc("/{id}/random", randomSiteRedirectHandler(db)).Methods("GET")
apiRouter := r.PathPrefix("").Subrouter()
apiRouter.Use(middleware.CORSMiddleware)

apiRouter.HandleFunc("/{id}/prev/", previousSiteHandler(db)).Methods("GET")
apiRouter.HandleFunc("/{id}/next/", nextSiteHandler(db)).Methods("GET")
apiRouter.HandleFunc("/{id}/prev", previousSiteRedirectHandler(db)).Methods("GET")
apiRouter.HandleFunc("/{id}/next", nextSiteRedirectHandler(db)).Methods("GET")
apiRouter.HandleFunc("/{id}/data", siteDataHandler(db)).Methods("GET")
apiRouter.HandleFunc("/{id}/random/", randomSiteHandler(db)).Methods("GET")
apiRouter.HandleFunc("/{id}/random", randomSiteRedirectHandler(db)).Methods("GET")
apiRouter.HandleFunc("/sites", listPublicSitesHandler(db)).Methods("GET")
}

func previousSiteHandler(db *sql.DB) http.HandlerFunc {
Expand Down Expand Up @@ -159,17 +163,57 @@ func randomSiteRedirectHandler(db *sql.DB) http.HandlerFunc {
}
}

func listPublicSitesHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sites, err := getRespondingSites(db)
if err != nil {
http.Error(w, "Error fetching sites", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(sites)
if err != nil {
http.Error(w, "Error encoding response", http.StatusInternalServerError)
return
}
}
}

func getRespondingSites(db *sql.DB) ([]models.PublicSite, error) {
rows, err := db.Query("SELECT id, name, url, favicon FROM sites WHERE is_up = true ORDER BY id")
if err != nil {
return nil, err
}
defer func(rows *sql.Rows) {
err := rows.Close()
if err != nil {
log.Printf("Error closing rows: %v", err)
}
}(rows)

var sites []models.PublicSite
for rows.Next() {
var site models.PublicSite
if err := rows.Scan(&site.ID, &site.Name, &site.URL, &site.Favicon); err != nil {
return nil, err
}
sites = append(sites, site)
}
return sites, nil
}

func getNextSite(db *sql.DB, currentID string) (*models.PublicSite, error) {
var site models.PublicSite
err := db.QueryRow(`
WITH ring AS (
SELECT id, name, url, is_up,
SELECT id, name, url, favicon, is_up,
LEAD(id) OVER (ORDER BY id) AS next_id,
LAG(id) OVER (ORDER BY id) AS prev_id
FROM sites
WHERE is_up = true
)
SELECT id, name, url
SELECT id, name, url, favicon
FROM ring
WHERE (id = $1 AND next_id IS NOT NULL AND next_id = (SELECT MIN(id) FROM ring))
OR (id > $1 AND is_up = true)
Expand All @@ -179,7 +223,7 @@ func getNextSite(db *sql.DB, currentID string) (*models.PublicSite, error) {
ELSE (SELECT MAX(id) FROM ring) + 1
END
LIMIT 1
`, currentID).Scan(&site.ID, &site.Name, &site.URL)
`, currentID).Scan(&site.ID, &site.Name, &site.URL, &site.Favicon)
if err != nil {
return nil, err
}
Expand All @@ -190,13 +234,13 @@ func getPreviousSite(db *sql.DB, currentID string) (*models.PublicSite, error) {
var site models.PublicSite
err := db.QueryRow(`
WITH ring AS (
SELECT id, name, url, is_up,
SELECT id, name, url, favicon, is_up,
LEAD(id) OVER (ORDER BY id) AS next_id,
LAG(id) OVER (ORDER BY id) AS prev_id
FROM sites
WHERE is_up = true
)
SELECT id, name, url
SELECT id, name, url, favicon
FROM ring
WHERE (id = $1 AND prev_id IS NOT NULL AND prev_id = (SELECT MAX(id) FROM ring))
OR (id < $1 AND is_up = true)
Expand All @@ -206,7 +250,7 @@ func getPreviousSite(db *sql.DB, currentID string) (*models.PublicSite, error) {
ELSE 0
END DESC
LIMIT 1
`, currentID).Scan(&site.ID, &site.Name, &site.URL)
`, currentID).Scan(&site.ID, &site.Name, &site.URL, &site.Favicon)
if err != nil {
return nil, err
}
Expand All @@ -217,13 +261,15 @@ func getSiteData(db *sql.DB, id string) (*models.SiteData, error) {
var data models.SiteData
err := db.QueryRow(`
WITH ring AS (
SELECT id, name, url, is_up,
SELECT id, name, url, favicon, is_up,
LAG(id) OVER (ORDER BY id) AS prev_id,
LAG(name) OVER (ORDER BY id) AS prev_name,
LAG(url) OVER (ORDER BY id) AS prev_url,
LAG(favicon) OVER (ORDER BY id) AS prev_favicon,
LEAD(id) OVER (ORDER BY id) AS next_id,
LEAD(name) OVER (ORDER BY id) AS next_name,
LEAD(url) OVER (ORDER BY id) AS next_url
LEAD(url) OVER (ORDER BY id) AS next_url,
LEAD(favicon) OVER (ORDER BY id) AS next_favicon
FROM sites
WHERE is_up = true
),
Expand All @@ -232,27 +278,32 @@ func getSiteData(db *sql.DB, id string) (*models.SiteData, error) {
FIRST_VALUE(id) OVER (ORDER BY id) AS first_id,
FIRST_VALUE(name) OVER (ORDER BY id) AS first_name,
FIRST_VALUE(url) OVER (ORDER BY id) AS first_url,
FIRST_VALUE(favicon) OVER (ORDER BY id) AS first_favicon,
LAST_VALUE(id) OVER (ORDER BY id RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_id,
LAST_VALUE(name) OVER (ORDER BY id RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_name,
LAST_VALUE(url) OVER (ORDER BY id RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_url
LAST_VALUE(url) OVER (ORDER BY id RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_url,
LAST_VALUE(favicon) OVER (ORDER BY id RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_favicon
FROM ring
)
SELECT
COALESCE(prev_id, last_id) AS prev_id,
COALESCE(prev_name, last_name) AS prev_name,
COALESCE(prev_url, last_url) AS prev_url,
COALESCE(prev_favicon, last_favicon) AS prev_favicon,
id AS curr_id,
name AS curr_name,
url AS curr_url,
favicon AS curr_favicon,
COALESCE(next_id, first_id) AS next_id,
COALESCE(next_name, first_name) AS next_name,
COALESCE(next_url, first_url) AS next_url
COALESCE(next_url, first_url) AS next_url,
COALESCE(next_favicon, first_favicon) AS next_favicon
FROM wrapped
WHERE id = $1
`, id).Scan(
&data.Prev.ID, &data.Prev.Name, &data.Prev.URL,
&data.Curr.ID, &data.Curr.Name, &data.Curr.URL,
&data.Next.ID, &data.Next.Name, &data.Next.URL,
&data.Prev.ID, &data.Prev.Name, &data.Prev.URL, &data.Prev.Favicon,
&data.Curr.ID, &data.Curr.Name, &data.Curr.URL, &data.Curr.Favicon,
&data.Next.ID, &data.Next.Name, &data.Next.URL, &data.Next.Favicon,
)
if err != nil {
return nil, err
Expand All @@ -263,12 +314,12 @@ func getSiteData(db *sql.DB, id string) (*models.SiteData, error) {
func getRandomSite(db *sql.DB, currentID string) (*models.PublicSite, error) {
var site models.PublicSite
err := db.QueryRow(`
SELECT id, name, url
SELECT id, name, url, favicon
FROM sites
WHERE is_up = true AND id != $1
ORDER BY RANDOM()
LIMIT 1
`, currentID).Scan(&site.ID, &site.Name, &site.URL)
`, currentID).Scan(&site.ID, &site.Name, &site.URL, &site.Favicon)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("no available sites found")
Expand Down
25 changes: 25 additions & 0 deletions internal/api/middleware/cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package middleware

import "net/http"

func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow all origins
w.Header().Set("Access-Control-Allow-Origin", "*")

// Allow common HTTP methods
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")

// Allow common HTTP headers
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")

// Handle preflight requests
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}

// Call the next handler
next.ServeHTTP(w, r)
})
}
Loading

0 comments on commit d22425e

Please sign in to comment.