Skip to content

Commit 055ac9e

Browse files
Ian MaddoxIan Maddox
authored andcommitted
Implementing go/.mine which shows only the links created by the viewer
Similar to Google's original golink implementation, this adds a page where a user can see just the links they've created.
1 parent 701eb19 commit 055ac9e

File tree

7 files changed

+187
-0
lines changed

7 files changed

+187
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,7 @@ enabled over its HTTP interface you _must_ specify the `-L` flag to follow these
253253
redirects or else your request will terminate early with an empty response. We
254254
recommend the use of the `-L` flag in all deployments regardless of current
255255
HTTPS status to avoid accidental outages should it be enabled in the future.
256+
257+
## My Links
258+
259+
Navigate to `http://go/.mine` to view all the links you have created. This page filters the links to display only those owned by your account, providing a personalized view of your shortlinks.

db.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,31 @@ func (s *SQLiteDB) LoadAll() ([]*Link, error) {
9090
return links, rows.Err()
9191
}
9292

93+
// LoadByOwner retrieves all links owned by the specified user.
94+
func (s *SQLiteDB) LoadByOwner(owner string) ([]*Link, error) {
95+
s.mu.RLock()
96+
defer s.mu.RUnlock()
97+
98+
var links []*Link
99+
rows, err := s.db.Query("SELECT Short, Long, Created, LastEdit, Owner FROM Links WHERE Owner = ?", owner)
100+
if err != nil {
101+
return nil, err
102+
}
103+
for rows.Next() {
104+
link := new(Link)
105+
var created, lastEdit int64
106+
err := rows.Scan(&link.Short, &link.Long, &created, &lastEdit, &link.Owner)
107+
if err != nil {
108+
return nil, err
109+
}
110+
link.Created = time.Unix(created, 0).UTC()
111+
link.LastEdit = time.Unix(lastEdit, 0).UTC()
112+
links = append(links, link)
113+
}
114+
return links, rows.Err()
115+
}
116+
117+
93118
// Load returns a Link by its short name.
94119
//
95120
// It returns fs.ErrNotExist if the link does not exist.
@@ -217,3 +242,5 @@ func (s *SQLiteDB) DeleteStats(short string) error {
217242
}
218243
return nil
219244
}
245+
246+

golink.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ var (
260260

261261
// opensearchTmpl is the template used by the http://go/.opensearch page
262262
opensearchTmpl *template.Template
263+
264+
// mineTmpl is the template used by the http://go/.mine page
265+
mineTmpl *template.Template
263266
)
264267

265268
type visitData struct {
@@ -292,6 +295,7 @@ func init() {
292295
allTmpl = newTemplate("base.html", "all.html")
293296
deleteTmpl = newTemplate("base.html", "delete.html")
294297
opensearchTmpl = newTemplate("opensearch.xml")
298+
mineTmpl = newTemplate("base.html", "mine.html")
295299

296300
b := make([]byte, 24)
297301
rand.Read(b)
@@ -496,6 +500,29 @@ func serveAll(w http.ResponseWriter, _ *http.Request) {
496500
allTmpl.Execute(w, links)
497501
}
498502

503+
func serveMine(w http.ResponseWriter, r *http.Request) {
504+
cu, err := currentUser(r)
505+
if err != nil {
506+
http.Error(w, "Failed to retrieve current user", http.StatusInternalServerError)
507+
return
508+
}
509+
510+
// Flush stats before loading links
511+
if err := flushStats(); err != nil {
512+
http.Error(w, err.Error(), http.StatusInternalServerError)
513+
return
514+
}
515+
516+
// Load links owned by the current user
517+
links, err := db.LoadByOwner(cu.login)
518+
if err != nil {
519+
http.Error(w, err.Error(), http.StatusInternalServerError)
520+
return
521+
}
522+
523+
mineTmpl.Execute(w, links)
524+
}
525+
499526
func serveHelp(w http.ResponseWriter, _ *http.Request) {
500527
helpTmpl.Execute(w, nil)
501528
}
@@ -516,6 +543,16 @@ func serveGo(w http.ResponseWriter, r *http.Request) {
516543
return
517544
}
518545

546+
// Route the user-specific link list /.mine endpoint
547+
if r.URL.Path == "/.mine" {
548+
if r.Method != "GET" {
549+
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
550+
return
551+
}
552+
serveMine(w, r)
553+
return
554+
}
555+
519556
short, remainder, _ := strings.Cut(strings.TrimPrefix(r.URL.Path, "/"), "/")
520557

521558
// redirect {name}+ links to /.detail/{name}
@@ -1020,3 +1057,4 @@ func isRequestAuthorized(r *http.Request, u user, short string) bool {
10201057

10211058
return xsrftoken.Valid(r.PostFormValue("xsrf"), xsrfKey, u.login, short)
10221059
}
1060+

golink_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package golink
55

66
import (
7+
"encoding/json"
78
"errors"
89
"net/http"
910
"net/http/httptest"
@@ -612,3 +613,86 @@ func TestNoHSTSShortDomain(t *testing.T) {
612613
})
613614
}
614615
}
616+
617+
func TestServeMine(t *testing.T) {
618+
var err error
619+
db, err = NewSQLiteDB(":memory:")
620+
if err != nil {
621+
t.Fatal(err)
622+
}
623+
// Seed the database with links
624+
db.Save(&Link{Short: "link1", Long: "http://example.com/1", Owner: "user1"})
625+
db.Save(&Link{Short: "link2", Long: "http://example.com/2", Owner: "user2"})
626+
db.Save(&Link{Short: "link3", Long: "http://example.com/3", Owner: "user1"})
627+
628+
tests := []struct {
629+
name string
630+
currentUser func(*http.Request) (user, error)
631+
wantLinks []*Link
632+
wantStatus int
633+
}{
634+
{
635+
name: "User with links",
636+
currentUser: func(*http.Request) (user, error) {
637+
return user{login: "[email protected]"}, nil
638+
},
639+
wantLinks: []*Link{
640+
{Short: "link1", Long: "http://example.com/1", Owner: "user1"},
641+
{Short: "link3", Long: "http://example.com/3", Owner: "user1"},
642+
},
643+
wantStatus: http.StatusOK,
644+
},
645+
{
646+
name: "User with no links",
647+
currentUser: func(*http.Request) (user, error) {
648+
return user{login: "[email protected]"}, nil
649+
},
650+
wantLinks: []*Link{},
651+
wantStatus: http.StatusOK,
652+
},
653+
{
654+
name: "Failed to retrieve user",
655+
currentUser: func(*http.Request) (user, error) {
656+
return user{}, errors.New("authentication failed")
657+
},
658+
wantLinks: nil,
659+
wantStatus: http.StatusInternalServerError,
660+
},
661+
}
662+
663+
for _, tt := range tests {
664+
t.Run(tt.name, func(t *testing.T) {
665+
if tt.currentUser != nil {
666+
oldCurrentUser := currentUser
667+
currentUser = tt.currentUser
668+
t.Cleanup(func() {
669+
currentUser = oldCurrentUser
670+
})
671+
}
672+
673+
r := httptest.NewRequest("GET", "/.mine", nil)
674+
w := httptest.NewRecorder()
675+
serveMine(w, r)
676+
677+
if w.Code != tt.wantStatus {
678+
t.Errorf("serveMine() = %d; want %d", w.Code, tt.wantStatus)
679+
}
680+
681+
if tt.wantStatus == http.StatusOK {
682+
var gotLinks []*Link
683+
err := json.NewDecoder(w.Body).Decode(&gotLinks)
684+
if err != nil {
685+
t.Fatalf("Failed to decode response: %v", err)
686+
}
687+
if len(gotLinks) != len(tt.wantLinks) {
688+
t.Errorf("Number of links = %d; want %d", len(gotLinks), len(tt.wantLinks))
689+
}
690+
for i, link := range gotLinks {
691+
if link.Short != tt.wantLinks[i].Short || link.Owner != tt.wantLinks[i].Owner {
692+
t.Errorf("Link %d = %+v; want %+v", i, link, tt.wantLinks[i])
693+
}
694+
}
695+
}
696+
})
697+
}
698+
}

