feat: login screen

This commit is contained in:
Araozu 2024-10-07 19:37:20 -05:00
parent fede2c3f8e
commit fe982dd4ca
14 changed files with 258 additions and 23 deletions

2
go.mod
View File

@ -8,6 +8,8 @@ require (
gorm.io/gorm v1.25.12 gorm.io/gorm v1.25.12
) )
require github.com/go-resty/resty/v2 v2.15.3 // indirect
require ( require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect

2
go.sum
View File

@ -5,6 +5,8 @@ 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8=
github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=

View File

@ -1,3 +1,9 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
body {
font-family: "Atkinson Hyperlegible", sans-serif;
}

View File

@ -7,13 +7,6 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// Represents a User in the Database
type User struct {
gorm.Model
Name string
Password string
}
// A package-level database handle. // A package-level database handle.
// It is set up by the [SetupSchema] func, // It is set up by the [SetupSchema] func,
// which should be called before the webserver // which should be called before the webserver
@ -23,8 +16,6 @@ var db *gorm.DB
// Creates all the database tables that this module requires, // Creates all the database tables that this module requires,
// and sets up the database handle for this module // and sets up the database handle for this module
func SetupSchema(dbHandle *gorm.DB) { func SetupSchema(dbHandle *gorm.DB) {
dbHandle.AutoMigrate(&User{})
db = dbHandle
} }
// Registers all the routes that this module (auth) provides // Registers all the routes that this module (auth) provides

View File

@ -0,0 +1,59 @@
package index
import (
"acide/src/utils"
"fmt"
"log"
"net/http"
"time"
"github.com/labstack/echo"
)
// Registers all the routes that this module (auth) provides
func SetupRoutes(g *echo.Group) {
log.Print("Setting up the index module")
// To include custom rendering logic:
g.GET("/", index)
g.POST("/f/login", loginFragment)
}
func index(c echo.Context) error {
return utils.RenderTempl(c, http.StatusOK, IndexTempl())
}
func loginFragment(c echo.Context) error {
navidromeServer := c.FormValue("navidrome-url")
username := c.FormValue("username")
password := c.FormValue("password")
// TODO: validation
sessionToken, err := login(navidromeServer, username, password)
if err != nil {
errorMessage := fmt.Sprintf("<div class='bg-red-500 text-white p-2 m-2 rounded'>Error logging in: %s</div>", err)
return c.HTML(http.StatusBadRequest, errorMessage)
}
cookie1 := new(http.Cookie)
cookie1.Name = "session-token"
cookie1.Value = sessionToken
cookie1.Expires = time.Now().Add(24 * time.Hour)
cookie1.Path = "/"
cookie1.HttpOnly = true
cookie1.Secure = true
c.SetCookie(cookie1)
cookie2 := new(http.Cookie)
cookie2.Name = "navidrome-url"
cookie2.Value = navidromeServer
cookie2.Expires = time.Now().Add(24 * time.Hour)
cookie2.Path = "/"
cookie2.HttpOnly = true
cookie2.Secure = true
c.SetCookie(cookie2)
return c.HTML(http.StatusOK, "wrote some cookies :D")
}

View File

@ -0,0 +1,64 @@
package index
import "acide/src/utils"
templ IndexTempl() {
@utils.SkeletonTempl() {
<nav class="bg-sky-500 text-white text-xl font-bold p-2">
music to go
</nav>
<div hx-ext="response-targets">
Login to Navidrome:
<br/>
<form
hx-post="/f/login"
hx-swap="innerHTML"
hx-target="#login-result"
hx-target-400="#login-result"
hx-target-500="#login-result"
>
<label for="navidrome-url" class="text-sm">
Navidrome URL:
</label>
<input
id="navidrome-url"
class="w-full py-1 px-2 rounded"
placeholder="https://"
name="navidrome-url"
required
/>
<br/>
<label for="username" class="text-sm">
Username:
</label>
<input
id="username"
class="w-full py-1 px-2 rounded"
placeholder="username"
name="username"
required
/>
<br/>
<label for="password" class="text-sm">
Password:
</label>
<input
id="password"
class="w-full py-1 px-2 rounded"
placeholder="password"
name="password"
type="password"
required
/>
<br/>
<button class="py-2 px-4 my-2 bg-sky-500 text-white rounded" type="submit">
Log-in
</button>
<br/>
<div id="login-result">
Result:
</div>
</form>
</div>
}
}

View File

@ -0,0 +1,60 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package index
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "acide/src/utils"
func IndexTempl() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"bg-sky-500 text-white text-xl font-bold p-2\">music to go</nav><div hx-ext=\"response-targets\">Login to Navidrome:<br><form hx-post=\"/f/login\" hx-swap=\"innerHTML\" hx-target=\"#login-result\" hx-target-400=\"#login-result\" hx-target-500=\"#login-result\"><label for=\"navidrome-url\" class=\"text-sm\">Navidrome URL:</label> <input id=\"navidrome-url\" class=\"w-full py-1 px-2 rounded\" placeholder=\"https://\" name=\"navidrome-url\" required><br><label for=\"username\" class=\"text-sm\">Username:</label> <input id=\"username\" class=\"w-full py-1 px-2 rounded\" placeholder=\"username\" name=\"username\" required><br><label for=\"password\" class=\"text-sm\">Password:</label> <input id=\"password\" class=\"w-full py-1 px-2 rounded\" placeholder=\"password\" name=\"password\" type=\"password\" required><br><button class=\"py-2 px-4 my-2 bg-sky-500 text-white rounded\" type=\"submit\">Log-in</button><br><div id=\"login-result\">Result:</div></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = utils.SkeletonTempl().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,41 @@
package index
import (
"acide/src/utils"
"errors"
"fmt"
"github.com/go-resty/resty/v2"
)
type AuthError struct {
Error string `json:"error"`
}
// Attempts to login to a navidrome server with the provided credentials.
// Returns the session key if succesful, an error otherwise
func login(server, username, password string) (string, error) {
client := resty.New()
var loginData utils.AuthSuccess
var loginError AuthError
response, err := client.R().
SetHeader("Content-Type", "").
SetBody(fmt.Sprintf(`{"username":"%s","password":"%s"}`, username, password)).
SetResult(&loginData).
SetError(&loginError).
Post(fmt.Sprintf("%s/auth/login", server))
if err != nil {
return "", err
}
if !response.IsSuccess() {
return "", errors.New(loginError.Error)
}
var sessionToken = loginData.Token
return sessionToken, nil
}

View File

@ -2,6 +2,7 @@ package src
import ( import (
"acide/src/modules/auth" "acide/src/modules/auth"
"acide/src/modules/index"
"net/http" "net/http"
"os" "os"
@ -9,7 +10,7 @@ import (
"github.com/labstack/echo/middleware" "github.com/labstack/echo/middleware"
) )
var dev = os.Getenv("DEV") != "" var dev = os.Getenv("APP_ENV") == "dev"
// Sets up the Echo server, and registers all routes and sub routes // Sets up the Echo server, and registers all routes and sub routes
func (s *Server) RegisterRoutes() http.Handler { func (s *Server) RegisterRoutes() http.Handler {
@ -23,14 +24,10 @@ func (s *Server) RegisterRoutes() http.Handler {
http.FileServer(http.Dir("public")))) http.FileServer(http.Dir("public"))))
e.GET("/public/*", echo.WrapHandler(staticFilesFolder)) e.GET("/public/*", echo.WrapHandler(staticFilesFolder))
// TODO: Register subroutes here // NOTE: Register subroutes here
// login.SetupRoutes(e.Group("/auth")) index.SetupRoutes(e.Group(""))
auth.SetupRoutes(e.Group("/auth")) auth.SetupRoutes(e.Group("/auth"))
e.GET("/", func(ctx echo.Context) error {
return ctx.String(http.StatusOK, "Hello")
})
return e return e
} }

