diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7984597 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +*~ +*.db +*.bak +*.sqlite3 +**/.DS_Store diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0cfc43c..48911ca 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,15 @@ updates: directory: "/" schedule: interval: "weekly" + groups: + github-deps: + patterns: + - "*" - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" + groups: + go-deps: + patterns: + - "*" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3e407da..085a3d8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,15 +23,15 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: go # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:go" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9dcbd25..18d470e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -26,12 +26,12 @@ jobs: uses: actions/checkout@v4 - name: Setup Docker buildx - uses: docker/setup-buildx-action@v3.0.0 + uses: docker/setup-buildx-action@v3.2.0 # Login against a Docker registry # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -41,7 +41,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v5.0.0 + uses: docker/metadata-action@v5.5.1 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -49,7 +49,7 @@ jobs: # https://github.com/docker/build-push-action - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@v5.0.0 + uses: docker/build-push-action@v5.3.0 with: context: . push: true diff --git a/Dockerfile b/Dockerfile index 6e4f4c7..1e24efe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ -FROM golang:1.21.3-alpine3.18 AS builder +FROM golang:1.22-alpine AS builder -RUN apk add gcc -RUN apk add musl-dev +RUN apk add --no-cache gcc libc-dev WORKDIR /app @@ -12,7 +11,7 @@ ADD . . RUN go build -o main -FROM alpine:3.18.4 +FROM alpine:3.19 WORKDIR /app COPY --from=builder /app/main /usr/local/bin/phoenix @@ -21,7 +20,6 @@ COPY templates ./templates RUN mkdir /var/lib/phoenix ENV P_DBPATH="/var/lib/phoenix/db.sqlite3" -ENV P_PRODUCTION="true" EXPOSE 8080 diff --git a/Makefile b/Makefile index 0802ee3..3b92889 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ -all: fmt vet +all: fmt test fmt: gofmt -s -w . -vet: - go vet ./... +test: + go test ./... + +run: + go run . favicons: convert -background none assets/favicons/favicon.svg -resize 16x16 assets/favicons/favicon-16.png diff --git a/assets/css/base.css b/assets/css/base.css index 30d263d..826c2aa 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -19,7 +19,8 @@ body { } input, -button { +button, +textarea { font-size: inherit; font-family: inherit; padding: 8px 10px; @@ -35,11 +36,17 @@ button { cursor: pointer; } +textarea { + resize: vertical; +} + input:focus, input:hover, button:active, button:hover, -button:focus { +button:focus, +textarea:focus, +textarea:hover { border-color: #812abd; } diff --git a/assets/css/import.css b/assets/css/import.css new file mode 100644 index 0000000..d635e23 --- /dev/null +++ b/assets/css/import.css @@ -0,0 +1,31 @@ +body { + padding: 2em 1em; +} + +@media screen and (min-width: 800px) { + body { + padding: 2em 10em; + } +} + +p { + max-width: 400px; + margin-top: 2em; +} + +form { + width: 100%; + max-width: 400px; + margin-top: 2em; +} + +form textarea { + width: 100%; + margin-top: 10px; + margin-bottom: 10px; + min-height: 100px; +} + +form button { + width: 100%; +} diff --git a/assets/css/index.css b/assets/css/index-common.css similarity index 68% rename from assets/css/index.css rename to assets/css/index-common.css index ac84f39..e1283d9 100644 --- a/assets/css/index.css +++ b/assets/css/index-common.css @@ -9,8 +9,29 @@ margin-top: 3em; } -.group { - width: 100%; +@media screen and (min-width: 600px) { + .page { + padding: 2em 10em + } + .row { + gap: 4em; + } +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 1em; +} + +.controls a { + display: flex; + gap: 8px; + align-items: center; +} + +.controls a img { + filter: invert(100%); } h2 { @@ -18,23 +39,6 @@ h2 { margin-bottom: 6px; } -div > a { - display: flex; - padding-top: 6px; - padding-bottom: 6px; - align-items: center; - gap: 8px; -} - -div > a > img { +img { filter: invert(100%); } - -@media screen and (min-width: 600px) { - .page { - padding: 2em 10em - } - .group { - max-width: 230px; - } -} diff --git a/assets/css/index-list.css b/assets/css/index-list.css new file mode 100644 index 0000000..92da401 --- /dev/null +++ b/assets/css/index-list.css @@ -0,0 +1,12 @@ +.group { + width: 100%; + max-width: max-content; +} + +.links > a { + display: flex; + padding-top: 6px; + padding-bottom: 6px; + align-items: center; + gap: 8px; +} diff --git a/assets/css/index-tiles.css b/assets/css/index-tiles.css new file mode 100644 index 0000000..9a88226 --- /dev/null +++ b/assets/css/index-tiles.css @@ -0,0 +1,37 @@ +.group { + width: 100%; + max-width: max-content; +} + +.links { + width: 100%; + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.links > a { + display: flex; + flex-direction: column; + justify-content: center; + width: 130px; + height: 130px; + padding-top: 6px; + padding-bottom: 6px; + align-items: center; + gap: 8px; + border: 2px solid #444; + border-radius: 5px; + text-align: center; +} + +.links > a:hover { + border-color: #812abd; + text-decoration: none; +} + +.links img { + flex-grow: 1; + width: 30px; + height: 30px; +} diff --git a/assets/css/settings.css b/assets/css/settings.css index 9020098..3c833d4 100644 --- a/assets/css/settings.css +++ b/assets/css/settings.css @@ -2,6 +2,21 @@ body { padding: 2em 1em; } +.actions { + display: flex; + gap: 2em; +} + +.actions a { + display: flex; + align-items: center; + gap: 8px; +} + +.actions a img { + filter: invert(100%); +} + .row { display: flex; gap: 10px; diff --git a/config/main.go b/config/main.go index be3e7e6..2d09e3b 100644 --- a/config/main.go +++ b/config/main.go @@ -6,18 +6,29 @@ import ( "github.com/sirupsen/logrus" ) +var Cfg Config + type Config struct { - SecretKey string `required:"true"` - DBPath string `required:"true"` - LogLevel string `default:"warning"` - EnableGinLogger bool `default:"false"` - Production bool `default:"true"` - HeaderAuth bool `default:"false"` + // A long and random secret string used for authorization. + SecretKey string `required:"true"` + // Path to the sqlite database. + DBPath string `required:"true"` + + LogLevel string `default:"warning"` + + // Allows you to skip authorization if the "Remote-User" header is specified. + // Don't use it if you don't know why you need it. + HeaderAuth bool `default:"false"` + + // Data for the first user. + // Optional, the site also allows you to create the first user. DefaultUsername string DefaultPassword string + // Controls the "secure" option for a token cookie. SecureCookie bool `default:"true"` + // Site title. Title string `default:"Phoenix"` // Any supported css value, embedded directly into every page. FontFamily string `default:"sans-serif"` @@ -29,13 +40,12 @@ func GetConfig() (*Config, error) { logrus.Infof("Config: %v", err) } - var cfg Config - err = envconfig.Process("p", &cfg) + err = envconfig.Process("p", &Cfg) if err != nil { return nil, err } - return &cfg, nil + return &Cfg, nil } func (cfg *Config) GetLogLevel() logrus.Level { @@ -44,7 +54,7 @@ func (cfg *Config) GetLogLevel() logrus.Level { return logrus.DebugLevel case "info": return logrus.InfoLevel - case "warning": + case "warning", "warn": return logrus.WarnLevel case "error": return logrus.ErrorLevel diff --git a/database/admins.go b/database/admins.go index b23e4ec..a06a9bd 100644 --- a/database/admins.go +++ b/database/admins.go @@ -2,53 +2,94 @@ package database import ( "golang.org/x/crypto/bcrypt" - "gorm.io/gorm" ) type Admin struct { - ID uint64 `gorm:"primaryKey"` - Username string `gorm:"unique;notNull"` - Bcrypt string `gorm:"notNull"` + ID int + Username string + Bcrypt string } -func CountAdmins(db *gorm.DB) int64 { - var admins []Admin +func CountAdmins() (int64, error) { var count int64 - db.Model(&admins).Count(&count) - return count + query := `SELECT COUNT(*) FROM admins` + if err := DB.QueryRow(query).Scan(&count); err != nil { + return 0, err + } + + return count, nil } -func CreateAdmin(db *gorm.DB, username string, password string) (Admin, error) { +func CreateAdmin(username string, password string) (*Admin, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) if err != nil { - return Admin{}, err + return nil, err } - admin := Admin{ - Username: username, - Bcrypt: string(hash), - } - result := db.Create(&admin) + query := ` + INSERT INTO admins(username, bcrypt) + VALUES (?, ?) + RETURNING id + ` - if result.Error != nil { - return Admin{}, result.Error - } - - return admin, nil -} - -func AuthorizeAdmin(db *gorm.DB, username string, password string) (Admin, error) { var admin Admin - result := db.Where("username = ?", username).First(&admin) + admin.Username = username + admin.Bcrypt = string(hash) - if result.Error != nil { - return Admin{}, result.Error - } + err = DB. + QueryRow(query, admin.Username, admin.Bcrypt). + Scan(&admin.ID) - err := bcrypt.CompareHashAndPassword([]byte(admin.Bcrypt), []byte(password)) if err != nil { - return Admin{}, err + return nil, err } - return admin, nil + return &admin, nil +} + +func GetAdminIfPasswordMatches(username string, password string) (*Admin, error) { + query := ` + SELECT id, username, bcrypt + FROM admins + WHERE username = ? + ` + + var admin Admin + err := DB. + QueryRow(query, username). + Scan(&admin.ID, &admin.Username, &admin.Bcrypt) + + if err != nil { + return nil, err + } + + err = bcrypt.CompareHashAndPassword([]byte(admin.Bcrypt), []byte(password)) + if err != nil { + return nil, err + } + + return &admin, nil +} + +func DeleteAdmin(id int) error { + query := ` + DELETE FROM admins + WHERE id = ? + ` + + res, err := DB.Exec(query, id) + if err != nil { + return err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return err + } + + if rowsAffected != 1 { + return ErrWrongNumberOfAffectedRows + } + + return nil } diff --git a/database/admins_test.go b/database/admins_test.go new file mode 100644 index 0000000..4028896 --- /dev/null +++ b/database/admins_test.go @@ -0,0 +1,59 @@ +package database + +import ( + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +func TestAdmins(t *testing.T) { + initTestDatabase(t) + defer deleteTestDatabase(t) + + // We should have no admins. + count, err := CountAdmins() + if err != nil { + t.Fatal(err) + } + + if count != 0 { + t.Fatal("user count is not zero") + } + + // Create the first user. + username := "test" + password := "test" + admin, err := CreateAdmin(username, password) + if err != nil { + t.Fatal(err) + } + + // Check password and get admin. + dbAdmin, err := GetAdminIfPasswordMatches(username, password) + if err != nil { + t.Fatal(err) + } + if dbAdmin.ID != admin.ID { + t.Fatal("wrong admin id") + } + + // Check wrong password handling. + if _, err := GetAdminIfPasswordMatches("test", "wrong-password"); err == nil { + t.Fatal("wrong password was accepted") + } + + // Count users again. + count, err = CountAdmins() + if err != nil { + t.Fatal(err) + } + + if count != 1 { + t.Fatal("user count is not one") + } + + // Delete user. + if err := DeleteAdmin(admin.ID); err != nil { + t.Fatal(err) + } +} diff --git a/database/connection.go b/database/connection.go new file mode 100644 index 0000000..2fd569c --- /dev/null +++ b/database/connection.go @@ -0,0 +1,21 @@ +package database + +import ( + "database/sql" + + _ "github.com/mattn/go-sqlite3" + "github.com/ordinary-dev/phoenix/config" +) + +var DB *sql.DB + +func EstablishDatabaseConnection(cfg *config.Config) error { + var err error + DB, err = sql.Open("sqlite3", cfg.DBPath) + + if err := DB.Ping(); err != nil { + return err + } + + return err +} diff --git a/database/connection_test.go b/database/connection_test.go new file mode 100644 index 0000000..f9c3c2c --- /dev/null +++ b/database/connection_test.go @@ -0,0 +1,29 @@ +package database + +import ( + "database/sql" + "os" + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +const TEST_DB_PATH = "/tmp/phoenix.sqlite3" + +func initTestDatabase(t *testing.T) { + var err error + DB, err = sql.Open("sqlite3", TEST_DB_PATH) + if err != nil { + t.Fatal(err) + } + + if err := ApplyMigrations(); err != nil { + t.Fatal(err) + } +} + +func deleteTestDatabase(t *testing.T) { + if err := os.Remove(TEST_DB_PATH); err != nil { + t.Fatal(err) + } +} diff --git a/database/db.go b/database/db.go deleted file mode 100644 index 51303d5..0000000 --- a/database/db.go +++ /dev/null @@ -1,19 +0,0 @@ -package database - -import ( - "github.com/ordinary-dev/phoenix/config" - "gorm.io/driver/sqlite" - "gorm.io/gorm" -) - -func GetDatabaseConnection(cfg *config.Config) (*gorm.DB, error) { - db, err := gorm.Open(sqlite.Open(cfg.DBPath), &gorm.Config{}) - if err != nil { - return nil, err - } - - // Migrate the schema - db.AutoMigrate(&Admin{}, &Group{}, &Link{}) - - return db, nil -} diff --git a/database/errors.go b/database/errors.go new file mode 100644 index 0000000..e1e9715 --- /dev/null +++ b/database/errors.go @@ -0,0 +1,9 @@ +package database + +import ( + "errors" +) + +var ( + ErrWrongNumberOfAffectedRows = errors.New("wrong number of affected rows") +) diff --git a/database/groups.go b/database/groups.go index cdeae45..ccb2e2e 100644 --- a/database/groups.go +++ b/database/groups.go @@ -1,7 +1,106 @@ package database type Group struct { - ID uint64 `gorm:"primaryKey"` - Name string `gorm:"unique,notNull"` - Links []Link `gorm:"constraint:OnDelete:CASCADE;"` + ID int `json:"id"` + Name string `json:"name"` + Links []Link `json:"links"` +} + +func GetGroupsWithLinks() ([]Group, error) { + query := ` + SELECT id, name + FROM groups + ORDER BY groups.id + ` + + rows, err := DB.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var groups []Group + for rows.Next() { + var group Group + if err := rows.Scan(&group.ID, &group.Name); err != nil { + return nil, err + } + groups = append(groups, group) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + for i := range groups { + groups[i].Links, err = GetLinksFromGroup(groups[i].ID) + if err != nil { + return nil, err + } + } + + return groups, nil +} + +// Create a new group in the database. +// The function fills in the ID. +func CreateGroup(group *Group) error { + query := ` + INSERT INTO groups (name) + VALUES (?) + RETURNING id + ` + + if err := DB.QueryRow(query, group.Name).Scan(&group.ID); err != nil { + return err + } + + return nil +} + +func UpdateGroup(id int, name string) error { + query := ` + UPDATE groups + SET name = ? + WHERE id = ? + ` + + res, err := DB.Exec(query, name, id) + if err != nil { + return err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return err + } + + if rowsAffected != 1 { + return ErrWrongNumberOfAffectedRows + } + + return nil +} + +func DeleteGroup(groupID int) error { + query := ` + DELETE FROM groups + WHERE id = ? + ` + + res, err := DB.Exec(query, groupID) + if err != nil { + return err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return err + } + + if rowsAffected != 1 { + return ErrWrongNumberOfAffectedRows + } + + return nil } diff --git a/database/groups_test.go b/database/groups_test.go new file mode 100644 index 0000000..a98ed52 --- /dev/null +++ b/database/groups_test.go @@ -0,0 +1,47 @@ +package database + +import ( + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +func TestGroups(t *testing.T) { + initTestDatabase(t) + defer deleteTestDatabase(t) + + // Create the first group. + group := Group{ + Name: "test", + } + if err := CreateGroup(&group); err != nil { + t.Fatal(err) + } + if group.ID == 0 { + t.Fatal("group id is zero") + } + + // Update group. + if err := UpdateGroup(group.ID, "new-name"); err != nil { + t.Fatal(err) + } + + // Read groups. + groupList, err := GetGroupsWithLinks() + if err != nil { + t.Fatal(err) + } + + if len(groupList) != 1 { + t.Fatal("group list length is not one") + } + + if groupList[0].Name != "new-name" { + t.Fatal("wrong group name") + } + + // Delete group. + if err := DeleteGroup(group.ID); err != nil { + t.Fatal(err) + } +} diff --git a/database/links.go b/database/links.go index 3b44042..5b898dd 100644 --- a/database/links.go +++ b/database/links.go @@ -1,9 +1,124 @@ package database type Link struct { - ID uint64 `gorm:"primaryKey"` - Name string `gorm:"notNull"` - Href string `gorm:"notNull"` - GroupID uint64 `gorm:"notNull"` - Icon *string + ID int `json:"id"` + Name string `json:"name"` + Href string `json:"href"` + GroupID int `json:"-"` + Icon *string `json:"icon,omitempty"` +} + +func GetLinksFromGroup(groupID int) ([]Link, error) { + query := ` + SELECT id, name, href, group_id, icon + FROM links + WHERE group_id = ? + ORDER BY id + ` + + rows, err := DB.Query(query, groupID) + if err != nil { + return nil, err + } + defer rows.Close() + + var links []Link + for rows.Next() { + var link Link + if err := rows.Scan(&link.ID, &link.Name, &link.Href, &link.GroupID, &link.Icon); err != nil { + return nil, err + } + links = append(links, link) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return links, nil +} + +func GetLink(id int) (*Link, error) { + query := ` + SELECT id, name, href, group_id, icon + FROM links + WHERE id = ? + ` + + var link Link + err := DB. + QueryRow(query, id). + Scan(&link.ID, &link.Name, &link.Href, &link.GroupID, &link.Icon) + if err != nil { + return nil, err + } + + return &link, nil +} + +// Create a new link in the database. +// The function fills in the ID. +func CreateLink(link *Link) error { + query := ` + INSERT INTO links (name, href, group_id, icon) + VALUES (?, ?, ?, ?) + RETURNING id + ` + + err := DB. + QueryRow(query, link.Name, link.Href, link.GroupID, link.Icon). + Scan(&link.ID) + + if err != nil { + return err + } + + return nil +} + +func UpdateLink(link *Link) error { + query := ` + UPDATE links + SET name = ?, href = ?, group_id = ?, icon = ? + WHERE id = ? + ` + + res, err := DB.Exec(query, link.Name, link.Href, link.GroupID, link.Icon, link.ID) + if err != nil { + return err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return err + } + + if rowsAffected != 1 { + return ErrWrongNumberOfAffectedRows + } + + return nil +} + +func DeleteLink(linkID int) error { + query := ` + DELETE FROM links + WHERE id = ? + ` + + res, err := DB.Exec(query, linkID) + if err != nil { + return err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return err + } + + if rowsAffected != 1 { + return ErrWrongNumberOfAffectedRows + } + + return nil } diff --git a/database/links_test.go b/database/links_test.go new file mode 100644 index 0000000..e735f84 --- /dev/null +++ b/database/links_test.go @@ -0,0 +1,51 @@ +package database + +import ( + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +func TestLinks(t *testing.T) { + initTestDatabase(t) + defer deleteTestDatabase(t) + + // Create the first group. + group := Group{ + Name: "test", + } + if err := CreateGroup(&group); err != nil { + t.Fatal(err) + } + + // Create the first link. + icon := "test/icon" + link := Link{ + Name: "test", + Href: "/test", + GroupID: group.ID, + Icon: &icon, + } + if err := CreateLink(&link); err != nil { + t.Fatal(err) + } + if link.ID == 0 { + t.Fatal("link id is zero") + } + + // Update link. + link.Href = "/new-href" + if err := UpdateLink(&link); err != nil { + t.Fatal(err) + } + + // Delete link. + if err := DeleteLink(link.ID); err != nil { + t.Fatal(err) + } + + // Delete group. + if err := DeleteGroup(group.ID); err != nil { + t.Fatal(err) + } +} diff --git a/database/migrations.go b/database/migrations.go new file mode 100644 index 0000000..e1854e0 --- /dev/null +++ b/database/migrations.go @@ -0,0 +1,98 @@ +package database + +import ( + "database/sql" + "errors" + "fmt" + + log "github.com/sirupsen/logrus" +) + +// List of migrations that should be applied. +// Migration ID = index + 1. +var migrations = []string{ + `CREATE TABLE IF NOT EXISTS admins ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + bcrypt TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS groups ( + id INTEGER PRIMARY KEY, + name TEXT + )`, + `CREATE TABLE IF NOT EXISTS links ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + href TEXT NOT NULL, + group_id INTEGER NOT NULL, + icon TEXT, + CONSTRAINT fk_groups_links + FOREIGN KEY (group_id) + REFERENCES groups(id) + ON DELETE CASCADE + )`, +} + +func ApplyMigrations() error { + // Create a table to record applied migrations and retrieve the saved data. + _, err := DB.Exec(`CREATE TABLE IF NOT EXISTS migrations ( + version INTEGER NOT NULL DEFAULT 0 + )`) + if err != nil { + return err + } + + var currentVersion int + err = DB. + QueryRow("SELECT version FROM migrations"). + Scan(¤tVersion) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return err + } + + // The table is empty, create a record. + _, err = DB.Exec("INSERT INTO migrations (version) VALUES (0)") + if err != nil { + return err + } + } + + // Apply all migrations. + for i, migration := range migrations { + migrationID := i + 1 + if migrationID <= currentVersion { + continue + } + + if err := applyMigration(migrationID, migration); err != nil { + return fmt.Errorf("migration #%d: %w", migrationID, err) + } + + log.Infof("Migration #%v has been applied", migrationID) + } + + return nil +} + +func applyMigration(migrationID int, query string) error { + tx, err := DB.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + if _, err := tx.Exec(query); err != nil { + return fmt.Errorf("error when applying migration: %w", err) + } + + if _, err := tx.Exec("UPDATE migrations SET version = ?", migrationID); err != nil { + return fmt.Errorf("error when updating schema version: %w", err) + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} diff --git a/database/migrations_test.go b/database/migrations_test.go new file mode 100644 index 0000000..fd53730 --- /dev/null +++ b/database/migrations_test.go @@ -0,0 +1,17 @@ +package database + +import ( + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +func TestMigrations(t *testing.T) { + initTestDatabase(t) + defer deleteTestDatabase(t) + + // We should be able to call the function multiple times. + if err := ApplyMigrations(); err != nil { + t.Fatal(err) + } +} diff --git a/go.mod b/go.mod index 3d211a0..ba7560b 100644 --- a/go.mod +++ b/go.mod @@ -1,43 +1,17 @@ module github.com/ordinary-dev/phoenix -go 1.20 +go 1.22 require ( - github.com/gin-gonic/gin v1.9.1 - github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 + github.com/mattn/go-sqlite3 v1.14.22 github.com/sirupsen/logrus v1.9.3 - golang.org/x/crypto v0.14.0 - gorm.io/driver/sqlite v1.5.4 - gorm.io/gorm v1.25.5 + golang.org/x/crypto v0.21.0 ) require ( - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-sqlite3 v1.14.17 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/stretchr/testify v1.8.3 // indirect + golang.org/x/sys v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index 149258f..d4a3009 100644 --- a/go.sum +++ b/go.sum @@ -1,104 +1,28 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= -github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= -gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= -gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= -gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/jwttoken/token.go b/jwttoken/token.go new file mode 100644 index 0000000..fa857c5 --- /dev/null +++ b/jwttoken/token.go @@ -0,0 +1,33 @@ +package jwttoken + +import ( + "net/http" + "time" + + "github.com/golang-jwt/jwt/v5" + + "github.com/ordinary-dev/phoenix/config" +) + +const ( + TOKEN_LIFETIME_IN_SECONDS = 60 * 60 * 24 * 30 + TOKEN_COOKIE_NAME = "phoenix-token" +) + +func GetJWTToken() (string, error) { + claims := jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * TOKEN_LIFETIME_IN_SECONDS)), + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(config.Cfg.SecretKey)) +} + +func TokenToCookie(value string) *http.Cookie { + return &http.Cookie{ + Name: TOKEN_COOKIE_NAME, + Value: value, + HttpOnly: true, + Secure: config.Cfg.SecureCookie, + MaxAge: TOKEN_LIFETIME_IN_SECONDS, + } +} diff --git a/main.go b/main.go index e7239f5..fa55756 100644 --- a/main.go +++ b/main.go @@ -4,42 +4,56 @@ import ( "github.com/ordinary-dev/phoenix/config" "github.com/ordinary-dev/phoenix/database" "github.com/ordinary-dev/phoenix/views" - "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" ) func main() { // Configure logger - logrus.SetFormatter(&logrus.TextFormatter{ + log.SetFormatter(&log.TextFormatter{ FullTimestamp: true, }) // Read config cfg, err := config.GetConfig() if err != nil { - logrus.Fatalf("%v", err) + log.Fatal(err) } // Set log level logLevel := cfg.GetLogLevel() - logrus.SetLevel(logLevel) - logrus.Infof("Setting log level to %v", logLevel) + log.SetLevel(logLevel) + log.Infof("Setting log level to %v", logLevel) // Connect to the database - db, err := database.GetDatabaseConnection(cfg) + err = database.EstablishDatabaseConnection(cfg) if err != nil { - logrus.Fatalf("%v", err) + log.Fatal(err) + } + + // Apply migrations. + if err := database.ApplyMigrations(); err != nil { + log.Fatal(err) } // Create the first user if cfg.DefaultUsername != "" && cfg.DefaultPassword != "" { - if database.CountAdmins(db) < 1 { - _, err := database.CreateAdmin(db, cfg.DefaultUsername, cfg.DefaultPassword) + adminCount, err := database.CountAdmins() + if err != nil { + log.Fatal(err) + } + + if adminCount < 1 { + _, err := database.CreateAdmin(cfg.DefaultUsername, cfg.DefaultPassword) if err != nil { - logrus.Errorf("%v", err) + log.Fatal(err) } } } - engine := views.GetGinEngine(cfg, db) - engine.Run(":8080") + server, err := views.GetHttpServer() + if err != nil { + log.Fatal(err) + } + + server.ListenAndServe() } diff --git a/readme.md b/readme.md index 0ccad97..d5f12ba 100644 --- a/readme.md +++ b/readme.md @@ -8,10 +8,11 @@ Self-hosted start page without the extra stuff. ## Features - No javascript -- Relatively low resource consumption (around 7 MiB of RAM) - Authorization support - SSO via Trusted Header Auth (_Reverse Proxy_) - Font Awesome integration +- Multiple styles +- Export and import ## Configuration Service settings can be set through environment variables. @@ -21,8 +22,6 @@ Service settings can be set through environment variables. | P_DBPATH | Path to the sqlite database. | Docker: `/var/lib/phoenix/db.sqlite3` | | P_SECRETKEY | A long and random secret string used for authorization. | | | P_LOGLEVEL | Log level settings: `debug`, `info`, `warning`, `error`, `fatal` | `warning` | -| P_ENABLEGINLOGGER | Enable gin's logging middleware. Can create a lot of logs. | `false` | -| P_PRODUCTION | Is this instance running in production mode? | `true` | | P_HEADERAUTH | Enable Trusted Header Auth (SSO) | `false` | | P_DEFAULTUSERNAME | Data for the first user. | | | P_DEFAULTPASSWORD | Data for the first user. | | diff --git a/templates/auth.html.tmpl b/templates/auth.html.tmpl index bd0163c..021e7ea 100644 --- a/templates/auth.html.tmpl +++ b/templates/auth.html.tmpl @@ -1,14 +1,14 @@ - {{template "head" .}} + {{ template "head" . }}
-
-