schema.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ CREATE TABLE IF NOT EXISTS Links (
77
Owner TEXT NOT NULL DEFAULT ""
88
);
99

10+
CREATE INDEX IF NOT EXISTS idx_owner ON Links (Owner);
11+
1012
CREATE TABLE IF NOT EXISTS Stats (
1113
ID TEXT NOT NULL DEFAULT "",
1214
Created INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), -- unix seconds

tmpl/home.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ <h2 class="text-xl font-bold pt-6 pb-2">Popular Links</h2>
4040
</tbody>
4141
</table>
4242
<p class="my-2 text-sm"><a class="text-blue-600 hover:underline" href="/.all">See all links.</a></p>
43+
<p class="my-2 text-sm"><a class="text-blue-600 hover:underline" href="/.mine">See all your links.</a></p>
4344
{{ end }}

tmpl/mine.html

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{{ define "main" }}
2+
<h2 class="text-xl font-bold pt-6 pb-2">My Links ({{ len . }} total)</h2>
3+
<table class="table-auto w-full max-w-screen-lg">
4+
<thead class="border-b border-gray-200 uppercase text-xs text-gray-500 text-left">
5+
<tr class="flex">
6+
<th class="flex-1 p-2">Link</th>
7+
<th class="hidden md:block w-60 truncate p-2">Owner</th>
8+
<th class="hidden md:block w-32 p-2">Last Edited</th>
9+
</tr>
10+
</thead>
11+
<tbody>
12+
{{ range . }}
13+
<tr class="flex hover:bg-gray-100 group border-b border-gray-200">
14+
<td class="flex-1 p-2">
15+
<div class="flex">
16+
<a class="flex-1 hover:text-blue-500 hover:underline" href="/{{ .Short }}">{{go}}/{{ .Short }}</a>
17+
<a class="flex items-center px-2 invisible group-hover:visible" title="Link Details" href="/.detail/{{ .Short }}">
18+
<svg class="hover:fill-blue-500" xmlns="http://www.w3.org/2000/svg" height="1.3em" viewBox="0 0 24 24" width="1.3em" fill="#000000" stroke-width="2"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
19+
</a>
20+
</div>
21+
<p class="text-sm leading-normal text-gray-500 group-hover:text-gray-700 max-w-[75vw] md:max-w-[40vw] truncate">{{ .Long }}</p>
22+
<p class="md:hidden text-sm leading-normal text-gray-700"><span class="text-gray-500 inline-block w-20">Owner</span> {{ .Owner }}</p>
23+
<p class="md:hidden text-sm leading-normal text-gray-700"><span class="text-gray-500 inline-block w-20">Last Edited</span> {{ .LastEdit.Format "Jan 2, 2006" }}</p>
24+
</td>
25+
<td class="hidden md:block w-60 truncate p-2">{{ .Owner }}</td>
26+
<td class="hidden md:block w-32 p-2">{{ .LastEdit.Format "Jan 2, 2006" }}</td>
27+
</tr>
28+
{{ end }}
29+
</tbody>
30+
</table>
31+
{{ end }}

0 commit comments

Comments
 (0)