View File

@ -1,7 +1,6 @@
package src package src
import ( import (
"acide/src/database"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@ -9,13 +8,10 @@ import (
"time" "time"
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"
"gorm.io/gorm"
) )
type Server struct { type Server struct {
port int port int
db *gorm.DB
} }
// Creates a new Server // Creates a new Server
@ -28,7 +24,6 @@ func NewServer() *http.Server {
NewServer := &Server{ NewServer := &Server{
port: port, port: port,
db: database.New(),
} }
// TODO: Register DB schemas here // TODO: Register DB schemas here

12
src/utils/types.go Normal file
View File

@ -0,0 +1,12 @@
package utils
// The result of a Login
type AuthSuccess struct {
Id string `json:"id"`
IsAdmin bool `json:"isAdmin"`
Name string `json:"name"`
SubsonicSalt string `json:"subsonicSalt"`
SubsonicToken string `json:"subsonicToken"`
Token string `json:"token"`
Username string `json:"username"`
}

View File

@ -7,7 +7,13 @@ templ SkeletonTempl() {
<meta charset="utf-8"/> <meta charset="utf-8"/>
<title>Acide Template</title> <title>Acide Template</title>
<link href="/public/css/output.css" rel="stylesheet"/> <link href="/public/css/output.css" rel="stylesheet"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet"/>
<script src="/public/js/htmx.min.js" defer></script> <script src="/public/js/htmx.min.js" defer></script>
<script src="/public/js/_hyperscript.min.js" defer></script>
<script src="https://unpkg.com/htmx-ext-response-targets@2.0.0/response-targets.js" defer></script>
</head> </head>
<body> <body>
<main> <main>

View File

@ -29,7 +29,7 @@ func SkeletonTempl() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"es\"><head><meta charset=\"utf-8\"><title>Acide Template</title><link href=\"/public/css/output.css\" rel=\"stylesheet\"><script src=\"/public/js/htmx.min.js\" defer></script></head><body><main>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"es\"><head><meta charset=\"utf-8\"><title>Acide Template</title><link href=\"/public/css/output.css\" rel=\"stylesheet\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin><link href=\"https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&amp;display=swap\" rel=\"stylesheet\"><script src=\"/public/js/htmx.min.js\" defer></script><script src=\"/public/js/_hyperscript.min.js\" defer></script><script src=\"https://unpkg.com/htmx-ext-response-targets@2.0.0/response-targets.js\" defer></script></head><body><main>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -1,7 +1,7 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: [
"./src/**/*.{html,templ}", "./src/**/*.{html,templ,go}",
], ],
theme: { theme: {
extend: {}, extend: {},