{{.title}}

-

{{.description}}

+ +

{{ .title }}

+

{{ .description }}

- {{template "head" .}} + {{ template "head" . }}

Error

- {{.error}} + {{ .error }}

Main page

diff --git a/templates/head.html.tmpl b/templates/fragments/head.html.tmpl similarity index 90% rename from templates/head.html.tmpl rename to templates/fragments/head.html.tmpl index 3487400..f05e657 100644 --- a/templates/head.html.tmpl +++ b/templates/fragments/head.html.tmpl @@ -1,5 +1,5 @@ {{define "head"}} -{{.WebsiteTitle}} +{{ .title }} @@ -10,7 +10,7 @@ {{end}} diff --git a/templates/import.html.tmpl b/templates/import.html.tmpl new file mode 100644 index 0000000..8d0eadd --- /dev/null +++ b/templates/import.html.tmpl @@ -0,0 +1,27 @@ + + + + {{template "head" .}} + + + +

Import

+ + Settings + +

+ Importing does not erase existing links, but may create duplicates. +

+ + + + + + + + diff --git a/templates/index.html.tmpl b/templates/index.html.tmpl index e3550ab..87728b0 100644 --- a/templates/index.html.tmpl +++ b/templates/index.html.tmpl @@ -2,32 +2,54 @@ {{template "head" .}} - + +
-

{{.WebsiteTitle}}

- {{if not .groups}} +

{{ .title }}

+ + {{ if not .groups }}

- You don't have any links. - Go to settings and create one. + You don't have any links.
+ Go to settings and create one.

- {{else}} - Settings - {{end}} + {{ end }} + +
+ + Settings + + + {{ if ne .style "list" }} + + List + + {{ end }} + + {{ if ne .style "tiles" }} + + Tiles + + {{ end }} +
{{range .groups}}

{{.Name}}

+
{{end}}
diff --git a/templates/settings.html.tmpl b/templates/settings.html.tmpl index c2cf34c..4fcedf9 100644 --- a/templates/settings.html.tmpl +++ b/templates/settings.html.tmpl @@ -6,12 +6,23 @@

Settings

- Main page + + {{range .groups}}

Group "{{.Name}}"

-
+
-
+
-
+