aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/database/database.go45
-rw-r--r--pkg/database/migrations/0001_initial.sql17
-rw-r--r--pkg/database/query/favourites.sql19
-rw-r--r--pkg/database/query/users.sql23
-rw-r--r--pkg/database/sqlc/db.go32
-rw-r--r--pkg/database/sqlc/favourites.sql.go134
-rw-r--r--pkg/database/sqlc/models.go22
-rw-r--r--pkg/database/sqlc/users.sql.go90
-rw-r--r--pkg/favourites/service.go96
-rw-r--r--pkg/schedule/service.go146
-rw-r--r--pkg/user/service.go121
11 files changed, 745 insertions, 0 deletions
diff --git a/pkg/database/database.go b/pkg/database/database.go
new file mode 100644
index 0000000..5570c95
--- /dev/null
+++ b/pkg/database/database.go
@@ -0,0 +1,45 @@
+package database
+
+import (
+ "context"
+ "embed"
+ "fmt"
+
+ "github.com/jackc/pgx/v5/pgxpool"
+ "github.com/jackc/pgx/v5/stdlib"
+ "github.com/pressly/goose/v3"
+)
+
+//go:embed migrations/*.sql
+var embedMigrations embed.FS
+
+func Connect(connString string) (*pgxpool.Pool, error) {
+ ctx := context.Background()
+
+ config, err := pgxpool.ParseConfig(connString)
+ if err != nil {
+ return nil, err
+ }
+
+ pool, err := pgxpool.NewWithConfig(ctx, config)
+ if err != nil {
+ return nil, err
+ }
+
+ return pool, nil
+}
+
+func Migrate(pool *pgxpool.Pool) error {
+ goose.SetBaseFS(embedMigrations)
+
+ if err := goose.SetDialect("postgres"); err != nil {
+ return fmt.Errorf("could not set dialect: %w", err)
+ }
+
+ db := stdlib.OpenDBFromPool(pool)
+ if err := goose.Up(db, "migrations"); err != nil {
+ return fmt.Errorf("could not apply migrations: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/database/migrations/0001_initial.sql b/pkg/database/migrations/0001_initial.sql
new file mode 100644
index 0000000..eea0a73
--- /dev/null
+++ b/pkg/database/migrations/0001_initial.sql
@@ -0,0 +1,17 @@
+-- +goose Up
+CREATE TABLE users (
+ id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ username varchar(20) UNIQUE NOT NULL,
+ password char(60) NOT NULL
+);
+
+CREATE TABLE favourites (
+ id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ user_id int NOT NULL,
+ event_guid uuid,
+ event_id int,
+ UNIQUE(user_id, event_guid, event_id),
+ CONSTRAINT chk_favourites CHECK (event_guid IS NOT NULL OR event_id IS NOT NULL),
+ FOREIGN KEY (user_id) REFERENCES users(id)
+);
+
diff --git a/pkg/database/query/favourites.sql b/pkg/database/query/favourites.sql
new file mode 100644
index 0000000..0661daa
--- /dev/null
+++ b/pkg/database/query/favourites.sql
@@ -0,0 +1,19 @@
+-- name: GetFavouritesForUser :many
+SELECT * FROM favourites
+WHERE user_id = $1;
+
+-- name: CreateFavourite :one
+INSERT INTO favourites (
+ user_id, event_guid, event_id
+) VALUES (
+ $1, $2, $3
+)
+RETURNING *;
+
+-- name: DeleteFavourite :exec
+DELETE FROM favourites
+WHERE id = $1;
+
+-- name: DeleteFavouriteByEventDetails :execrows
+DELETE FROM favourites
+WHERE (event_guid = $1 OR event_id = $2) AND user_id = $3; \ No newline at end of file
diff --git a/pkg/database/query/users.sql b/pkg/database/query/users.sql
new file mode 100644
index 0000000..c70ebbb
--- /dev/null
+++ b/pkg/database/query/users.sql
@@ -0,0 +1,23 @@
+-- name: GetUserByID :one
+SELECT * FROM users
+WHERE id = $1 LIMIT 1;
+
+-- name: GetUserByName :one
+SELECT * FROM users
+WHERE username = $1 LIMIT 1;
+
+-- name: ListUsers :many
+SELECT * FROM users
+ORDER BY username;
+
+-- name: CreateUser :one
+INSERT INTO users (
+ username, password
+) VALUES (
+ $1, $2
+)
+RETURNING *;
+
+-- name: DeleteUser :exec
+DELETE FROM users
+WHERE id = $1;
diff --git a/pkg/database/sqlc/db.go b/pkg/database/sqlc/db.go
new file mode 100644
index 0000000..b931bc5
--- /dev/null
+++ b/pkg/database/sqlc/db.go
@@ -0,0 +1,32 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+// sqlc v1.27.0
+
+package sqlc
+
+import (
+ "context"
+
+ "github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgconn"
+)
+
+type DBTX interface {
+ Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
+ Query(context.Context, string, ...interface{}) (pgx.Rows, error)
+ QueryRow(context.Context, string, ...interface{}) pgx.Row
+}
+
+func New(db DBTX) *Queries {
+ return &Queries{db: db}
+}
+
+type Queries struct {
+ db DBTX
+}
+
+func (q *Queries) WithTx(tx pgx.Tx) *Queries {
+ return &Queries{
+ db: tx,
+ }
+}
diff --git a/pkg/database/sqlc/favourites.sql.go b/pkg/database/sqlc/favourites.sql.go
new file mode 100644
index 0000000..3bf7c06
--- /dev/null
+++ b/pkg/database/sqlc/favourites.sql.go
@@ -0,0 +1,134 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+// sqlc v1.27.0
+// source: favourites.sql
+
+package sqlc
+
+import (
+ "context"
+
+ "github.com/jackc/pgx/v5/pgtype"
+)
+
+const createFavourite = `-- name: CreateFavourite :one
+INSERT INTO favourites (
+ user_id, event_guid, event_id
+) VALUES (
+ $1, $2, $3
+)
+RETURNING id, user_id, event_guid, event_id
+`
+
+type CreateFavouriteParams struct {
+ UserID int32 `json:"user_id"`
+ EventGuid pgtype.UUID `json:"event_guid"`
+ EventID pgtype.Int4 `json:"event_id"`
+}
+
+func (q *Queries) CreateFavourite(ctx context.Context, arg CreateFavouriteParams) (Favourite, error) {
+ row := q.db.QueryRow(ctx, createFavourite, arg.UserID, arg.EventGuid, arg.EventID)
+ var i Favourite
+ err := row.Scan(
+ &i.ID,
+ &i.UserID,
+ &i.EventGuid,
+ &i.EventID,
+ )
+ return i, err
+}
+
+const deleteFavourite = `-- name: DeleteFavourite :exec
+DELETE FROM favourites
+WHERE id = $1
+`
+
+func (q *Queries) DeleteFavourite(ctx context.Context, id int32) error {
+ _, err := q.db.Exec(ctx, deleteFavourite, id)
+ return err
+}
+
+const deleteFavouriteByEventDetails = `-- name: DeleteFavouriteByEventDetails :execrows
+DELETE FROM favourites
+WHERE (event_guid = $1 OR event_id = $2) AND user_id = $3
+`
+
+type DeleteFavouriteByEventDetailsParams struct {
+ EventGuid pgtype.UUID `json:"event_guid"`
+ EventID pgtype.Int4 `json:"event_id"`
+ UserID int32 `json:"user_id"`
+}
+
+func (q *Queries) DeleteFavouriteByEventDetails(ctx context.Context, arg DeleteFavouriteByEventDetailsParams) (int64, error) {
+ result, err := q.db.Exec(ctx, deleteFavouriteByEventDetails, arg.EventGuid, arg.EventID, arg.UserID)
+ if err != nil {
+ return 0, err
+ }
+ return result.RowsAffected(), nil
+}
+
+const deleteFavouriteByEventGuid = `-- name: DeleteFavouriteByEventGuid :execrows
+DELETE FROM favourites
+WHERE event_guid = $1 AND user_id = $2
+`
+
+type DeleteFavouriteByEventGuidParams struct {
+ EventGuid pgtype.UUID `json:"event_guid"`
+ UserID int32 `json:"user_id"`
+}
+
+func (q *Queries) DeleteFavouriteByEventGuid(ctx context.Context, arg DeleteFavouriteByEventGuidParams) (int64, error) {
+ result, err := q.db.Exec(ctx, deleteFavouriteByEventGuid, arg.EventGuid, arg.UserID)
+ if err != nil {
+ return 0, err
+ }
+ return result.RowsAffected(), nil
+}
+
+const deleteFavouriteByEventId = `-- name: DeleteFavouriteByEventId :execrows
+DELETE FROM favourites
+WHERE event_id = $1 AND user_id = $2
+`
+
+type DeleteFavouriteByEventIdParams struct {
+ EventID pgtype.Int4 `json:"event_id"`
+ UserID int32 `json:"user_id"`
+}
+
+func (q *Queries) DeleteFavouriteByEventId(ctx context.Context, arg DeleteFavouriteByEventIdParams) (int64, error) {
+ result, err := q.db.Exec(ctx, deleteFavouriteByEventId, arg.EventID, arg.UserID)
+ if err != nil {
+ return 0, err
+ }
+ return result.RowsAffected(), nil
+}
+
+const getFavouritesForUser = `-- name: GetFavouritesForUser :many
+SELECT id, user_id, event_guid, event_id FROM favourites
+WHERE user_id = $1
+`
+
+func (q *Queries) GetFavouritesForUser(ctx context.Context, userID int32) ([]Favourite, error) {
+ rows, err := q.db.Query(ctx, getFavouritesForUser, userID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Favourite
+ for rows.Next() {
+ var i Favourite
+ if err := rows.Scan(
+ &i.ID,
+ &i.UserID,
+ &i.EventGuid,
+ &i.EventID,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
diff --git a/pkg/database/sqlc/models.go b/pkg/database/sqlc/models.go
new file mode 100644
index 0000000..09208aa
--- /dev/null
+++ b/pkg/database/sqlc/models.go
@@ -0,0 +1,22 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+// sqlc v1.27.0
+
+package sqlc
+
+import (
+ "github.com/jackc/pgx/v5/pgtype"
+)
+
+type Favourite struct {
+ ID int32 `json:"id"`
+ UserID int32 `json:"user_id"`
+ EventGuid pgtype.UUID `json:"event_guid"`
+ EventID pgtype.Int4 `json:"event_id"`
+}
+
+type User struct {
+ ID int32 `json:"id"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+}
diff --git a/pkg/database/sqlc/users.sql.go b/pkg/database/sqlc/users.sql.go
new file mode 100644
index 0000000..dfd2c2f
--- /dev/null
+++ b/pkg/database/sqlc/users.sql.go
@@ -0,0 +1,90 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+// sqlc v1.27.0
+// source: users.sql
+
+package sqlc
+
+import (
+ "context"
+)
+
+const createUser = `-- name: CreateUser :one
+INSERT INTO users (
+ username, password
+) VALUES (
+ $1, $2
+)
+RETURNING id, username, password
+`
+
+type CreateUserParams struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+}
+
+func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
+ row := q.db.QueryRow(ctx, createUser, arg.Username, arg.Password)
+ var i User
+ err := row.Scan(&i.ID, &i.Username, &i.Password)
+ return i, err
+}
+
+const deleteUser = `-- name: DeleteUser :exec
+DELETE FROM users
+WHERE id = $1
+`
+
+func (q *Queries) DeleteUser(ctx context.Context, id int32) error {
+ _, err := q.db.Exec(ctx, deleteUser, id)
+ return err
+}
+
+const getUserByID = `-- name: GetUserByID :one
+SELECT id, username, password FROM users
+WHERE id = $1 LIMIT 1
+`
+
+func (q *Queries) GetUserByID(ctx context.Context, id int32) (User, error) {
+ row := q.db.QueryRow(ctx, getUserByID, id)
+ var i User
+ err := row.Scan(&i.ID, &i.Username, &i.Password)
+ return i, err
+}
+
+const getUserByName = `-- name: GetUserByName :one
+SELECT id, username, password FROM users
+WHERE username = $1 LIMIT 1
+`
+
+func (q *Queries) GetUserByName(ctx context.Context, username string) (User, error) {
+ row := q.db.QueryRow(ctx, getUserByName, username)
+ var i User
+ err := row.Scan(&i.ID, &i.Username, &i.Password)
+ return i, err
+}
+
+const listUsers = `-- name: ListUsers :many
+SELECT id, username, password FROM users
+ORDER BY username
+`
+
+func (q *Queries) ListUsers(ctx context.Context) ([]User, error) {
+ rows, err := q.db.Query(ctx, listUsers)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []User
+ for rows.Next() {
+ var i User
+ if err := rows.Scan(&i.ID, &i.Username, &i.Password); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
diff --git a/pkg/favourites/service.go b/pkg/favourites/service.go
new file mode 100644
index 0000000..cba249e
--- /dev/null
+++ b/pkg/favourites/service.go
@@ -0,0 +1,96 @@
+package favourites
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/LMBishop/confplanner/pkg/database/sqlc"
+ "github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgtype"
+ "github.com/jackc/pgx/v5/pgxpool"
+)
+
+type Service interface {
+ GetFavouritesForUser(id int32) (*[]sqlc.Favourite, error)
+ CreateFavouriteForUser(id int32, eventGuid pgtype.UUID, eventId *int32) (*sqlc.Favourite, error)
+ DeleteFavouriteForUserByEventDetails(id int32, eventGuid pgtype.UUID, eventId *int32) error
+}
+
+var (
+ ErrImproperType = errors.New("improper type")
+ ErrNotFound = errors.New("not found")
+)
+
+type service struct {
+ pool *pgxpool.Pool
+}
+
+func NewService(pool *pgxpool.Pool) Service {
+ return &service{
+ pool: pool,
+ }
+}
+
+func (s *service) CreateFavouriteForUser(id int32, eventGuid pgtype.UUID, eventId *int32) (*sqlc.Favourite, error) {
+ queries := sqlc.New(s.pool)
+
+ var pgEventId pgtype.Int4
+ if eventId != nil {
+ pgEventId = pgtype.Int4{
+ Int32: *eventId,
+ Valid: true,
+ }
+ }
+
+ favourite, err := queries.CreateFavourite(context.Background(), sqlc.CreateFavouriteParams{
+ UserID: id,
+ EventGuid: eventGuid,
+ EventID: pgEventId,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("could not create favourite: %w", err)
+ }
+
+ return &favourite, nil
+}
+
+func (s *service) GetFavouritesForUser(id int32) (*[]sqlc.Favourite, error) {
+ queries := sqlc.New(s.pool)
+
+ favourites, err := queries.GetFavouritesForUser(context.Background(), id)
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ empty := make([]sqlc.Favourite, 0)
+ return &empty, nil
+ }
+ return nil, fmt.Errorf("could not fetch user: %w", err)
+ }
+
+ return &favourites, nil
+}
+
+func (s *service) DeleteFavouriteForUserByEventDetails(id int32, eventGuid pgtype.UUID, eventId *int32) error {
+ queries := sqlc.New(s.pool)
+
+ var pgEventId pgtype.Int4
+ if eventId != nil {
+ pgEventId = pgtype.Int4{
+ Int32: *eventId,
+ Valid: true,
+ }
+ }
+ rowsAffected, err := queries.DeleteFavouriteByEventDetails(context.Background(), sqlc.DeleteFavouriteByEventDetailsParams{
+ EventGuid: eventGuid,
+ EventID: pgEventId,
+ UserID: id,
+ })
+ if err != nil {
+ return fmt.Errorf("could not delete favourite: %w", err)
+ }
+ if rowsAffected == 0 {
+ return ErrNotFound
+ }
+
+ return nil
+}
diff --git a/pkg/schedule/service.go b/pkg/schedule/service.go
new file mode 100644
index 0000000..70587ef
--- /dev/null
+++ b/pkg/schedule/service.go
@@ -0,0 +1,146 @@
+package schedule
+
+import (
+ "bufio"
+ "encoding/xml"
+ "fmt"
+ "net/http"
+ "sync"
+ "time"
+)
+
+type Service interface {
+ GetSchedule() (*Schedule, *time.Time, error)
+}
+
+type Schedule struct {
+ XMLName xml.Name `json:"-" xml:"schedule"`
+ Conference Conference `json:"conference" xml:"conference"`
+ Tracks []Track `json:"tracks" xml:"tracks>track"`
+ Days []Day `json:"days" xml:"day"`
+}
+
+type Conference struct {
+ Title string `json:"title" xml:"title"`
+ Venue string `json:"venue" xml:"venue"`
+ City string `json:"city" xml:"city"`
+ Start string `json:"start" xml:"start"`
+ End string `json:"end" xml:"end"`
+ Days int `json:"days" xml:"days"`
+ DayChange string `json:"dayChange" xml:"day_change"`
+ TimeslotDuration string `json:"timeslotDuration" xml:"timeslot_duration"`
+ BaseURL string `json:"baseUrl" xml:"base_url"`
+ TimeZoneName string `json:"timeZoneName" xml:"time_zone_name"`
+}
+
+type Track struct {
+ Name string `json:"name" xml:",chardata"`
+}
+
+type Day struct {
+ Date string `json:"date" xml:"date,attr"`
+ Start string `json:"start" xml:"start"`
+ End string `json:"end" xml:"end"`
+ Rooms []Room `json:"rooms" xml:"room"`
+}
+
+type Room struct {
+ Name string `json:"name" xml:"name,attr"`
+ Events []Event `json:"events" xml:"event"`
+}
+
+type Event struct {
+ ID int `json:"id" xml:"id,attr"`
+ GUID string `json:"guid" xml:"guid,attr"`
+ Date string `json:"date" xml:"date"`
+ Start string `json:"start" xml:"start"`
+ Duration string `json:"duration" xml:"duration"`
+ Room string `json:"room" xml:"room"`
+ URL string `json:"url" xml:"url"`
+ Track string `json:"track" xml:"track"`
+ Type string `json:"type" xml:"type"`
+ Title string `json:"title" xml:"title"`
+ Abstract string `json:"abstract" xml:"abstract"`
+ Persons []Person `json:"persons" xml:"persons>person"`
+ Attachments []Attachment `json:"attachments" xml:"attachments>attachment"`
+ Links []Link `json:"links" xml:"links>link"`
+}
+
+type Person struct {
+ ID int `json:"id" xml:"id,attr"`
+ Name string `json:"name" xml:",chardata"`
+}
+
+type Attachment struct {
+ Type string `json:"string" xml:"id,attr"`
+ Href string `json:"href" xml:"href,attr"`
+ Name string `json:"name" xml:",chardata"`
+}
+
+type Link struct {
+ Href string `json:"href" xml:"href,attr"`
+ Name string `json:"name" xml:",chardata"`
+}
+
+type service struct {
+ schedule *Schedule
+ pentabarfUrl string
+ lastUpdated time.Time
+ lock sync.Mutex
+}
+
+func NewService(pentabarfUrl string) (Service, error) {
+ service := &service{
+ pentabarfUrl: pentabarfUrl,
+ lastUpdated: time.Unix(0, 0),
+ }
+
+ err := service.updateSchedule()
+ if err != nil {
+ return nil, fmt.Errorf("could not read schedule from '%s' (is it a valid pentabarf XML file?): %w", pentabarfUrl, err)
+ }
+ return service, nil
+}
+
+func (s *service) GetSchedule() (*Schedule, *time.Time, error) {
+ if s.hasScheduleExpired() {
+ err := s.updateSchedule()
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ return s.schedule, &s.lastUpdated, nil
+}
+
+func (s *service) hasScheduleExpired() bool {
+ expire := s.lastUpdated.Add(15 * time.Minute)
+ return time.Now().After(expire)
+}
+
+func (s *service) updateSchedule() error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if !s.hasScheduleExpired() {
+ return nil
+ }
+
+ res, err := http.Get(s.pentabarfUrl)
+ if err != nil {
+ return err
+ }
+
+ reader := bufio.NewReader(res.Body)
+
+ var schedule Schedule
+
+ decoder := xml.NewDecoder(reader)
+ if err := decoder.Decode(&schedule); err != nil {
+ return fmt.Errorf("failed to decode XML: %w", err)
+ }
+
+ s.schedule = &schedule
+ s.lastUpdated = time.Now()
+ return nil
+}
diff --git a/pkg/user/service.go b/pkg/user/service.go
new file mode 100644
index 0000000..7784811
--- /dev/null
+++ b/pkg/user/service.go
@@ -0,0 +1,121 @@
+package user
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/LMBishop/confplanner/pkg/database/sqlc"
+ "github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgconn"
+ "github.com/jackc/pgx/v5/pgxpool"
+ "golang.org/x/crypto/bcrypt"
+)
+
+type Service interface {
+ CreateUser(username string, password string) (*sqlc.User, error)
+ GetUserByName(username string) (*sqlc.User, error)
+ GetUserByID(id int32) (*sqlc.User, error)
+ Authenticate(username string, password string) (*sqlc.User, error)
+}
+
+var (
+ ErrUserExists = errors.New("user already exists")
+ ErrUserNotFound = errors.New("user not found")
+ ErrNotAcceptingRegistrations = errors.New("not currently accepting registrations")
+)
+
+type service struct {
+ pool *pgxpool.Pool
+ acceptingRegistrations bool
+}
+
+func NewService(pool *pgxpool.Pool, acceptingRegistrations bool) Service {
+ return &service{
+ pool: pool,
+ acceptingRegistrations: acceptingRegistrations,
+ }
+}
+
+func (s *service) CreateUser(username string, password string) (*sqlc.User, error) {
+ if !s.acceptingRegistrations {
+ return nil, ErrNotAcceptingRegistrations
+ }
+
+ queries := sqlc.New(s.pool)
+
+ var passwordBytes = []byte(password)
+
+ hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
+ if err != nil {
+ return nil, fmt.Errorf("could not hash password: %w", err)
+ }
+
+ user, err := queries.CreateUser(context.Background(), sqlc.CreateUserParams{
+ Username: strings.ToLower(username),
+ Password: string(hash),
+ })
+ if err != nil {
+ var pgErr *pgconn.PgError
+ if errors.As(err, &pgErr) && pgErr.Code == "23505" {
+ return nil, ErrUserExists
+ }
+ return nil, fmt.Errorf("could not create user: %w", err)
+ }
+
+ return &user, nil
+}
+
+func (s *service) GetUserByName(username string) (*sqlc.User, error) {
+ queries := sqlc.New(s.pool)
+
+ user, err := queries.GetUserByName(context.Background(), username)
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return nil, ErrUserNotFound
+ }
+ return nil, fmt.Errorf("could not fetch user: %w", err)
+ }
+
+ return &user, nil
+}
+
+func (s *service) GetUserByID(id int32) (*sqlc.User, error) {
+ queries := sqlc.New(s.pool)
+
+ user, err := queries.GetUserByID(context.Background(), id)
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return nil, ErrUserNotFound
+ }
+ return nil, fmt.Errorf("could not fetch user: %w", err)
+ }
+
+ return &user, nil
+}
+
+func (s *service) Authenticate(username string, password string) (*sqlc.User, error) {
+ random, err := bcrypt.GenerateFromPassword([]byte("00000000"), bcrypt.DefaultCost)
+ if err != nil {
+ return nil, err
+ }
+
+ user, err := s.GetUserByName(username)
+ if err != nil {
+ if errors.Is(err, ErrUserNotFound) {
+ bcrypt.CompareHashAndPassword(random, []byte(password))
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
+ if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return user, nil
+}