From 27b2f95a7be14b02353b277906c3bc4d98a9b297 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Mon, 23 Nov 2020 19:39:44 +0100
Subject: [PATCH] Start on migrating database integration to gorm

---
 api/.vscode/launch.json              |   4 +-
 api/database/mysql.go                |  88 ++++++---------
 api/go.mod                           |   2 +
 api/go.sum                           |   9 ++
 api/graphql/auth/auth.go             |   6 +-
 api/graphql/directive.go             |   4 +-
 api/graphql/models/site_info.go      |  75 +++++++------
 api/graphql/models/user.go           | 159 +++++++++++++++------------
 api/graphql/resolvers/root.go        |   4 +-
 api/graphql/resolvers/share_token.go |   3 +-
 api/routes/authenticate_media.go     |   3 +-
 api/routes/photos.go                 |   4 +-
 api/routes/videos.go                 |   4 +-
 api/scanner/cleanup_media.go         |   5 +-
 api/scanner/periodic_scanner.go      |   9 +-
 api/scanner/queue.go                 |  21 ++--
 api/scanner/scanner_album.go         |   6 +-
 api/scanner/scanner_user.go          |   4 +-
 api/server.go                        |   1 -
 19 files changed, 214 insertions(+), 197 deletions(-)

diff --git a/api/.vscode/launch.json b/api/.vscode/launch.json
index a4b3b9f..55263cc 100644
--- a/api/.vscode/launch.json
+++ b/api/.vscode/launch.json
@@ -11,7 +11,9 @@
       "mode": "auto",
       "program": "${workspaceRoot}/server.go",
       "cwd": "${workspaceRoot}",
-      "env": {},
+      "env": {
+        "PATH": "${env:PATH}:/Applications/darktable.app/Contents/MacOS/"
+      },
       "args": []
     }
   ]
diff --git a/api/database/mysql.go b/api/database/mysql.go
index c5d1912..27b305b 100644
--- a/api/database/mysql.go
+++ b/api/database/mysql.go
@@ -1,27 +1,19 @@
 package database
 
 import (
-	"context"
-	"database/sql"
 	"log"
 	"net/url"
 	"os"
-	"time"
 
 	"github.com/pkg/errors"
+	"github.com/viktorstrate/photoview/api/graphql/models"
 
-	// Load mysql driver
-	_ "github.com/go-sql-driver/mysql"
-
-	"github.com/golang-migrate/migrate"
-	"github.com/golang-migrate/migrate/database/mysql"
-
-	// Migrate from file
-	_ "github.com/golang-migrate/migrate/source/file"
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
 )
 
 // SetupDatabase connects to the database using environment variables
