initial commit

This commit is contained in:
Daniel Hjartland
2024-11-26 18:08:49 +01:00
commit 75be79c6cf
26 changed files with 1885 additions and 0 deletions

116
internal/auth/auth.go Normal file
View File

@@ -0,0 +1,116 @@
package auth
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
type TokenType string
const (
TokenTypeAccess TokenType = "tubely-access"
)
var ErrNoAuthHeaderIncluded = errors.New("no auth header included in request")
func HashPassword(password string) (string, error) {
dat, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(dat), nil
}
func CheckPasswordHash(password, hash string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
func MakeJWT(
userID uuid.UUID,
tokenSecret string,
expiresIn time.Duration,
) (string, error) {
signingKey := []byte(tokenSecret)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
Issuer: string(TokenTypeAccess),
IssuedAt: jwt.NewNumericDate(time.Now().UTC()),
ExpiresAt: jwt.NewNumericDate(time.Now().UTC().Add(expiresIn)),
Subject: userID.String(),
})
return token.SignedString(signingKey)
}
func ValidateJWT(tokenString, tokenSecret string) (uuid.UUID, error) {
claimsStruct := jwt.RegisteredClaims{}
token, err := jwt.ParseWithClaims(
tokenString,
&claimsStruct,
func(token *jwt.Token) (interface{}, error) { return []byte(tokenSecret), nil },
)
if err != nil {
return uuid.Nil, err
}
userIDString, err := token.Claims.GetSubject()
if err != nil {
return uuid.Nil, err
}
issuer, err := token.Claims.GetIssuer()
if err != nil {
return uuid.Nil, err
}
if issuer != string(TokenTypeAccess) {
return uuid.Nil, errors.New("invalid issuer")
}
id, err := uuid.Parse(userIDString)
if err != nil {
return uuid.Nil, fmt.Errorf("invalid user ID: %w", err)
}
return id, nil
}
func GetBearerToken(headers http.Header) (string, error) {
authHeader := headers.Get("Authorization")
if authHeader == "" {
return "", ErrNoAuthHeaderIncluded
}
splitAuth := strings.Split(authHeader, " ")
if len(splitAuth) < 2 || splitAuth[0] != "Bearer" {
return "", errors.New("malformed authorization header")
}
return splitAuth[1], nil
}
func MakeRefreshToken() (string, error) {
token := make([]byte, 32)
_, err := rand.Read(token)
if err != nil {
return "", err
}
return hex.EncodeToString(token), nil
}
func GetAPIKey(headers http.Header) (string, error) {
authHeader := headers.Get("Authorization")
if authHeader == "" {
return "", ErrNoAuthHeaderIncluded
}
splitAuth := strings.Split(authHeader, " ")
if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" {
return "", errors.New("malformed authorization header")
}
return splitAuth[1], nil
}

View File

@@ -0,0 +1,89 @@
package database
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
type Client struct {
db *sql.DB
}
func NewClient(pathToDB string) (Client, error) {
db, err := sql.Open("sqlite3", pathToDB)
if err != nil {
return Client{}, err
}
c := Client{db}
err = c.autoMigrate()
if err != nil {
return Client{}, err
}
return c, nil
}
func (c *Client) autoMigrate() error {
userTable := `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
password TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
`
_, err := c.db.Exec(userTable)
if err != nil {
return err
}
refreshTokenTable := `
CREATE TABLE IF NOT EXISTS refresh_tokens (
token TEXT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
revoked_at TIMESTAMP,
user_id TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);
`
_, err = c.db.Exec(refreshTokenTable)
if err != nil {
return err
}
videoTable := `
CREATE TABLE IF NOT EXISTS videos (
id TEXT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
title TEXT NOT NULL,
description TEXT,
thumbnail_url TEXT,
video_url TEXT TEXT,
user_id INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id)
);
`
_, err = c.db.Exec(videoTable)
if err != nil {
return err
}
return nil
}
func (c Client) Reset() error {
if _, err := c.db.Exec("DELETE FROM refresh_tokens"); err != nil {
return fmt.Errorf("failed to reset table refresh_tokens: %w", err)
}
if _, err := c.db.Exec("DELETE FROM users"); err != nil {
return fmt.Errorf("failed to reset table users: %w", err)
}
if _, err := c.db.Exec("DELETE FROM videos"); err != nil {
return fmt.Errorf("failed to reset table videos: %w", err)
}
return nil
}

