fcb3de74975de626df4bc6009309f0c21fdda20f
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/.air.toml b/.air.toml
2index 1d3745f0e4a16dc45ab0667ff503cbfe0e1d4d8b..97c50c5d88668fe6ee05d242de2076d62233f38b 100644
3--- a/.air.toml
4+++ b/.air.toml
5@@ -4,6 +4,7 @@ tmp_dir = "/tmp"
6 [build]
7 args_bin = [
8 "--db-uri=postgres://ytrssil:ytrssil@localhost:5432/ytrssil?sslmode=disable",
9+ "--auth-token=foo",
10 ]
11 entrypoint = "./dist/ytrssil"
12 cmd = "make build"
13diff --git a/cmd/main.go b/cmd/main.go
14index 728a32ca960f9c812dfc566e2437f21b3bd89698..ef0a991aa8e25d311446a3006cd5438ebb15f107 100644
15--- a/cmd/main.go
16+++ b/cmd/main.go
17@@ -21,14 +21,12 @@ import (
18 "github.com/TheEdgeOfRage/ytrssil-api/httpserver/ytrssil"
19 )
20
21-const LevelFatal slog.Level = slog.LevelError + 4
22-
23 func init() {
24 // always use UTC
25 time.Local = time.UTC
26 }
27
28-func fetcherRoutine(ctx context.Context, wg *sync.WaitGroup, l *slog.Logger, h handler.Handler) {
29+func fetcherRoutine(ctx context.Context, l *slog.Logger, h handler.Handler) {
30 ticker := time.NewTicker(5 * time.Minute)
31 for {
32 select {
33@@ -47,15 +45,14 @@ func fetcherRoutine(ctx context.Context, wg *sync.WaitGroup, l *slog.Logger, h h
34 func main() {
35 logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
36
37- config, err := config.Parse()
38+ cfg, err := config.Parse()
39 if err != nil {
40- logger.Log(context.Background(), LevelFatal, "Failed to parse config", "call", "config.Parse", "error", err)
41+ logger.Error("Failed to parse config", "call", "config.Parse", "error", err)
42 return
43 }
44- db, err := db.NewPostgresDB(logger, config.DB)
45+ db, err := db.NewPostgresDB(logger, cfg.DBURI)
46 if err != nil {
47- logger.Log(
48- context.Background(), LevelFatal,
49+ logger.Error(
50 "Failed to create DB connection",
51 "call", "db.NewPostgresDB",
52 "error", err,
53@@ -68,11 +65,11 @@ func main() {
54 router, err := ytrssil.SetupGinRouter(
55 logger,
56 handler,
57- auth.AuthMiddleware(db),
58+ auth.APIAuthMiddleware(cfg.AuthToken),
59+ auth.PageAuthMiddleware(cfg.AuthToken),
60 )
61 if err != nil {
62- logger.Log(
63- context.Background(), LevelFatal,
64+ logger.Error(
65 "Failed to set up gin server",
66 "call", "ytrssil.SetupGinServer",
67 "error", err,
68@@ -81,7 +78,7 @@ func main() {
69 }
70
71 server := &http.Server{
72- Addr: fmt.Sprintf(":%v", config.Gin.Port),
73+ Addr: fmt.Sprintf(":%v", cfg.Port),
74 Handler: router,
75 }
76
77@@ -92,11 +89,11 @@ func main() {
78 // start periodic fetch videos routine
79 fetcherContext, cancelFetcher := context.WithCancel(context.Background())
80 wg.Go(func() {
81- fetcherRoutine(fetcherContext, wg, logger, handler)
82+ fetcherRoutine(fetcherContext, logger, handler)
83 })
84
85 wg.Go(func() {
86- logger.Info("ytrssil API is starting up", "port", config.Gin.Port)
87+ logger.Info("ytrssil API is starting up", "port", cfg.Port)
88 if err := server.ListenAndServe(); err != nil {
89 if err != http.ErrServerClosed {
90 logger.Error("Server crashed", "call", "server.ListenAndServe", "error", err)
91diff --git a/cmd/main_test.go b/cmd/main_test.go
92deleted file mode 100644
93index 2d1f25576870a5b0de424928cb1e8f3b3cc558df..0000000000000000000000000000000000000000
94--- a/cmd/main_test.go
95+++ /dev/null
96@@ -1,90 +0,0 @@
97-package main
98-
99-import (
100- "bytes"
101- "context"
102- "encoding/json"
103- "fmt"
104- "io"
105- "log/slog"
106- "net/http"
107- "net/http/httptest"
108- "testing"
109-
110- "github.com/gin-gonic/gin"
111- "github.com/stretchr/testify/assert"
112-
113- "github.com/TheEdgeOfRage/ytrssil-api/config"
114- "github.com/TheEdgeOfRage/ytrssil-api/db"
115- "github.com/TheEdgeOfRage/ytrssil-api/feedparser"
116- "github.com/TheEdgeOfRage/ytrssil-api/handler"
117- "github.com/TheEdgeOfRage/ytrssil-api/httpserver/auth"
118- "github.com/TheEdgeOfRage/ytrssil-api/httpserver/ytrssil"
119- "github.com/TheEdgeOfRage/ytrssil-api/models"
120-)
121-
122-var testConfig config.Config
123-
124-func init() {
125- testConfig = config.TestConfig()
126-}
127-
128-func setupTestServer(t *testing.T) (*http.Server, db.DB) {
129- l := slog.New(slog.NewTextHandler(io.Discard, nil))
130-
131- db, err := db.NewPostgresDB(l, testConfig.DB)
132- if !assert.NoError(t, err) {
133- return nil, nil
134- }
135- parser := feedparser.NewParser(l)
136- handler := handler.New(l, db, parser)
137- gin.SetMode(gin.TestMode)
138- router, err := ytrssil.SetupGinRouter(
139- l,
140- handler,
141- auth.AuthMiddleware(db),
142- )
143- if !assert.NoError(t, err) {
144- return nil, nil
145- }
146-
147- return &http.Server{
148- Addr: fmt.Sprintf(":%v", testConfig.Gin.Port),
149- Handler: router,
150- }, db
151-}
152-
153-func TestHealthz(t *testing.T) {
154- server, _ := setupTestServer(t)
155- if !assert.NotNil(t, server) {
156- return
157- }
158-
159- w := httptest.NewRecorder()
160- req, _ := http.NewRequest("GET", "/healthz", nil)
161- server.Handler.ServeHTTP(w, req)
162-
163- assert.Equal(t, 200, w.Code)
164- assert.Equal(t, "healthy", w.Body.String())
165-}
166-
167-func TestCreateUser(t *testing.T) {
168- server, db := setupTestServer(t)
169- if !assert.NotNil(t, server) {
170- return
171- }
172-
173- jsonData, err := json.Marshal(models.User{Username: "test", Password: "test"})
174- if !assert.Nil(t, err) {
175- return
176- }
177- data := bytes.NewBuffer(jsonData)
178- req, _ := http.NewRequest("POST", "/register", data)
179- w := httptest.NewRecorder()
180- server.Handler.ServeHTTP(w, req)
181-
182- assert.Equal(t, 200, w.Code)
183- assert.Equal(t, `{"msg":"user created"}`, w.Body.String())
184-
185- db.DeleteUser(context.TODO(), "test")
186-}
187diff --git a/config/config.go b/config/config.go
188index d018dd006d4f38606389af27f123459d7fc82f98..9374ea34f7a9a4d225f0ae1c25303f3ec3d57116 100644
189--- a/config/config.go
190+++ b/config/config.go
191@@ -7,19 +7,10 @@ import (
192 flags "github.com/jessevdk/go-flags"
193 )
194
195-type DB struct {
196- DBURI string `long:"db-uri" env:"DB_URI"`
197-}
198-
199-// Gin contains configuration for the gin framework
200-type Gin struct {
201- Port int `long:"port" env:"PORT" default:"8080"`
202-}
203-
204-// Config ties together all configs
205 type Config struct {
206- DB DB
207- Gin Gin
208+ Port int `long:"port" env:"PORT" default:"8080"`
209+ DBURI string `long:"db-uri" env:"DB_URI"`
210+ AuthToken string `long:"auth-token" env:"AUTH_TOKEN"`
211 }
212
213 func getenvOrDefault(key string, defaultValue string) string {
214@@ -46,15 +37,10 @@ func TestConfig() Config {
215 dbURI = dbURI + "?sslmode=disable"
216 }
217
218- gin := Gin{
219- Port: 8080,
220- }
221- db := DB{
222- DBURI: dbURI,
223- }
224 config := Config{
225- Gin: gin,
226- DB: db,
227+ Port: 8080,
228+ DBURI: dbURI,
229+ AuthToken: "foo",
230 }
231
232 return config
233diff --git a/db/channels.go b/db/channels.go
234index d18ce7034ff8097f4db8105a6a052d27675c60f7..bb55cd89793d0d32ca255eab830610d6cc9065cb 100644
235--- a/db/channels.go
236+++ b/db/channels.go
237@@ -8,7 +8,7 @@ import (
238
239 var createChannelQuery = `INSERT INTO channels (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING`
240
241-func (d *postgresDB) CreateChannel(ctx context.Context, channel models.Channel) error {
242+func (d *postgresDB) SubscribeToChannel(ctx context.Context, channel models.Channel) error {
243 resp, err := d.db.ExecContext(ctx, createChannelQuery, channel.ID, channel.Name)
244 if err != nil {
245 d.l.Error("Failed to create channel", "call", "sql.ExecContext", "error", err)
246@@ -45,55 +45,9 @@ func (d *postgresDB) ListChannels(ctx context.Context) ([]models.Channel, error)
247 return channels, nil
248 }
249
250-var getChannelSubscribersQuery = `SELECT username FROM user_subscriptions WHERE channel_id = $1`
251-
252-func (d *postgresDB) GetChannelSubscribers(ctx context.Context, channelID string) ([]string, error) {
253- rows, err := d.db.QueryContext(ctx, getChannelSubscribersQuery, channelID)
254- if err != nil {
255- d.l.Error("Filed to query channel subscribers", "call", "sql.QueryContext", "error", err)
256- return nil, err
257- }
258- defer rows.Close()
259-
260- subs := make([]string, 0)
261- for rows.Next() {
262- var sub string
263- err = rows.Scan(&sub)
264- if err != nil {
265- d.l.Error("Failed to scan channel subscribers", "call", "sql.Scan", "error", err)
266- return nil, err
267- }
268- subs = append(subs, sub)
269- }
270-
271- return subs, nil
272-}
273-
274-var subscribeUserToChannelQuery = `
275-INSERT INTO user_subscriptions (
276- username
277- , channel_id
278-) VALUES ($1, $2)
279-ON CONFLICT DO NOTHING
280-`
281-
282-func (d *postgresDB) SubscribeUserToChannel(ctx context.Context, username string, channelID string) error {
283- resp, err := d.db.ExecContext(ctx, subscribeUserToChannelQuery, username, channelID)
284- if err != nil {
285- d.l.Error("Failed to subscribe to channel", "call", "sql.ExecContext", "error", err)
286- return err
287- }
288- if affected, _ := resp.RowsAffected(); affected == 0 {
289- return ErrAlreadySubscribed
290- }
291-
292- return nil
293-}
294-
295-var unsubscribeUserFromChannelQuery = `DELETE FROM user_subscriptions WHERE username = $1 AND channel_id = $2`
296-
297-func (d *postgresDB) UnsubscribeUserFromChannel(ctx context.Context, username string, channelID string) error {
298- resp, err := d.db.ExecContext(ctx, unsubscribeUserFromChannelQuery, username, channelID)
299+func (d *postgresDB) UnsubscribeFromChannel(ctx context.Context, channelID string) error {
300+ query := `DELETE FROM channels WHERE id = $1`
301+ resp, err := d.db.ExecContext(ctx, query, channelID)
302 if err != nil {
303 d.l.Error("Failed to unsubscribe from channel", "call", "sql.ExecContext", "error", err)
304 return err
305diff --git a/db/db.go b/db/db.go
306index 65473339e28cf91c687310040727c123a876b388..a451ad811262762f616dcea2f55e0870b658125a 100644
307--- a/db/db.go
308+++ b/db/db.go
309@@ -13,37 +13,23 @@ var (
310 ErrChannelNotFound = errors.New("no channel with that ID found")
311 ErrAlreadySubscribed = errors.New("already subscribed to channel")
312 ErrVideoExists = errors.New("video already exists")
313- ErrUserExists = errors.New("user already exists")
314 )
315
316 // DB represents a database layer for getting video and channel data
317 type DB interface {
318- // AuthenticateUser verifies a user's password against a hashed value
319- AuthenticateUser(ctx context.Context, user models.User) (bool, error)
320- // CreateUser registers a new user in the database
321- CreateUser(ctx context.Context, user models.User) error
322- // DeleteUser registers a new user in the database
323- DeleteUser(ctx context.Context, username string) error
324-
325- // CreateChannel starts tracking a new channel and fetch new videos for it
326- CreateChannel(ctx context.Context, channel models.Channel) error
327 // ListChannels lists all channels from the database
328 ListChannels(ctx context.Context) ([]models.Channel, error)
329- // GetChannelSubscribers lists all channels from the database
330- GetChannelSubscribers(ctx context.Context, channelID string) ([]string, error)
331- // SubscribeUserToChannel will start adding new videos from that channel to the user
332- SubscribeUserToChannel(ctx context.Context, username string, channelID string) error
333- // SubscribeUserToChannel will stop adding videos from that channel to the user
334- UnsubscribeUserFromChannel(ctx context.Context, username string, channelID string) error
335+ // SubscribeToChannel will start fetching new videos from that channel
336+ SubscribeToChannel(ctx context.Context, channel models.Channel) error
337+ // UnsubscribeToChannel will stop fetching videos from that channel
338+ UnsubscribeFromChannel(ctx context.Context, channelID string) error
339
340 // GetNewVideos returns a list of unwatched videos from all subscribed channels
341- GetNewVideos(ctx context.Context, username string, sortDesc bool) ([]models.Video, error)
342- // GetWatchedVideos returns a list of all watched videos for a user
343- GetWatchedVideos(ctx context.Context, username string) ([]models.Video, error)
344+ GetNewVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
345+ // GetWatchedVideos returns a list of all watched videos
346+ GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
347 // AddVideo adds a newly published video to the database
348 AddVideo(ctx context.Context, video models.Video, channelID string) error
349- // AddVideoToUser will list the video in the users feed
350- AddVideoToUser(ctx context.Context, username string, videoID string) error
351- // SetVideoWatchTime sets or unsets the watch timestamp of a user's video
352- SetVideoWatchTime(ctx context.Context, username string, videoID string, watchTime *time.Time) error
353+ // SetVideoWatchTime sets or unsets the watch timestamp of a video
354+ SetVideoWatchTime(ctx context.Context, videoID string, watchTime *time.Time) error
355 }
356diff --git a/db/psql.go b/db/psql.go
357index ea3b6e1dc419910bacf911f40794875e68014173..730dad66c8e67fd352c4205393e0ce978668c54b 100644
358--- a/db/psql.go
359+++ b/db/psql.go
360@@ -5,8 +5,6 @@ import (
361 "log/slog"
362
363 _ "github.com/lib/pq"
364-
365- ytrssilConfig "github.com/TheEdgeOfRage/ytrssil-api/config"
366 )
367
368 type postgresDB struct {
369@@ -14,8 +12,10 @@ type postgresDB struct {
370 db *sql.DB
371 }
372
373-func NewPostgresDB(log *slog.Logger, dbCfg ytrssilConfig.DB) (*postgresDB, error) {
374- db, err := sql.Open("postgres", dbCfg.DBURI)
375+var _ DB = (*postgresDB)(nil)
376+
377+func NewPostgresDB(log *slog.Logger, dbURI string) (*postgresDB, error) {
378+ db, err := sql.Open("postgres", dbURI)
379 if err != nil {
380 return nil, err
381 }
382diff --git a/db/users.go b/db/users.go
383deleted file mode 100644
384index 535de5952e168b9ab615659b279bdd92bbeb0348..0000000000000000000000000000000000000000
385--- a/db/users.go
386+++ /dev/null
387@@ -1,63 +0,0 @@
388-package db
389-
390-import (
391- "context"
392- "database/sql"
393- "errors"
394-
395- "github.com/alexedwards/argon2id"
396-
397- "github.com/TheEdgeOfRage/ytrssil-api/models"
398-)
399-
400-var authenticateUserQuery = `SELECT password FROM users WHERE username = $1`
401-
402-func (d *postgresDB) AuthenticateUser(ctx context.Context, user models.User) (bool, error) {
403- row := d.db.QueryRowContext(ctx, authenticateUserQuery, user.Username)
404- var hashedPassword string
405- err := row.Scan(&hashedPassword)
406- if err != nil {
407- if errors.Is(err, sql.ErrNoRows) {
408- return false, nil
409- }
410-
411- d.l.Error("Failed to query user for auth", "error", err)
412- return false, err
413- }
414-
415- match, err := argon2id.ComparePasswordAndHash(user.Password, hashedPassword)
416- if err != nil {
417- d.l.Error("Failed to hash password", "error", err)
418- return false, err
419- }
420-
421- return match, nil
422-}
423-
424-var createUserQuery = `INSERT INTO users (username, password) VALUES ($1, $2) ON CONFLICT DO NOTHING`
425-
426-func (d *postgresDB) CreateUser(ctx context.Context, user models.User) error {
427- resp, err := d.db.ExecContext(ctx, createUserQuery, user.Username, user.Password)
428- if err != nil {
429- d.l.Error("Failed to create user", "error", err)
430- return err
431- }
432-
433- if affected, _ := resp.RowsAffected(); affected == 0 {
434- return ErrUserExists
435- }
436-
437- return nil
438-}
439-
440-var deleteUserQuery = `DELETE FROM users WHERE username = $1`
441-
442-func (d *postgresDB) DeleteUser(ctx context.Context, username string) error {
443- _, err := d.db.ExecContext(ctx, deleteUserQuery, username)
444- if err != nil {
445- d.l.Error("Failed to delete user", "error", err)
446- return err
447- }
448-
449- return nil
450-}
451diff --git a/db/videos.go b/db/videos.go
452index 70ea0e8a20b3db14a0b2bcbc4ccf3670bf22c35b..70440126e85e3f12a7052c48d8c0f7b13d51b487 100644
453--- a/db/videos.go
454+++ b/db/videos.go
455@@ -7,29 +7,25 @@ import (
456 "github.com/TheEdgeOfRage/ytrssil-api/models"
457 )
458
459-func (d *postgresDB) GetNewVideos(ctx context.Context, username string, sortDesc bool) ([]models.Video, error) {
460- getNewVideosQuery := `
461+func (d *postgresDB) GetNewVideos(ctx context.Context, sortDesc bool) ([]models.Video, error) {
462+ query := `
463 SELECT
464 videos.id
465- , videos.title
466- , videos.published_timestamp
467- , videos.is_short
468- , channels.name as channel_name
469- , channels.id as channel_id
470- FROM user_videos
471- LEFT JOIN videos ON video_id=videos.id
472- LEFT JOIN channels ON channel_id=channels.id
473- WHERE
474- 1=1
475- AND watch_timestamp IS NULL
476- AND username=$1
477+ , title
478+ , published_timestamp
479+ , is_short
480+ , channels.name
481+ , channels.id
482+ FROM videos
483+ LEFT JOIN channels ON videos.channel_id=channels.id
484+ WHERE watch_timestamp IS NULL
485 ORDER BY published_timestamp
486 `
487 if sortDesc {
488- getNewVideosQuery += "DESC"
489+ query += " DESC"
490 }
491
492- rows, err := d.db.QueryContext(ctx, getNewVideosQuery, username)
493+ rows, err := d.db.QueryContext(ctx, query)
494 if err != nil {
495 d.l.Error("Failed to query new videos", "call", "sql.QueryContext", "error", err)
496 return nil, err
497@@ -57,27 +53,26 @@ func (d *postgresDB) GetNewVideos(ctx context.Context, username string, sortDesc
498 return videos, nil
499 }
500
501-const getWatchedVideosQuery = `
502- SELECT
503- videos.id
504- , videos.title
505- , videos.published_timestamp
506- , videos.watch_timestamp
507- , videos.is_short
508- , channels.name as channel_name
509- , channels.id as channel_id
510- FROM user_videos
511- LEFT JOIN videos ON video_id=videos.id
512- LEFT JOIN channels ON channel_id=channels.id
513- WHERE
514- 1=1
515- AND watch_timestamp IS NOT NULL
516- AND username=$1
517- ORDER BY watch_timestamp DESC
518-`
519-
520-func (d *postgresDB) GetWatchedVideos(ctx context.Context, username string) ([]models.Video, error) {
521- rows, err := d.db.QueryContext(ctx, getWatchedVideosQuery, username)
522+func (d *postgresDB) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error) {
523+ query := `
524+ SELECT
525+ videos.id
526+ , title
527+ , published_timestamp
528+ , watch_timestamp
529+ , is_short
530+ , channels.name
531+ , channels.id
532+ FROM videos
533+ LEFT JOIN channels ON videos.channel_id=channels.id
534+ WHERE watch_timestamp IS NOT NULL
535+ ORDER BY watch_timestamp DESC
536+ `
537+ if sortDesc {
538+ query += " DESC"
539+ }
540+
541+ rows, err := d.db.QueryContext(ctx, query)
542 if err != nil {
543 d.l.Error("Failed to query for watched videos", "call", "sql.QueryContext", "error", err)
544 return nil, err
545@@ -105,21 +100,21 @@ func (d *postgresDB) GetWatchedVideos(ctx context.Context, username string) ([]m
546 return videos, nil
547 }
548
549-const addVideoQuery = `
550-INSERT INTO videos (
551- id
552- , title
553- , published_timestamp
554- , is_short
555diff --git a/go.mod b/go.mod
556index 1e455d6a77b8aeb8ecf0f3a9ac3a74e933943f54..d99507e0ee034722b5dabf22374d8634956d1c74 100644
557--- a/go.mod
558+++ b/go.mod
559@@ -4,7 +4,6 @@ go 1.25
560
561 require (
562 github.com/a-h/templ v0.3.960
563- github.com/alexedwards/argon2id v1.0.0
564 github.com/gin-gonic/gin v1.11.0
565 github.com/jessevdk/go-flags v1.6.1
566 github.com/lib/pq v1.10.9
567diff --git a/go.sum b/go.sum
568index adb41e85d1ec16ccf21216c6647335362075c2d7..ae873fe176a15ec20a1a0c42beb9955f35927458 100644
569--- a/go.sum
570+++ b/go.sum
571@@ -1,7 +1,5 @@
572 github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
573 github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
574-github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
575-github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
576 github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
577 github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
578 github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
579@@ -72,63 +70,25 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
580 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
581 github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
582 github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
583-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
584 go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
585 go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
586 golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
587 golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
588-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
589-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
590-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
591 golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
592 golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
593-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
594-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
595 golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
596 golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
597-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
598-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
599-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
600-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
601-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
602 golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
603 golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
604-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
605-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
606-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
607 golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
608 golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
609-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
610-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
611-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
612-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
613-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
614-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
615 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
616-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
617-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
618 golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
619 golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
620-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
621-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
622-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
623-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
624-golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
625-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
626-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
627-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
628-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
629-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
630-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
631 golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
632 golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
633-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
634-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
635-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
636-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
637 golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
638 golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
639-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
640 google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
641 google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
642 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
643diff --git a/handler/channels.go b/handler/channels.go
644index 1a92a9e38f594391cc8d1b033dfca28cbea86fca..1635f71c60166cb12ac82ca2d5f6dc93cf3a6e84 100644
645--- a/handler/channels.go
646+++ b/handler/channels.go
647@@ -8,7 +8,7 @@ import (
648 "github.com/TheEdgeOfRage/ytrssil-api/models"
649 )
650
651-func (h *handler) SubscribeToChannel(ctx context.Context, username string, channelID string) error {
652+func (h *handler) SubscribeToChannel(ctx context.Context, channelID string) error {
653 parsedChannel, err := h.parser.Parse(channelID)
654 if err != nil {
655 return err
656@@ -19,14 +19,14 @@ func (h *handler) SubscribeToChannel(ctx context.Context, username string, chann
657 Name: parsedChannel.Name,
658 }
659
660- err = h.db.CreateChannel(ctx, channel)
661+ err = h.db.SubscribeToChannel(ctx, channel)
662 if err != nil && !errors.Is(err, db.ErrChannelExists) {
663 return err
664 }
665
666- return h.db.SubscribeUserToChannel(ctx, username, channelID)
667+ return nil
668 }
669
670-func (h *handler) UnsubscribeFromChannel(ctx context.Context, username string, channelID string) error {
671- return h.db.UnsubscribeUserFromChannel(ctx, username, channelID)
672+func (h *handler) UnsubscribeFromChannel(ctx context.Context, channelID string) error {
673+ return h.db.UnsubscribeFromChannel(ctx, channelID)
674 }
675diff --git a/handler/handler.go b/handler/handler.go
676index 5da10ab2147d27ad4224c0fc0022ac7c8dd39db1..17426a27e10b168c7b56cd03dea399eb9cc0ef82 100644
677--- a/handler/handler.go
678+++ b/handler/handler.go
679@@ -10,14 +10,13 @@ import (
680 )
681
682 type Handler interface {
683- CreateUser(ctx context.Context, user models.User) error
684- SubscribeToChannel(ctx context.Context, username string, channelID string) error
685- UnsubscribeFromChannel(ctx context.Context, username string, channelID string) error
686- GetNewVideos(ctx context.Context, username string, sortDesc bool) ([]models.Video, error)
687- GetWatchedVideos(ctx context.Context, username string) ([]models.Video, error)
688+ SubscribeToChannel(ctx context.Context, channelID string) error
689+ UnsubscribeFromChannel(ctx context.Context, channelID string) error
690+ GetNewVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
691+ GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
692 FetchVideos(ctx context.Context) error
693- MarkVideoAsWatched(ctx context.Context, username string, videoID string) error
694- MarkVideoAsUnwatched(ctx context.Context, username string, videoID string) error
695+ MarkVideoAsWatched(ctx context.Context, videoID string) error
696+ MarkVideoAsUnwatched(ctx context.Context, videoID string) error
697 }
698
699 type handler struct {
700diff --git a/handler/handler_test.go b/handler/handler_test.go
701index ec522f3353906e152cc26ada26ad9482202dc143..e5d789d55f9929ac0d6d0bd84e99582af3ac80b7 100644
702--- a/handler/handler_test.go
703+++ b/handler/handler_test.go
704@@ -24,7 +24,7 @@ func init() {
705 func TestGetNewVideos(t *testing.T) {
706 l := slog.New(slog.NewTextHandler(io.Discard, nil))
707 handler := New(l, &db_mock.DBMock{
708- GetNewVideosFunc: func(ctx context.Context, username string, _ bool) ([]models.Video, error) {
709+ GetNewVideosFunc: func(ctx context.Context, _ bool) ([]models.Video, error) {
710 return []models.Video{
711 {
712 ID: "test",
713@@ -35,7 +35,7 @@ func TestGetNewVideos(t *testing.T) {
714 }, nil
715 },
716 }, &parser_mock.ParserMock{})
717- resp, err := handler.GetNewVideos(context.TODO(), "username", false)
718+ resp, err := handler.GetNewVideos(context.TODO(), false)
719
720 if assert.NoError(t, err) {
721 if assert.NotNil(t, resp) {
722diff --git a/handler/users.go b/handler/users.go
723deleted file mode 100644
724index 913889547dfb1671704226d3f262bc38820a325a..0000000000000000000000000000000000000000
725--- a/handler/users.go
726+++ /dev/null
727@@ -1,19 +0,0 @@
728-package handler
729-
730-import (
731- "context"
732-
733- "github.com/alexedwards/argon2id"
734-
735- "github.com/TheEdgeOfRage/ytrssil-api/models"
736-)
737-
738-func (h *handler) CreateUser(ctx context.Context, user models.User) error {
739- hashedPassword, err := argon2id.CreateHash(user.Password, argon2id.DefaultParams)
740- if err != nil {
741- return err
742- }
743- user.Password = hashedPassword
744-
745- return h.db.CreateUser(ctx, user)
746-}
747diff --git a/handler/videos.go b/handler/videos.go
748index 5a91db50daa6e3a03ffc7d69955ca9c4a5e845a1..f6ec420bbdeb023351dc5301011b35697be2c393 100644
749--- a/handler/videos.go
750+++ b/handler/videos.go
751@@ -14,30 +14,12 @@ import (
752 "github.com/TheEdgeOfRage/ytrssil-api/models"
753 )
754
755-func (h *handler) GetNewVideos(ctx context.Context, username string, sortDesc bool) ([]models.Video, error) {
756- return h.db.GetNewVideos(ctx, username, sortDesc)
757+func (h *handler) GetNewVideos(ctx context.Context, sortDesc bool) ([]models.Video, error) {
758+ return h.db.GetNewVideos(ctx, sortDesc)
759 }
760
761-func (h *handler) GetWatchedVideos(ctx context.Context, username string) ([]models.Video, error) {
762- return h.db.GetWatchedVideos(ctx, username)
763-}
764-
765-func (h *handler) addVideoToAllSubscribers(ctx context.Context, channelID string, videoID string) error {
766- subs, err := h.db.GetChannelSubscribers(ctx, channelID)
767- if err != nil {
768- h.log.Error("failed to get channel subscribers", "call", "db.GetChannelSubscribers", "err", err)
769- return err
770- }
771-
772- for _, sub := range subs {
773- err = h.db.AddVideoToUser(ctx, sub, videoID)
774- if err != nil {
775- h.log.Error("Failed to add video to user", "call", "db.AddVideoToUser", "err", err)
776- continue
777- }
778- }
779-
780- return nil
781+func (h *handler) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error) {
782+ return h.db.GetWatchedVideos(ctx, sortDesc)
783 }
784
785 func (h *handler) isShort(ctx context.Context, videoID string) (bool, error) {
786@@ -92,10 +74,6 @@ func (h *handler) addVideosForChannel(ctx context.Context, parsedChannel *feedpa
787 }
788 continue
789 }
790- err = h.addVideoToAllSubscribers(ctx, parsedChannel.ID, videoID)
791- if err != nil {
792- continue
793- }
794 }
795 }
796
797@@ -128,11 +106,11 @@ func (h *handler) FetchVideos(ctx context.Context) error {
798 return nil
799 }
800
801-func (h *handler) MarkVideoAsWatched(ctx context.Context, username string, videoID string) error {
802+func (h *handler) MarkVideoAsWatched(ctx context.Context, videoID string) error {
803 watchTime := time.Now()
804- return h.db.SetVideoWatchTime(ctx, username, videoID, &watchTime)
805+ return h.db.SetVideoWatchTime(ctx, videoID, &watchTime)
806 }
807
808-func (h *handler) MarkVideoAsUnwatched(ctx context.Context, username string, videoID string) error {
809- return h.db.SetVideoWatchTime(ctx, username, videoID, nil)
810+func (h *handler) MarkVideoAsUnwatched(ctx context.Context, videoID string) error {
811+ return h.db.SetVideoWatchTime(ctx, videoID, nil)
812 }
813diff --git a/httpserver/auth/auth.go b/httpserver/auth/auth.go
814index 0445cbcb98345951beb3e509c9e7c5b927c6eb85..47b21aa8bcf60a70c67bcab6a82f6327b67797aa 100644
815--- a/httpserver/auth/auth.go
816+++ b/httpserver/auth/auth.go
817@@ -4,35 +4,39 @@ import (
818 "net/http"
819
820 "github.com/gin-gonic/gin"
821-
822- "github.com/TheEdgeOfRage/ytrssil-api/db"
823- "github.com/TheEdgeOfRage/ytrssil-api/models"
824 )
825
826-// AuthMiddleware will authenticate against a static API key
827-func AuthMiddleware(db db.DB) gin.HandlerFunc {
828+// PageAuthMiddleware will authenticate against a static auth token
829+func PageAuthMiddleware(authToken string) gin.HandlerFunc {
830 return func(c *gin.Context) {
831- username, password, ok := c.Request.BasicAuth()
832- if !ok {
833- c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid basic auth header"})
834+ tokenCookie, err := c.Request.Cookie("token")
835+ if err != nil {
836+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing auth token cookie"})
837 return
838 }
839- user := models.User{
840- Username: username,
841- Password: password,
842+ if tokenCookie.Value != authToken {
843+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid auth token"})
844+ return
845 }
846- authenticated, err := db.AuthenticateUser(c.Request.Context(), user)
847- if err != nil {
848- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
849+
850+ // handle request
851+ c.Next()
852+ }
853+}
854+
855+// APIAuthMiddleware will authenticate API endpoints against a static auth token
856+func APIAuthMiddleware(authToken string) gin.HandlerFunc {
857+ return func(c *gin.Context) {
858+ token, ok := c.Request.Header["Authorization"]
859+ if !ok || len(token) != 1 {
860+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing Authorization header"})
861 return
862 }
863- if !authenticated {
864- c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
865+ if token[0] != authToken {
866+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid auth token"})
867 return
868 }
869
870- c.Set("username", username)
871-
872 // handle request
873 c.Next()
874 }
875diff --git a/httpserver/auth/auth_test.go b/httpserver/auth/auth_test.go
876index 141a150c11b06ee4e3c8cfee8c4b3a522ea2c409..c6cb38a7f0ef538cebf49cec477484833a7d01ff 100644
877--- a/httpserver/auth/auth_test.go
878+++ b/httpserver/auth/auth_test.go
879@@ -1,70 +1,69 @@
880 package auth
881
882 import (
883- "context"
884 "net/http"
885 "net/http/httptest"
886 "testing"
887
888 "github.com/gin-gonic/gin"
889- "github.com/stretchr/testify/assert"
890+ "github.com/stretchr/testify/suite"
891
892- db_mock "github.com/TheEdgeOfRage/ytrssil-api/mocks/db"
893- "github.com/TheEdgeOfRage/ytrssil-api/models"
894+ "github.com/TheEdgeOfRage/ytrssil-api/config"
895 )
896
897-func setupTestServer() *http.Server {
898- db := &db_mock.DBMock{
899- AuthenticateUserFunc: func(ctx context.Context, user models.User) (bool, error) {
900- return user.Username == "username" && user.Password == "password", nil
901- },
902- }
903+type AuthTestSuite struct {
904+ suite.Suite
905+
906+ cfg config.Config
907+ server *http.Server
908+ engine *gin.Engine
909+}
910+
911+func TestAuthTestSuite(t *testing.T) {
912+ suite.Run(t, new(AuthTestSuite))
913+}
914+
915+func (s *AuthTestSuite) SetupSuite() {
916+ s.cfg = config.TestConfig()
917
918 gin.SetMode(gin.TestMode)
919- router := gin.New()
920- // Middlewares are executed top to bottom in a stack-like manner
921- router.Use(
922- gin.Recovery(), // Recovery needs to go before other middlewares to catch panics
923- AuthMiddleware(db),
924+ s.engine = gin.New()
925+ s.engine.Use(
926+ gin.Recovery(),
927+ APIAuthMiddleware(s.cfg.AuthToken),
928 )
929- router.GET("/", func(c *gin.Context) {
930+ s.engine.GET("/", func(c *gin.Context) {
931 c.String(http.StatusOK, "OK")
932 })
933
934- return &http.Server{Handler: router}
935+ s.server = &http.Server{Handler: s.engine}
936 }
937
938-func TestSuccessfulAuthentication(t *testing.T) {
939- server := setupTestServer()
940-
941+func (s *AuthTestSuite) TestSuccessfulAuthentication() {
942 w := httptest.NewRecorder()
943 req, _ := http.NewRequest("GET", "/", nil)
944- req.SetBasicAuth("username", "password") // Valid credentials
945- server.Handler.ServeHTTP(w, req)
946+ req.Header["Authorization"] = []string{"foo"}
947+ s.server.Handler.ServeHTTP(w, req)
948
949- assert.Equal(t, http.StatusOK, w.Code)
950- assert.Equal(t, "OK", w.Body.String())
951+ s.Equal(http.StatusOK, w.Code)
952+ s.Equal("OK", w.Body.String())
953 }
954
955-func TestMissingAuthorizationHeader(t *testing.T) {
956- server := setupTestServer()
957-
958+func (s *AuthTestSuite) TestMissingAuthorizationHeader() {
959 w := httptest.NewRecorder()
960 req, _ := http.NewRequest("GET", "/", nil)
961- server.Handler.ServeHTTP(w, req)
962+ s.server.Handler.ServeHTTP(w, req)
963
964- assert.Equal(t, http.StatusUnauthorized, w.Code)
965- assert.Equal(t, `{"error":"invalid basic auth header"}`, w.Body.String())
966+ s.Equal(http.StatusUnauthorized, w.Code)
967+ s.Equal(`{"error":"missing Authorization header"}`, w.Body.String())
968 }
969
970-func TestWrongCredentials(t *testing.T) {
971- server := setupTestServer()
972-
973+func (s *AuthTestSuite) TestWrongCredentials() {
974 w := httptest.NewRecorder()
975 req, _ := http.NewRequest("GET", "/", nil)
976- req.SetBasicAuth("test", "test") // Invalid credentials
977- server.Handler.ServeHTTP(w, req)
978+ req.Header["Authorization"] = []string{"bar"}
979diff --git a/httpserver/ytrssil/api_setup_test.go b/httpserver/ytrssil/api_setup_test.go
980index 5506873c0dc52e0fc9aa8ba1fcff31aa22b8c488..23b098c4a77a0f7d6da56b113c25e2c27cd0b480 100644
981--- a/httpserver/ytrssil/api_setup_test.go
982+++ b/httpserver/ytrssil/api_setup_test.go
983@@ -35,12 +35,13 @@ func setupTestServer(t *testing.T) *http.Server {
984 router, err := ytrssil.SetupGinRouter(
985 l,
986 handler,
987- auth.AuthMiddleware(nil),
988+ auth.APIAuthMiddleware(""),
989+ auth.PageAuthMiddleware(""),
990 )
991 assert.Nil(t, err)
992
993 return &http.Server{
994- Addr: fmt.Sprintf(":%v", testConfig.Gin.Port),
995+ Addr: fmt.Sprintf(":%v", testConfig.Port),
996 Handler: router,
997 }
998 }
999diff --git a/httpserver/ytrssil/channels.go b/httpserver/ytrssil/channels.go
1000index a726a86ff02960af6e167c467ee3db309c71160e..f4101d5bac44ec10f73b198fee1ed75a784210f7 100644
1001--- a/httpserver/ytrssil/channels.go
1002+++ b/httpserver/ytrssil/channels.go
1003@@ -18,9 +18,8 @@ func (srv *server) SubscribeToChannelJSON(c *gin.Context) {
1004 c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1005 return
1006 }
1007- username := c.GetString("username")
1008
1009- err = srv.handler.SubscribeToChannel(c.Request.Context(), username, channel.ID)
1010+ err = srv.handler.SubscribeToChannel(c.Request.Context(), channel.ID)
1011 if err != nil {
1012 if errors.Is(err, db.ErrAlreadySubscribed) {
1013 c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": err.Error()})
1014@@ -45,9 +44,8 @@ func (srv *server) UnsubscribeFromChannelJSON(c *gin.Context) {
1015 c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1016 return
1017 }
1018- username := c.GetString("username")
1019
1020- err = srv.handler.UnsubscribeFromChannel(c.Request.Context(), username, channel.ID)
1021+ err = srv.handler.UnsubscribeFromChannel(c.Request.Context(), channel.ID)
1022 if err != nil {
1023 if errors.Is(err, db.ErrChannelNotFound) {
1024 c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()})
1025diff --git a/httpserver/ytrssil/server.go b/httpserver/ytrssil/server.go
1026index a6c54a8f042819c97619c254d909d14a35d7950f..6e0df289bf3bdad6de5edef4a7e09c2a1bd024af 100644
1027--- a/httpserver/ytrssil/server.go
1028+++ b/httpserver/ytrssil/server.go
1029@@ -37,7 +37,12 @@ func (srv *server) Healthz(c *gin.Context) {
1030 }
1031
1032 // SetupGinRouter sets up routes for all APIs on a Gin server (aka router)
1033-func SetupGinRouter(l *slog.Logger, handler handler.Handler, authMiddleware func(c *gin.Context)) (*gin.Engine, error) {
1034+func SetupGinRouter(
1035+ l *slog.Logger,
1036+ handler handler.Handler,
1037+ apiAuthMiddleware func(c *gin.Context),
1038+ pageAuthMiddleware func(c *gin.Context),
1039+) (*gin.Engine, error) {
1040 engine := gin.New()
1041 // Middlewares are executed top to bottom in a stack-like manner
1042 engine.Use(
1043@@ -57,18 +62,17 @@ func SetupGinRouter(l *slog.Logger, handler handler.Handler, authMiddleware func
1044 return nil, err
1045 }
1046 engine.GET("/healthz", srv.Healthz)
1047- engine.POST("/register", srv.CreateUserJSON)
1048 engine.POST("/fetch", srv.FetchVideosJSON)
1049
1050 pages := engine.Group("")
1051- pages.Use(authMiddleware)
1052+ pages.Use(pageAuthMiddleware)
1053
1054 pages.GET("/", srv.NewVideosPage)
1055 pages.POST("/videos/:video_id/watch", srv.MarkVideoAsWatchedPage)
1056
1057 // all APIs go in this routing group and require authentication
1058 api := engine.Group("/api")
1059- api.Use(authMiddleware)
1060+ api.Use(apiAuthMiddleware)
1061 {
1062 api.POST("channels/:channel_id/subscribe", srv.SubscribeToChannelJSON)
1063 api.POST("channels/:channel_id/unsubscribe", srv.UnsubscribeFromChannelJSON)
1064diff --git a/httpserver/ytrssil/users.go b/httpserver/ytrssil/users.go
1065deleted file mode 100644
1066index e9a475e0716834c4ccf2faf45cffd804000a3e3c..0000000000000000000000000000000000000000
1067--- a/httpserver/ytrssil/users.go
1068+++ /dev/null
1069@@ -1,33 +0,0 @@
1070-package ytrssil
1071-
1072-import (
1073- "errors"
1074- "net/http"
1075-
1076- "github.com/gin-gonic/gin"
1077-
1078- "github.com/TheEdgeOfRage/ytrssil-api/db"
1079- "github.com/TheEdgeOfRage/ytrssil-api/models"
1080-)
1081-
1082-func (srv *server) CreateUserJSON(c *gin.Context) {
1083- var user models.User
1084- err := c.ShouldBindJSON(&user)
1085- if err != nil {
1086- c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1087- return
1088- }
1089-
1090- err = srv.handler.CreateUser(c.Request.Context(), user)
1091- if err != nil {
1092- if errors.Is(err, db.ErrUserExists) {
1093- c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": err.Error()})
1094- return
1095- }
1096-
1097- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1098- return
1099- }
1100-
1101- c.JSON(http.StatusOK, gin.H{"msg": "user created"})
1102-}
1103diff --git a/httpserver/ytrssil/videos.go b/httpserver/ytrssil/videos.go
1104index d4fe61635c8506c79d69d1a9faeab60663806503..053fd6a0bfb3f4fc18c2643a4a4fd1504e702364 100644
1105--- a/httpserver/ytrssil/videos.go
1106+++ b/httpserver/ytrssil/videos.go
1107@@ -9,8 +9,7 @@ import (
1108 )
1109
1110 func (srv *server) GetNewVideosJSON(c *gin.Context) {
1111- username := c.GetString("username")
1112- videos, err := srv.handler.GetNewVideos(c.Request.Context(), username, false)
1113+ videos, err := srv.handler.GetNewVideos(c.Request.Context(), false)
1114 if err != nil {
1115 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1116 return
1117@@ -22,8 +21,7 @@ func (srv *server) GetNewVideosJSON(c *gin.Context) {
1118 }
1119
1120 func (srv *server) GetWatchedVideosJSON(c *gin.Context) {
1121- username := c.GetString("username")
1122- videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), username)
1123+ videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), false)
1124 if err != nil {
1125 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1126 return
1127@@ -45,7 +43,6 @@ func (srv *server) FetchVideosJSON(c *gin.Context) {
1128 }
1129
1130 func (srv *server) MarkVideoAsWatchedJSON(c *gin.Context) {
1131- username := c.GetString("username")
1132 var req models.VideoURIRequest
1133 err := c.ShouldBindUri(&req)
1134 if err != nil {
1135@@ -53,7 +50,7 @@ func (srv *server) MarkVideoAsWatchedJSON(c *gin.Context) {
1136 return
1137 }
1138
1139- err = srv.handler.MarkVideoAsWatched(c.Request.Context(), username, req.VideoID)
1140+ err = srv.handler.MarkVideoAsWatched(c.Request.Context(), req.VideoID)
1141 if err != nil {
1142 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1143 return
1144@@ -63,7 +60,6 @@ func (srv *server) MarkVideoAsWatchedJSON(c *gin.Context) {
1145 }
1146
1147 func (srv *server) MarkVideoAsUnwatchedJSON(c *gin.Context) {
1148- username := c.GetString("username")
1149 var req models.VideoURIRequest
1150 err := c.ShouldBindUri(&req)
1151 if err != nil {
1152@@ -71,7 +67,7 @@ func (srv *server) MarkVideoAsUnwatchedJSON(c *gin.Context) {
1153 return
1154 }
1155
1156- err = srv.handler.MarkVideoAsUnwatched(c.Request.Context(), username, req.VideoID)
1157+ err = srv.handler.MarkVideoAsUnwatched(c.Request.Context(), req.VideoID)
1158 if err != nil {
1159 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1160 return
1161diff --git a/httpserver/ytrssil/videos_pages.go b/httpserver/ytrssil/videos_pages.go
1162index 288f7294af3bbe2c815ebc4e06658fc06e833eb3..85978d9a0be99a996a9b673b9bbdd03b4de7fd10 100644
1163--- a/httpserver/ytrssil/videos_pages.go
1164+++ b/httpserver/ytrssil/videos_pages.go
1165@@ -10,8 +10,7 @@ import (
1166 )
1167
1168 func (srv server) NewVideosPage(c *gin.Context) {
1169- username := c.GetString("username")
1170- videos, err := srv.handler.GetNewVideos(c.Request.Context(), username, true)
1171+ videos, err := srv.handler.GetNewVideos(c.Request.Context(), true)
1172 if err != nil {
1173 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1174 return
1175@@ -29,7 +28,6 @@ func (srv server) MarkVideoAsWatchedPage(c *gin.Context) {
1176 Ctx: c.Request.Context(),
1177 }
1178
1179- username := c.GetString("username")
1180 var req models.VideoURIRequest
1181 err := c.ShouldBindUri(&req)
1182 if err != nil {
1183@@ -38,7 +36,7 @@ func (srv server) MarkVideoAsWatchedPage(c *gin.Context) {
1184 return
1185 }
1186
1187- err = srv.handler.MarkVideoAsWatched(c.Request.Context(), username, req.VideoID)
1188+ err = srv.handler.MarkVideoAsWatched(c.Request.Context(), req.VideoID)
1189 if err != nil {
1190 r.Component = pages.ErrorPage(err)
1191 c.Render(http.StatusInternalServerError, r)
1192diff --git a/migrations/000003_remove_users.down.sql b/migrations/000003_remove_users.down.sql
1193new file mode 100644
1194index 0000000000000000000000000000000000000000..2fe66d4b0938e214102723d74460c941c68c6676
1195--- /dev/null
1196+++ b/migrations/000003_remove_users.down.sql
1197@@ -0,0 +1,19 @@
1198+ALTER TABLE videos DROP COLUMN IF EXISTS watch_timestamp;
1199+
1200+CREATE TABLE IF NOT EXISTS users (
1201+ username text NOT NULL PRIMARY KEY
1202+ , password text NOT NULL
1203+);
1204+
1205+CREATE TABLE IF NOT EXISTS user_videos (
1206+ username text NOT NULL REFERENCES users(username)
1207+ , video_id text NOT NULL REFERENCES videos(id)
1208+ , watch_timestamp timestamp with time zone
1209+ , CONSTRAINT user_videos_pkey PRIMARY KEY (username, video_id)
1210+);
1211+
1212+CREATE TABLE IF NOT EXISTS user_subscriptions (
1213+ username text NOT NULL REFERENCES users(username)
1214+ , channel_id text NOT NULL REFERENCES channels(id)
1215+ , CONSTRAINT user_subscriptions_pkey PRIMARY KEY (channel_id, username)
1216+);
1217diff --git a/migrations/000003_remove_users.up.sql b/migrations/000003_remove_users.up.sql
1218new file mode 100644
1219index 0000000000000000000000000000000000000000..85468061a5bf34db61a76cc413f8cf14a3ec2485
1220--- /dev/null
1221+++ b/migrations/000003_remove_users.up.sql
1222@@ -0,0 +1,10 @@
1223+ALTER TABLE videos ADD COLUMN IF NOT EXISTS watch_timestamp timestamp with time zone DEFAULT NULL;
1224+
1225+UPDATE videos
1226+ SET watch_timestamp = user_videos.watch_timestamp
1227+ FROM user_videos
1228+ WHERE videos.id = user_videos.video_id;
1229+
1230+DROP TABLE IF EXISTS user_subscriptions;
1231+DROP TABLE IF EXISTS user_videos;
1232+DROP TABLE IF EXISTS users;
1233diff --git a/mocks/db/db.go b/mocks/db/db.go
1234index 31c171fec25e6f06997a80fc7eb97f917d43b45b..0f0bb6475f14f597f0c416421d6debd75833875a 100644
1235--- a/mocks/db/db.go
1236+++ b/mocks/db/db.go
1237@@ -24,41 +24,23 @@ var _ db.DB = &DBMock{}
1238 // AddVideoFunc: func(ctx context.Context, video models.Video, channelID string) error {
1239 // panic("mock out the AddVideo method")
1240 // },
1241-// AddVideoToUserFunc: func(ctx context.Context, username string, videoID string) error {
1242-// panic("mock out the AddVideoToUser method")
1243-// },
1244-// AuthenticateUserFunc: func(ctx context.Context, user models.User) (bool, error) {
1245-// panic("mock out the AuthenticateUser method")
1246-// },
1247-// CreateChannelFunc: func(ctx context.Context, channel models.Channel) error {
1248-// panic("mock out the CreateChannel method")
1249-// },
1250-// CreateUserFunc: func(ctx context.Context, user models.User) error {
1251-// panic("mock out the CreateUser method")
1252-// },
1253-// DeleteUserFunc: func(ctx context.Context, username string) error {
1254-// panic("mock out the DeleteUser method")
1255-// },
1256-// GetChannelSubscribersFunc: func(ctx context.Context, channelID string) ([]string, error) {
1257-// panic("mock out the GetChannelSubscribers method")
1258-// },
1259-// GetNewVideosFunc: func(ctx context.Context, username string, sortDesc bool) ([]models.Video, error) {
1260+// GetNewVideosFunc: func(ctx context.Context, sortDesc bool) ([]models.Video, error) {
1261 // panic("mock out the GetNewVideos method")
1262 // },
1263-// GetWatchedVideosFunc: func(ctx context.Context, username string) ([]models.Video, error) {
1264+// GetWatchedVideosFunc: func(ctx context.Context, sortDesc bool) ([]models.Video, error) {
1265 // panic("mock out the GetWatchedVideos method")
1266 // },
1267 // ListChannelsFunc: func(ctx context.Context) ([]models.Channel, error) {
1268 // panic("mock out the ListChannels method")
1269 // },
1270-// SetVideoWatchTimeFunc: func(ctx context.Context, username string, videoID string, watchTime *time.Time) error {
1271+// SetVideoWatchTimeFunc: func(ctx context.Context, videoID string, watchTime *time.Time) error {
1272 // panic("mock out the SetVideoWatchTime method")
1273 // },
1274-// SubscribeUserToChannelFunc: func(ctx context.Context, username string, channelID string) error {
1275-// panic("mock out the SubscribeUserToChannel method")
1276+// SubscribeToChannelFunc: func(ctx context.Context, channel models.Channel) error {
1277+// panic("mock out the SubscribeToChannel method")
1278 // },
1279-// UnsubscribeUserFromChannelFunc: func(ctx context.Context, username string, channelID string) error {
1280-// panic("mock out the UnsubscribeUserFromChannel method")
1281+// UnsubscribeFromChannelFunc: func(ctx context.Context, channelID string) error {
1282+// panic("mock out the UnsubscribeFromChannel method")
1283 // },
1284 // }
1285 //
1286@@ -70,41 +52,23 @@ type DBMock struct {
1287 // AddVideoFunc mocks the AddVideo method.
1288 AddVideoFunc func(ctx context.Context, video models.Video, channelID string) error
1289
1290- // AddVideoToUserFunc mocks the AddVideoToUser method.
1291- AddVideoToUserFunc func(ctx context.Context, username string, videoID string) error
1292-
1293- // AuthenticateUserFunc mocks the AuthenticateUser method.
1294- AuthenticateUserFunc func(ctx context.Context, user models.User) (bool, error)
1295-
1296- // CreateChannelFunc mocks the CreateChannel method.
1297- CreateChannelFunc func(ctx context.Context, channel models.Channel) error
1298-
1299- // CreateUserFunc mocks the CreateUser method.
1300- CreateUserFunc func(ctx context.Context, user models.User) error
1301-
1302- // DeleteUserFunc mocks the DeleteUser method.
1303- DeleteUserFunc func(ctx context.Context, username string) error
1304-
1305- // GetChannelSubscribersFunc mocks the GetChannelSubscribers method.
1306- GetChannelSubscribersFunc func(ctx context.Context, channelID string) ([]string, error)
1307-
1308 // GetNewVideosFunc mocks the GetNewVideos method.
1309- GetNewVideosFunc func(ctx context.Context, username string, sortDesc bool) ([]models.Video, error)
1310+ GetNewVideosFunc func(ctx context.Context, sortDesc bool) ([]models.Video, error)
1311
1312 // GetWatchedVideosFunc mocks the GetWatchedVideos method.
1313- GetWatchedVideosFunc func(ctx context.Context, username string) ([]models.Video, error)
1314+ GetWatchedVideosFunc func(ctx context.Context, sortDesc bool) ([]models.Video, error)
1315
1316 // ListChannelsFunc mocks the ListChannels method.
1317 ListChannelsFunc func(ctx context.Context) ([]models.Channel, error)
1318
1319 // SetVideoWatchTimeFunc mocks the SetVideoWatchTime method.
1320- SetVideoWatchTimeFunc func(ctx context.Context, username string, videoID string, watchTime *time.Time) error
1321+ SetVideoWatchTimeFunc func(ctx context.Context, videoID string, watchTime *time.Time) error
1322
1323- // SubscribeUserToChannelFunc mocks the SubscribeUserToChannel method.
1324- SubscribeUserToChannelFunc func(ctx context.Context, username string, channelID string) error
1325+ // SubscribeToChannelFunc mocks the SubscribeToChannel method.
1326+ SubscribeToChannelFunc func(ctx context.Context, channel models.Channel) error
1327
1328- // UnsubscribeUserFromChannelFunc mocks the UnsubscribeUserFromChannel method.
1329- UnsubscribeUserFromChannelFunc func(ctx context.Context, username string, channelID string) error
1330+ // UnsubscribeFromChannelFunc mocks the UnsubscribeFromChannel method.
1331+ UnsubscribeFromChannelFunc func(ctx context.Context, channelID string) error
1332
1333 // calls tracks calls to the methods.
1334 calls struct {
1335@@ -117,56 +81,10 @@ type DBMock struct {
1336 // ChannelID is the channelID argument value.
1337diff --git a/models/user.go b/models/user.go
1338deleted file mode 100644
1339index a476da2efac83bcd59137e276588b3c5fa518c8b..0000000000000000000000000000000000000000
1340--- a/models/user.go
1341+++ /dev/null
1342@@ -1,6 +0,0 @@
1343-package models
1344-
1345-type User struct {
1346- Username string `json:"username" db:"username"`
1347- Password string `json:"password" db:"password"`
1348-}