-func SetupDatabase() (*sql.DB, error) {
+func SetupDatabase() (*gorm.DB, error) {
 
 	address, err := url.Parse(os.Getenv("MYSQL_URL"))
 	if err != nil {
@@ -40,62 +32,46 @@ func SetupDatabase() (*sql.DB, error) {
 
 	log.Printf("Connecting to database: %s", address)
 
-	var db *sql.DB
-
-	db, err = sql.Open("mysql", address.String())
+	db, err := gorm.Open(mysql.Open(address.String()), &gorm.Config{})
 	if err != nil {
-		return nil, errors.New("Could not connect to database, exiting")
+		return nil, errors.Wrap(err, "could not connect to database")
 	}
 
-	tryCount := 0
+	// var db *sql.DB
 
-	for {
-		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-		defer cancel()
+	// db, err = sql.Open("mysql", address.String())
+	// if err != nil {
+	// 	return nil, errors.New("Could not connect to database, exiting")
+	// }
 
-		if err := db.PingContext(ctx); err != nil {
-			if tryCount < 4 {
-				tryCount++
-				log.Printf("WARN: Could not ping database: %s, Will retry after 1 second", err)
-				time.Sleep(time.Second)
-				continue
-			} else {
-				return nil, errors.Wrap(err, "Could not ping database, exiting")
-			}
-		}
+	// tryCount := 0
 
-		break
-	}
+	// for {
+	// 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	// 	defer cancel()
 
-	db.SetMaxOpenConns(80)
+	// 	if err := db.PingContext(ctx); err != nil {
+	// 		if tryCount < 4 {
+	// 			tryCount++
+	// 			log.Printf("WARN: Could not ping database: %s, Will retry after 1 second", err)
+	// 			time.Sleep(time.Second)
+	// 			continue
+	// 		} else {
+	// 			return nil, errors.Wrap(err, "Could not ping database, exiting")
+	// 		}
+	// 	}
+
+	// 	break
+	// }
+
+	// db.SetMaxOpenConns(80)
 
 	return db, nil
 }
 
-func MigrateDatabase(db *sql.DB) error {
-	driver, err := mysql.WithInstance(db, &mysql.Config{})
-	if err != nil {
-		return err
-	}
+func MigrateDatabase(db *gorm.DB) error {
 
-	m, err := migrate.NewWithDatabaseInstance(
-		"file://database/migrations",
-		"mysql",
-		driver,
-	)
-	if err != nil {
-		return err
-	}
-
-	if err := m.Up(); err != nil {
-		if err.Error() == "no change" {
-			log.Println("Database is up to date")
-		} else {
-			return err
-		}
-	} else {
-		log.Println("Database migrated")
-	}
+	db.AutoMigrate(&models.User{}, &models.AccessToken{})
 
 	return nil
 }
diff --git a/api/go.mod b/api/go.mod
index 055143d..478dfc7 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -31,4 +31,6 @@ require (
 	golang.org/x/sys v0.0.0-20201112073958-5cba982894dd // indirect
 	gopkg.in/vansante/go-ffprobe.v2 v2.0.2
 	gopkg.in/yaml.v2 v2.3.0 // indirect
+	gorm.io/driver/mysql v1.0.3
+	gorm.io/gorm v1.20.7
 )
diff --git a/api/go.sum b/api/go.sum
index 70d5d43..1d6aca3 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -58,6 +58,10 @@ github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCO
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
+github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -176,5 +180,10 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg=
+gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI=
+gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
+gorm.io/gorm v1.20.7 h1:rMS4CL3pNmYq1V5/X+nHHjh1Dx6dnf27+Cai5zabo+M=
+gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
 sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
diff --git a/api/graphql/auth/auth.go b/api/graphql/auth/auth.go
index 6af833e..5f1d4cb 100644
--- a/api/graphql/auth/auth.go
+++ b/api/graphql/auth/auth.go
@@ -2,7 +2,6 @@ package auth
 
 import (
 	"context"
-	"database/sql"
 	"errors"
 	"log"
 	"net/http"
@@ -10,6 +9,7 @@ import (
 
 	"github.com/99designs/gqlgen/handler"
 	"github.com/viktorstrate/photoview/api/graphql/models"
+	"gorm.io/gorm"
 )
 
 var ErrUnauthorized = errors.New("unauthorized")
@@ -23,7 +23,7 @@ type contextKey struct {
 }
 
 // Middleware decodes the share session cookie and packs the session into context
-func Middleware(db *sql.DB) func(http.Handler) http.Handler {
+func Middleware(db *gorm.DB) func(http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 
@@ -66,7 +66,7 @@ func UserFromContext(ctx context.Context) *models.User {
 	return raw
 }
 
-func AuthWebsocketInit(db *sql.DB) func(context.Context, handler.InitPayload) (context.Context, error) {
+func AuthWebsocketInit(db *gorm.DB) func(context.Context, handler.InitPayload) (context.Context, error) {
 	return func(ctx context.Context, initPayload handler.InitPayload) (context.Context, error) {
 
 		bearer, exists := initPayload["Authorization"].(string)
diff --git a/api/graphql/directive.go b/api/graphql/directive.go
index b617f6d..2e6d831 100644
--- a/api/graphql/directive.go
+++ b/api/graphql/directive.go
@@ -2,14 +2,14 @@ package api
 
 import (
 	"context"
-	"database/sql"
 	"errors"
 
 	"github.com/99designs/gqlgen/graphql"
 	"github.com/viktorstrate/photoview/api/graphql/auth"
+	"gorm.io/gorm"
 )
 
-func IsAdmin(database *sql.DB) func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
+func IsAdmin(database *gorm.DB) func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
 	return func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
 
 		user := auth.UserFromContext(ctx)
diff --git a/api/graphql/models/site_info.go b/api/graphql/models/site_info.go
index dc36bfb..53041cd 100644
--- a/api/graphql/models/site_info.go
+++ b/api/graphql/models/site_info.go
@@ -1,48 +1,59 @@
 package models
 
 import (
-	"database/sql"
-
 	"github.com/pkg/errors"
+	"gorm.io/gorm"
 )
 
-func initializeSiteInfoRow(db *sql.DB) (*SiteInfo, error) {
-	_, err := db.Exec("INSERT INTO site_info (initial_setup, periodic_scan_interval, concurrent_workers) VALUES (true, 0, 3)")
-	if err != nil {
-		return nil, errors.Wrap(err, "initialize site_info row")
-	}
+// func initializeSiteInfoRow(db *gorm.DB) (*SiteInfo, error) {
+// 	_, err := db.Exec("INSERT INTO site_info (initial_setup, periodic_scan_interval, concurrent_workers) VALUES (true, 0, 3)")
+// 	if err != nil {
+// 		return nil, errors.Wrap(err, "initialize site_info row")
+// 	}
 
-	siteInfo := &SiteInfo{}
+// 	siteInfo := &SiteInfo{}
 
-	row := db.QueryRow("SELECT * FROM site_info")
-	if err := row.Scan(&siteInfo.InitialSetup, &siteInfo.PeriodicScanInterval, &siteInfo.ConcurrentWorkers); err != nil {
-		return nil, errors.Wrap(err, "get site_info row after initialization")
-	}
+// 	row := db.QueryRow("SELECT * FROM site_info")
+// 	if err := row.Scan(&siteInfo.InitialSetup, &siteInfo.PeriodicScanInterval, &siteInfo.ConcurrentWorkers); err != nil {
+// 		return nil, errors.Wrap(err, "get site_info row after initialization")
+// 	}
 
-	return siteInfo, nil
-}
+// 	return siteInfo, nil
+// }
 
 // GetSiteInfo gets the site info row from the database, and creates it if it does not exist
-func GetSiteInfo(db *sql.DB) (*SiteInfo, error) {
-	rows, err := db.Query("SELECT * FROM site_info")
-	defer rows.Close()
-	if err != nil {
-		return nil, err
+func GetSiteInfo(db *gorm.DB) (*SiteInfo, error) {
+
+	var siteInfo SiteInfo
+
+	result := db.FirstOrCreate(&siteInfo, SiteInfo{
+		InitialSetup:         true,
+		PeriodicScanInterval: 0,
+		ConcurrentWorkers:    3,
+	})
+	if result.Error != nil {
+		return nil, errors.Wrap(result.Error, "get site info from database")
 	}
 
-	siteInfo := &SiteInfo{}
+	// rows, err := db.Query("SELECT * FROM site_info")
+	// defer rows.Close()
+	// if err != nil {
+	// 	return nil, err
+	// }
 
-	if !rows.Next() {
-		// Entry does not exist
-		siteInfo, err = initializeSiteInfoRow(db)
-		if err != nil {
-			return nil, err
-		}
-	} else {
-		if err := rows.Scan(&siteInfo.InitialSetup, &siteInfo.PeriodicScanInterval, &siteInfo.ConcurrentWorkers); err != nil {
-			return nil, err
-		}
-	}
+	// siteInfo := &SiteInfo{}
 
-	return siteInfo, nil
+	// if !rows.Next() {
+	// 	// Entry does not exist
+	// 	siteInfo, err = initializeSiteInfoRow(db)
+	// 	if err != nil {
+	// 		return nil, err
+	// 	}
+	// } else {
+	// 	if err := rows.Scan(&siteInfo.InitialSetup, &siteInfo.PeriodicScanInterval, &siteInfo.ConcurrentWorkers); err != nil {
+	// 		return nil, err
+	// 	}
+	// }
+
+	return &siteInfo, nil
 }
diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go
index 9bd9a00..5b8d6a8 100644
--- a/api/graphql/models/user.go
+++ b/api/graphql/models/user.go
@@ -2,7 +2,6 @@ package models
 
 import (
 	"crypto/rand"
-	"database/sql"
 	"fmt"
 	"log"
 	"os"
@@ -10,63 +9,68 @@ import (
 
 	"github.com/pkg/errors"
 	"golang.org/x/crypto/bcrypt"
+	"gorm.io/gorm"
 )
 
 type User struct {
-	UserID   int
-	Username string
-	Password *string
-	RootPath string
-	Admin    bool
+	gorm.Model
+	Username string  `gorm:"unique,size:128"`
+	Password *string `gorm:"size:256`
+	RootPath string  `gorm:"size:512`
+	Admin    bool    `gorm:"default:false"`
 }
 
-func (u *User) ID() int {
-	return u.UserID
-}
+// func (u *User) ID() int {
+// 	return u.UserID
+// }
 
 type AccessToken struct {
-	Value  string
+	gorm.Model
+	UserID uint
+	User   User   `gorm:"constraint:OnDelete:CASCADE;"`
+	Value  string `gorm:"size:24`
 	Expire time.Time
 }
 
 var ErrorInvalidUserCredentials = errors.New("invalid credentials")
 
-func NewUserFromRow(row *sql.Row) (*User, error) {
-	user := User{}
+// func NewUserFromRow(row *sql.Row) (*User, error) {
+// 	user := User{}
 
-	if err := row.Scan(&user.UserID, &user.Username, &user.Password, &user.RootPath, &user.Admin); err != nil {
-		return nil, errors.Wrap(err, "failed to scan user from database")
-	}
+// 	if err := row.Scan(&user.UserID, &user.Username, &user.Password, &user.RootPath, &user.Admin); err != nil {
+// 		return nil, errors.Wrap(err, "failed to scan user from database")
+// 	}
 
-	return &user, nil
-}
+// 	return &user, nil
+// }
 
-func NewUsersFromRows(rows *sql.Rows) ([]*User, error) {
-	users := make([]*User, 0)
+// func NewUsersFromRows(rows *sql.Rows) ([]*User, error) {
+// 	users := make([]*User, 0)
 
-	for rows.Next() {
-		var user User
-		if err := rows.Scan(&user.UserID, &user.Username, &user.Password, &user.RootPath, &user.Admin); err != nil {
-			return nil, errors.Wrap(err, "failed to scan users from database")
-		}
-		users = append(users, &user)
-	}
+// 	for rows.Next() {
+// 		var user User
+// 		if err := rows.Scan(&user.UserID, &user.Username, &user.Password, &user.RootPath, &user.Admin); err != nil {
+// 			return nil, errors.Wrap(err, "failed to scan users from database")
+// 		}
+// 		users = append(users, &user)
+// 	}
 
-	rows.Close()
+// 	rows.Close()
 
-	return users, nil
-}
+// 	return users, nil
+// }
 
-func AuthorizeUser(database *sql.DB, username string, password string) (*User, error) {
-	row := database.QueryRow("SELECT * FROM user WHERE username = ?", username)
+func AuthorizeUser(db *gorm.DB, username string, password string) (*User, error) {
+	// row := database.QueryRow("SELECT * FROM user WHERE username = ?", username)
 
-	user, err := NewUserFromRow(row)
-	if err != nil {
-		if err == sql.ErrNoRows {
+	var user User
+
+	result := db.Where("username = ?", username).First(&user)
+	if result.Error != nil {
+		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, ErrorInvalidUserCredentials
-		} else {
-			return nil, err
 		}
+		return nil, errors.Wrap(result.Error, "failed to get user by username when authorizing")
 	}
 
 	if user.Password == nil {
@@ -81,7 +85,7 @@ func AuthorizeUser(database *sql.DB, username string, password string) (*User, e
 		}
 	}
 
-	return user, nil
+	return &user, nil
 }
 
 var ErrorInvalidRootPath = errors.New("invalid root path")
@@ -96,11 +100,17 @@ func ValidRootPath(rootPath string) bool {
 	return true
 }
 
-func RegisterUser(database *sql.Tx, username string, password *string, rootPath string, admin bool) (*User, error) {
+func RegisterUser(db *gorm.DB, username string, password *string, rootPath string, admin bool) (*User, error) {
 	if !ValidRootPath(rootPath) {
 		return nil, ErrorInvalidRootPath
 	}
 
+	user := User{
+		Username: username,
+		RootPath: rootPath,
+		Admin:    admin,
+	}
+
 	if password != nil {
 		hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12)
 		if err != nil {
@@ -108,29 +118,18 @@ func RegisterUser(database *sql.Tx, username string, password *string, rootPath
 		}
 		hashedPass := string(hashedPassBytes)
 
-		if _, err := database.Exec("INSERT INTO user (username, password, root_path, admin) VALUES (?, ?, ?, ?)", username, hashedPass, rootPath, admin); err != nil {
-			return nil, errors.Wrap(err, "insert new user with password into database")
-		}
-	} else {
-		if _, err := database.Exec("INSERT INTO user (username, root_path, admin) VALUES (?, ?, ?)", username, rootPath, admin); err != nil {
-			return nil, errors.Wrap(err, "insert user without password into database")
-		}
+		user.Password = &hashedPass
 	}
 
-	row := database.QueryRow("SELECT * FROM user WHERE username = ?", username)
-	if row == nil {
-		return nil, ErrorInvalidUserCredentials
+	result := db.Create(&user)
+	if result.Error != nil {
+		return nil, errors.Wrap(result.Error, "insert new user with password into database")
 	}
 
-	user, err := NewUserFromRow(row)
-	if err != nil {
-		return nil, err
-	}
-
-	return user, nil
+	return &user, nil
 }
 
-func (user *User) GenerateAccessToken(database *sql.Tx) (*AccessToken, error) {
+func (user *User) GenerateAccessToken(db *gorm.DB) (*AccessToken, error) {
 	bytes := make([]byte, 24)
 	if _, err := rand.Read(bytes); err != nil {
 		return nil, errors.New(fmt.Sprintf("Could not generate token: %s\n", err.Error()))
@@ -142,38 +141,56 @@ func (user *User) GenerateAccessToken(database *sql.Tx) (*AccessToken, error) {
 
 	token_value := string(bytes)
 	expire := time.Now().Add(14 * 24 * time.Hour)
-	expireString := expire.UTC().Format("2006-01-02 15:04:05")
+	// expireString := expire.UTC().Format("2006-01-02 15:04:05")
 
-	if _, err := database.Exec("INSERT INTO access_token (value, expire, user_id) VALUES (?, ?, ?)", token_value, expireString, user.UserID); err != nil {
-		return nil, err
-	}
+	// if _, err := database.Exec("INSERT INTO access_token (value, expire, user_id) VALUES (?, ?, ?)", token_value, expireString, user.UserID); err != nil {
+	// 	return nil, err
+	// }
 
 	token := AccessToken{
+		UserID: user.ID,
 		Value:  token_value,
 		Expire: expire,
 	}
 
+	result := db.Create(&token)
+	if result.Error != nil {
+		return nil, errors.Wrap(result.Error, "saving access token to database")
+	}
+
 	return &token, nil
 }
 
-func VerifyTokenAndGetUser(database *sql.DB, token string) (*User, error) {
+func VerifyTokenAndGetUser(db *gorm.DB, token string) (*User, error) {
 
 	now := time.Now().UTC().Format("2006-01-02 15:04:05")
 
-	row := database.QueryRow("SELECT (user_id) FROM access_token WHERE expire > ? AND value = ?", now, token)
+	// row := database.QueryRow("SELECT (user_id) FROM access_token WHERE expire > ? AND value = ?", now, token)
 
-	var userId string
-
-	if err := row.Scan(&userId); err != nil {
-		log.Println(err.Error())
-		return nil, err
+	var accessToken AccessToken
+	result := db.Where("expire > ? AND value = ?", now, token).First(&accessToken)
+	if result.Error != nil {
+		return nil, result.Error
 	}
 
-	row = database.QueryRow("SELECT * FROM user WHERE user_id = ?", userId)
-	user, err := NewUserFromRow(row)
-	if err != nil {
-		return nil, err
+	// var userId string
+
+	// if err := row.Scan(&userId); err != nil {
+	// 	log.Println(err.Error())
+	// 	return nil, err
+	// }
+
+	// row = db.QueryRow("SELECT * FROM user WHERE user_id = ?", userId)
+	// user, err := NewUserFromRow(row)
+	// if err != nil {
+	// 	return nil, err
+	// }
+
+	var user User
+	result = db.First(&user, accessToken.ID)
+	if result.Error != nil {
+		return nil, result.Error
 	}
 
-	return user, nil
+	return &user, nil
 }
diff --git a/api/graphql/resolvers/root.go b/api/graphql/resolvers/root.go
index 46b8f06..e506383 100644
--- a/api/graphql/resolvers/root.go
+++ b/api/graphql/resolvers/root.go
@@ -2,16 +2,16 @@ package resolvers
 
 import (
 	"context"
-	"database/sql"
 
 	api "github.com/viktorstrate/photoview/api/graphql"
 	"github.com/viktorstrate/photoview/api/graphql/models"
+	"gorm.io/gorm"
 )
 
 //go:generate go run github.com/99designs/gqlgen
 
 type Resolver struct {
-	Database *sql.DB
+	Database *gorm.DB
 }
 
 func (r *Resolver) Mutation() api.MutationResolver {
diff --git a/api/graphql/resolvers/share_token.go b/api/graphql/resolvers/share_token.go
index bf08f70..616059a 100644
--- a/api/graphql/resolvers/share_token.go
+++ b/api/graphql/resolvers/share_token.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/pkg/errors"
+	"gorm.io/gorm"
 
 	api "github.com/viktorstrate/photoview/api/graphql"
 	"github.com/viktorstrate/photoview/api/graphql/auth"
@@ -261,7 +262,7 @@ func hashSharePassword(password *string) (*string, error) {
 	return hashed_password, nil
 }
 
-func getUserToken(db *sql.DB, user *models.User, tokenValue string) (*models.ShareToken, error) {
+func getUserToken(db *gorm.DB, user *models.User, tokenValue string) (*models.ShareToken, error) {
 	row := db.QueryRow(`
 		SELECT share_token.* FROM share_token, user WHERE
 		share_token.value = ? AND
diff --git a/api/routes/authenticate_media.go b/api/routes/authenticate_media.go
index 0659ca0..5f540ce 100644
--- a/api/routes/authenticate_media.go
+++ b/api/routes/authenticate_media.go
@@ -8,9 +8,10 @@ import (
 	"github.com/viktorstrate/photoview/api/graphql/auth"
 	"github.com/viktorstrate/photoview/api/graphql/models"
 	"golang.org/x/crypto/bcrypt"
+	"gorm.io/gorm"
 )
 
-func authenticateMedia(media *models.Media, db *sql.DB, r *http.Request) (success bool, responseMessage string, responseStatus int, errorMessage error) {
+func authenticateMedia(media *models.Media, db *gorm.DB, r *http.Request) (success bool, responseMessage string, responseStatus int, errorMessage error) {
 	user := auth.UserFromContext(r.Context())
 
 	if user != nil {
diff --git a/api/routes/photos.go b/api/routes/photos.go
index 6ed40ae..0377bf4 100644
--- a/api/routes/photos.go
+++ b/api/routes/photos.go
@@ -1,7 +1,6 @@
 package routes
 
 import (
-	"database/sql"
 	"log"
 	"net/http"
 	"os"
@@ -9,12 +8,13 @@ import (
 	"strconv"
 
 	"github.com/gorilla/mux"
+	"gorm.io/gorm"
 
 	"github.com/viktorstrate/photoview/api/graphql/models"
 	"github.com/viktorstrate/photoview/api/scanner"
 )
 
-func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
+func RegisterPhotoRoutes(db *gorm.DB, router *mux.Router) {
 
 	router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) {
 		media_name := mux.Vars(r)["name"]
diff --git a/api/routes/videos.go b/api/routes/videos.go
index a33cb5c..c737fd6 100644
--- a/api/routes/videos.go
+++ b/api/routes/videos.go
@@ -1,7 +1,6 @@
 package routes
 
 import (
-	"database/sql"
 	"log"
 	"net/http"
 	"os"
@@ -11,9 +10,10 @@ import (
 	"github.com/gorilla/mux"
 	"github.com/viktorstrate/photoview/api/graphql/models"
 	"github.com/viktorstrate/photoview/api/scanner"
+	"gorm.io/gorm"
 )
 
-func RegisterVideoRoutes(db *sql.DB, router *mux.Router) {
+func RegisterVideoRoutes(db *gorm.DB, router *mux.Router) {
 
 	router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) {
 		media_name := mux.Vars(r)["name"]
diff --git a/api/scanner/cleanup_media.go b/api/scanner/cleanup_media.go
index 1365c3f..fa6c3ee 100644
--- a/api/scanner/cleanup_media.go
+++ b/api/scanner/cleanup_media.go
@@ -9,9 +9,10 @@ import (
 
 	"github.com/pkg/errors"
 	"github.com/viktorstrate/photoview/api/graphql/models"
+	"gorm.io/gorm"
 )
 
-func CleanupMedia(db *sql.DB, albumId int, albumMedia []*models.Media) []error {
+func CleanupMedia(db *gorm.DB, albumId int, albumMedia []*models.Media) []error {
 	albumMediaIds := make([]interface{}, len(albumMedia))
 	for i, photo := range albumMedia {
 		albumMediaIds[i] = photo.MediaID
@@ -72,7 +73,7 @@ func CleanupMedia(db *sql.DB, albumId int, albumMedia []*models.Media) []error {
 	return deleteErrors
 }
 
-func deleteOldUserAlbums(db *sql.DB, scannedAlbums []*models.Album, user *models.User) []error {
+func deleteOldUserAlbums(db *gorm.DB, scannedAlbums []*models.Album, user *models.User) []error {
 	if len(scannedAlbums) == 0 {
 		return nil
 	}
diff --git a/api/scanner/periodic_scanner.go b/api/scanner/periodic_scanner.go
index 671739c..c8a4152 100644
--- a/api/scanner/periodic_scanner.go
+++ b/api/scanner/periodic_scanner.go
@@ -1,22 +1,23 @@
 package scanner
 
 import (
-	"database/sql"
 	"log"
 	"sync"
 	"time"
+
+	"gorm.io/gorm"
 )
 
 type periodicScanner struct {
 	ticker         *time.Ticker
 	ticker_changed chan bool
 	mutex          *sync.Mutex
-	db             *sql.DB
+	db             *gorm.DB
 }
 
 var mainPeriodicScanner *periodicScanner = nil
 
-func getPeriodicScanInterval(db *sql.DB) (time.Duration, error) {
+func getPeriodicScanInterval(db *gorm.DB) (time.Duration, error) {
 	row := db.QueryRow("SELECT periodic_scan_interval FROM site_info")
 	var intervalSeconds int
 
@@ -27,7 +28,7 @@ func getPeriodicScanInterval(db *sql.DB) (time.Duration, error) {
 	return time.Duration(intervalSeconds) * time.Second, nil
 }
 
-func InitializePeriodicScanner(db *sql.DB) error {
+func InitializePeriodicScanner(db *gorm.DB) error {
 	if mainPeriodicScanner != nil {
 		panic("periodic scanner has already been initialized")
 	}
diff --git a/api/scanner/queue.go b/api/scanner/queue.go
index d3c4933..619661b 100644
--- a/api/scanner/queue.go
+++ b/api/scanner/queue.go
@@ -1,7 +1,6 @@
 package scanner
 
 import (
-	"database/sql"
 	"fmt"
 	"log"
 	"sync"
@@ -11,6 +10,7 @@ import (
 	"github.com/viktorstrate/photoview/api/graphql/models"
 	"github.com/viktorstrate/photoview/api/graphql/notification"
 	"github.com/viktorstrate/photoview/api/utils"
+	"gorm.io/gorm"
 )
 
 type ScannerJob struct {
@@ -18,7 +18,7 @@ type ScannerJob struct {
 	cache *AlbumScannerCache
 }
 
-func (job *ScannerJob) Run(db *sql.DB) {
+func (job *ScannerJob) Run(db *gorm.DB) {
 	scanAlbum(job.album, job.cache, db)
 }
 
@@ -31,13 +31,13 @@ type ScannerQueue struct {
 	idle_chan   chan bool
 	in_progress []ScannerJob
 	up_next     []ScannerJob
-	db          *sql.DB
+	db          *gorm.DB
 	settings    ScannerQueueSettings
 }
 
 var global_scanner_queue ScannerQueue
 
-func InitializeScannerQueue(db *sql.DB) error {
+func InitializeScannerQueue(db *gorm.DB) error {
 
 	var concurrentWorkers int
 	{
@@ -146,14 +146,11 @@ func (queue *ScannerQueue) notify() bool {
 }
 
 func AddAllToQueue() error {
-	rows, err := global_scanner_queue.db.Query("SELECT * FROM user")
-	if err != nil {
-		return errors.Wrap(err, "get all users from database")
-	}
 
-	users, err := models.NewUsersFromRows(rows)
-	if err != nil {
-		return errors.Wrap(err, "parse all users from db")
+	var users []*models.User
+	result := global_scanner_queue.db.Find(&users)
+	if result.Error != nil {
+		return errors.Wrap(result.Error, "get all users from database")
 	}
 
 	for _, user := range users {
@@ -167,7 +164,7 @@ func AddUserToQueue(user *models.User) error {
 	album_cache := MakeAlbumCache()
 	albums, album_errors := findAlbumsForUser(global_scanner_queue.db, user, album_cache)
 	for _, err := range album_errors {
-		return errors.Wrapf(err, "find albums for user (user_id: %d)", user.UserID)
+		return errors.Wrapf(err, "find albums for user (user_id: %d)", user.ID)
 	}
 
 	global_scanner_queue.mutex.Lock()
diff --git a/api/scanner/scanner_album.go b/api/scanner/scanner_album.go
index 552d36e..4c0aa6f 100644
--- a/api/scanner/scanner_album.go
+++ b/api/scanner/scanner_album.go
@@ -1,7 +1,6 @@
 package scanner
 
 import (
-	"database/sql"
 	"fmt"
 	"io/ioutil"
 	"path"
@@ -10,9 +9,10 @@ import (
 	"github.com/viktorstrate/photoview/api/graphql/models"
 	"github.com/viktorstrate/photoview/api/graphql/notification"
 	"github.com/viktorstrate/photoview/api/utils"
+	"gorm.io/gorm"
 )
 
-func scanAlbum(album *models.Album, cache *AlbumScannerCache, db *sql.DB) {
+func scanAlbum(album *models.Album, cache *AlbumScannerCache, db *gorm.DB) {
 
 	album_notify_key := utils.GenerateToken()
 	notifyThrottle := utils.NewThrottle(500 * time.Millisecond)
@@ -85,7 +85,7 @@ func scanAlbum(album *models.Album, cache *AlbumScannerCache, db *sql.DB) {
 	}
 }
 
-func findMediaForAlbum(album *models.Album, cache *AlbumScannerCache, db *sql.DB, onScanPhoto func(photo *models.Media, newPhoto bool)) ([]*models.Media, error) {
+func findMediaForAlbum(album *models.Album, cache *AlbumScannerCache, db *gorm.DB, onScanPhoto func(photo *models.Media, newPhoto bool)) ([]*models.Media, error) {
 
 	albumPhotos := make([]*models.Media, 0)
 
diff --git a/api/scanner/scanner_user.go b/api/scanner/scanner_user.go
index 7c297a6..07448bc 100644
--- a/api/scanner/scanner_user.go
+++ b/api/scanner/scanner_user.go
@@ -2,7 +2,6 @@ package scanner
 
 import (
 	"container/list"
-	"database/sql"
 	"fmt"
 	"io/ioutil"
 	"log"
@@ -13,9 +12,10 @@ import (
 	"github.com/viktorstrate/photoview/api/graphql/models"
 	"github.com/viktorstrate/photoview/api/graphql/notification"
 	"github.com/viktorstrate/photoview/api/utils"
+	"gorm.io/gorm"
 )
 
-func findAlbumsForUser(db *sql.DB, user *models.User, album_cache *AlbumScannerCache) ([]*models.Album, []error) {
+func findAlbumsForUser(db *gorm.DB, user *models.User, album_cache *AlbumScannerCache) ([]*models.Album, []error) {
 
 	// Check if user directory exists on the file system
 	if _, err := os.Stat(user.RootPath); err != nil {
diff --git a/api/server.go b/api/server.go
index 84ea874..36af483 100644
--- a/api/server.go
+++ b/api/server.go
@@ -35,7 +35,6 @@ func main() {
 	if err != nil {
 		log.Panicf("Could not connect to database: %s\n", err)
 	}
-	defer db.Close()
 
 	// Migrate database
 	if err := database.MigrateDatabase(db); err != nil {