View File

@@ -0,0 +1,83 @@
package database
import (
"database/sql"
"time"
"github.com/google/uuid"
)
type RefreshToken struct {
CreateRefreshTokenParams
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
RevokedAt *time.Time `json:"revoked_at"`
}
type CreateRefreshTokenParams struct {
Token string `json:"token"`
UserID uuid.UUID `json:"user_id"`
ExpiresAt time.Time `json:"expires_at"`
}
func (c Client) CreateRefreshToken(params CreateRefreshTokenParams) (RefreshToken, error) {
query := `
INSERT INTO refresh_tokens (
token,
created_at,
updated_at,
user_id,
expires_at
) VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?, ?)
`
_, err := c.db.Exec(query, params.Token, params.UserID.String(), params.ExpiresAt)
if err != nil {
return RefreshToken{}, err
}
return c.GetRefreshToken(params.Token)
}
func (c Client) RevokeRefreshToken(token string) error {
query := `
UPDATE refresh_tokens
SET revoked_at = CURRENT_TIMESTAMP
WHERE token = ?
`
_, err := c.db.Exec(query, token)
return err
}
func (c Client) GetRefreshToken(token string) (RefreshToken, error) {
query := `
SELECT token, created_at, updated_at, user_id, expires_at, revoked_at
FROM refresh_tokens
WHERE token = ?
`
var rt RefreshToken
var userID string
err := c.db.QueryRow(query, token).
Scan(&rt.Token, &rt.CreatedAt, &rt.UpdatedAt, &userID, &rt.ExpiresAt, &rt.RevokedAt)
if err != nil {
if err == sql.ErrNoRows {
return RefreshToken{}, nil
}
return RefreshToken{}, err
}
rt.UserID, err = uuid.Parse(userID)
if err != nil {
return RefreshToken{}, err
}
return rt, nil
}
func (c Client) DeleteRefreshToken(token string) error {
query := `
DELETE FROM refresh_tokens
WHERE token = ?
`
_, err := c.db.Exec(query, token)
return err
}

147
internal/database/users.go Normal file
View File

@@ -0,0 +1,147 @@
package database
import (
"database/sql"
"errors"
"time"
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreateUserParams
}
type CreateUserParams struct {
Email string `json:"email"`
Password string `json:"password"`
}
func (c Client) GetUsers() ([]User, error) {
query := `
SELECT
id,
email
FROM users
`
rows, err := c.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
users := []User{}
for rows.Next() {
var user User
var id string
if err := rows.Scan(&id, &user.Email); err != nil {
return nil, err
}
user.ID, err = uuid.Parse(id)
if err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
func (c Client) GetUserByEmail(email string) (User, error) {
query := `
SELECT id, created_at, updated_at, email, password
FROM users
WHERE email = ?
`
var user User
var id string
err := c.db.QueryRow(query, email).Scan(&id, &user.CreatedAt, &user.UpdatedAt, &user.Email, &user.Password)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return User{}, nil
}
return User{}, err
}
user.ID, err = uuid.Parse(id)
if err != nil {
return User{}, err
}
return user, nil
}
func (c Client) GetUserByRefreshToken(token string) (*User, error) {
query := `
SELECT u.id, u.email, u.created_at, u.updated_at, u.password
FROM users u
JOIN refresh_tokens rt ON u.id = rt.user_id
WHERE rt.token = ?
`
var user User
var id string
err := c.db.QueryRow(query, token).Scan(&id, &user.Email, &user.CreatedAt, &user.UpdatedAt, &user.Password)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
user.ID, err = uuid.Parse(id)
if err != nil {
return nil, err
}
return &user, nil
}
func (c Client) CreateUser(params CreateUserParams) (*User, error) {
id := uuid.New()
query := `
INSERT INTO users
(id, created_at, updated_at, email, password)
VALUES
(?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?, ?)
`
_, err := c.db.Exec(query, id.String(), params.Email, params.Password)
if err != nil {
return nil, err
}
return c.GetUser(id)
}
func (c Client) GetUser(id uuid.UUID) (*User, error) {
query := `
SELECT id, created_at, updated_at, email, password
FROM users
WHERE id = ?
`
var user User
var idStr string
err := c.db.QueryRow(query, id.String()).Scan(&idStr, &user.CreatedAt, &user.UpdatedAt, &user.Email, &user.Password)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
user.ID, err = uuid.Parse(idStr)
if err != nil {
return nil, err
}
return &user, nil
}
func (c Client) DeleteUser(id uuid.UUID) error {
query := `
DELETE FROM users
WHERE id = ?
`
_, err := c.db.Exec(query, id.String())
return err
}

155
internal/database/videos.go Normal file
View File

@@ -0,0 +1,155 @@
package database
import (
"database/sql"
"errors"
"time"
"github.com/google/uuid"
)
type Video struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ThumbnailURL *string `json:"thumbnail_url"`
VideoURL *string `json:"video_url"`
CreateVideoParams
}
type CreateVideoParams struct {
Title string `json:"title"`
Description string `json:"description"`
UserID uuid.UUID `json:"user_id"`
}
func (c Client) GetVideos(userID uuid.UUID) ([]Video, error) {
query := `
SELECT
id,
created_at,
updated_at,
title,
description,
thumbnail_url,
video_url,
user_id
FROM videos
WHERE user_id = ?
ORDER BY created_at DESC
`
rows, err := c.db.Query(query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
videos := []Video{}
for rows.Next() {
var video Video
if err := rows.Scan(
&video.ID,
&video.CreatedAt,
&video.UpdatedAt,
&video.Title,
&video.Description,
&video.ThumbnailURL,
&video.VideoURL,
&video.UserID,
); err != nil {
return nil, err
}
videos = append(videos, video)
}
return videos, nil
}
func (c Client) CreateVideo(params CreateVideoParams) (Video, error) {
id := uuid.New()
query := `
INSERT INTO videos (
id,
created_at,
updated_at,
title,
description,
user_id
) VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?, ?, ?)
`
_, err := c.db.Exec(query, id, params.Title, params.Description, params.UserID)
if err != nil {
return Video{}, err
}
return c.GetVideo(id)
}
func (c Client) GetVideo(id uuid.UUID) (Video, error) {
query := `
SELECT
id,
created_at,
updated_at,
title,
description,
thumbnail_url,
video_url,
user_id
FROM videos
WHERE id = ?
`
var video Video
err := c.db.QueryRow(query, id).Scan(
&video.ID,
&video.CreatedAt,
&video.UpdatedAt,
&video.Title,
&video.Description,
&video.ThumbnailURL,
&video.VideoURL,
&video.UserID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return Video{}, nil
}
return Video{}, err
}
return video, nil
}
func (c Client) UpdateVideo(video Video) error {
query := `
UPDATE videos
SET
title = ?,
description = ?,
thumbnail_url = ?,
video_url = ?,
user_id = ?
WHERE id = ?
`
_, err := c.db.Exec(
query,
video.Title,
video.Description,
&video.ThumbnailURL,
&video.VideoURL,
video.UserID,
video.ID,
)
return err
}
func (c Client) DeleteVideo(id uuid.UUID) error {
query := `
DELETE FROM videos
WHERE id = ?
`
_, err := c.db.Exec(query, id)
return err
}