implement database user login/auth

This commit is contained in:
James Ravenscroft 2022-02-13 15:49:22 +00:00
parent 40f717d683
commit d4043ebc88
10 changed files with 92 additions and 47 deletions

View File

@ -3,14 +3,27 @@ package controllers
import ( import (
"net/http" "net/http"
"git.jamesravey.me/ravenscroftj/indiescrobble/models"
"git.jamesravey.me/ravenscroftj/indiescrobble/scrobble" "git.jamesravey.me/ravenscroftj/indiescrobble/scrobble"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func Index(c *gin.Context) { func Index(c *gin.Context) {
// this is an authed endpoint so 'user' must be set and if not panicking is fair
currentUser, exists := c.Get("user")
var user *models.BaseUser
if exists {
user = currentUser.(*models.BaseUser)
}else{
user = nil
}
c.HTML(http.StatusOK, "index.tmpl", gin.H{ c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "test", "title": "test",
"user": c.GetString("user"), "user": user,
"scrobbleTypes": scrobble.ScrobbleTypeNames, "scrobbleTypes": scrobble.ScrobbleTypeNames,
}) })
} }

View File

@ -15,21 +15,22 @@ import (
"github.com/go-chi/jwtauth/v5" "github.com/go-chi/jwtauth/v5"
"github.com/hacdias/indieauth" "github.com/hacdias/indieauth"
"github.com/lestrrat-go/jwx/jwt" "github.com/lestrrat-go/jwx/jwt"
"gorm.io/gorm"
) )
type IndieAuthManager struct { type IndieAuthManager struct {
iac *indieauth.Client iac *indieauth.Client
jwtAuth *jwtauth.JWTAuth jwtAuth *jwtauth.JWTAuth
db *gorm.DB
} }
func NewIndieAuthManager() *IndieAuthManager{ func NewIndieAuthManager(db *gorm.DB) *IndieAuthManager {
config := config.GetConfig() config := config.GetConfig()
iam := new(IndieAuthManager) iam := new(IndieAuthManager)
iam.iac = indieauth.NewClient(config.GetString("indieauth.clientName"), config.GetString("indieauth.redirectURL"), nil) iam.iac = indieauth.NewClient(config.GetString("indieauth.clientName"), config.GetString("indieauth.redirectURL"), nil)
iam.db = db
iam.jwtAuth = jwtauth.New("HS256", []byte(config.GetString("jwt.signKey")), []byte(config.GetString("jwt.signKey"))) iam.jwtAuth = jwtauth.New("HS256", []byte(config.GetString("jwt.signKey")), []byte(config.GetString("jwt.signKey")))
return iam return iam
@ -41,27 +42,52 @@ func (iam *IndieAuthManager) GetCurrentUser(c *gin.Context) *models.BaseUser {
if err != nil { if err != nil {
return nil return nil
}else{ } else {
tok, err := iam.jwtAuth.Decode(jwt) tok, err := iam.jwtAuth.Decode(jwt)
if err != nil{ if err != nil {
log.Printf("Failed to decode jwt: %v", err) log.Printf("Failed to decode jwt: %v", err)
return nil return nil
} }
me, present := tok.Get("user") me, present := tok.Get("user")
if !present{ if !present {
return nil return nil
} }
indietok, present := tok.Get("token") indietok, present := tok.Get("token")
if !present{ if !present {
return nil return nil
} }
user := models.BaseUser{Me: me.(string), Token: indietok.(string)} // see if the user exists in the database or set up their profile
userRecord := models.User{}
result := iam.db.First(&userRecord, models.User{Me: me.(string)})
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
log.Printf("Create new user profile for user %v\n", me)
// create user record for current user
userRecord = models.User{Me: me.(string)}
userRecord.GenerateRandomKey()
result := iam.db.Create(&userRecord)
if result.Error != nil {
log.Printf("Failed to create user record in db: %v", result.Error)
return nil
}
} else {
log.Printf("Failed to get user from db: %v\n", result.Error)
return nil
}
}
user := models.BaseUser{Me: me.(string), Token: indietok.(string), UserRecord: &userRecord}
return &user return &user
@ -71,7 +97,6 @@ func (iam *IndieAuthManager) GetCurrentUser(c *gin.Context) *models.BaseUser {
func (iam *IndieAuthManager) getInformation(c *gin.Context) (*indieauth.AuthInfo, string, error) { func (iam *IndieAuthManager) getInformation(c *gin.Context) (*indieauth.AuthInfo, string, error) {
config := config.GetConfig() config := config.GetConfig()
cookie, err := c.Request.Cookie(config.GetString("indieauth.oauthCookieName")) cookie, err := c.Request.Cookie(config.GetString("indieauth.oauthCookieName"))
@ -166,7 +191,6 @@ func (iam *IndieAuthManager) saveAuthInfo(w http.ResponseWriter, r *http.Request
return nil return nil
} }
func (iam *IndieAuthManager) Logout(c *gin.Context) { func (iam *IndieAuthManager) Logout(c *gin.Context) {
// delete the cookie // delete the cookie
@ -190,7 +214,7 @@ func (iam *IndieAuthManager) IndieAuthLoginPost(c *gin.Context) {
err := c.Request.ParseForm() err := c.Request.ParseForm()
if err != nil{ if err != nil {
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{ c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
"message": err, "message": err,
}) })
@ -227,7 +251,6 @@ func (iam *IndieAuthManager) IndieAuthLoginPost(c *gin.Context) {
fmt.Printf("profile: %v\n", i) fmt.Printf("profile: %v\n", i)
err = iam.saveAuthInfo(c.Writer, c.Request, i) err = iam.saveAuthInfo(c.Writer, c.Request, i)
if err != nil { if err != nil {
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{ c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
@ -237,12 +260,11 @@ func (iam *IndieAuthManager) IndieAuthLoginPost(c *gin.Context) {
} }
// append me param so the user doesn't have to enter this twice // append me param so the user doesn't have to enter this twice
redirect = fmt.Sprintf("%v&me=%v", redirect, url.QueryEscape(i.Me) ) redirect = fmt.Sprintf("%v&me=%v", redirect, url.QueryEscape(i.Me))
c.Redirect(http.StatusSeeOther, redirect) c.Redirect(http.StatusSeeOther, redirect)
} }
func (iam *IndieAuthManager) LoginCallbackGet(c *gin.Context) { func (iam *IndieAuthManager) LoginCallbackGet(c *gin.Context) {
config := config.GetConfig() config := config.GetConfig()
@ -263,7 +285,6 @@ func (iam *IndieAuthManager) LoginCallbackGet(c *gin.Context) {
return return
} }
// profile, err := iam.iac.FetchProfile(i, code) // profile, err := iam.iac.FetchProfile(i, code)
// if err != nil { // if err != nil {
// c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{ // c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
@ -272,7 +293,6 @@ func (iam *IndieAuthManager) LoginCallbackGet(c *gin.Context) {
// return // return
// } // }
token, _, err := iam.iac.GetToken(i, code) token, _, err := iam.iac.GetToken(i, code)
if err != nil { if err != nil {
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{ c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
@ -283,7 +303,6 @@ func (iam *IndieAuthManager) LoginCallbackGet(c *gin.Context) {
me := token.Extra("me").(string) me := token.Extra("me").(string)
if err := indieauth.IsValidProfileURL(me); err != nil { if err := indieauth.IsValidProfileURL(me); err != nil {
err = fmt.Errorf("invalid 'me': %w", err) err = fmt.Errorf("invalid 'me': %w", err)
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{ c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
@ -299,7 +318,7 @@ func (iam *IndieAuthManager) LoginCallbackGet(c *gin.Context) {
jwt.IssuedAtKey: time.Now().Unix(), jwt.IssuedAtKey: time.Now().Unix(),
jwt.ExpirationKey: expiration, jwt.ExpirationKey: expiration,
"user": me, "user": me,
"token": token.AccessToken, "token": token.AccessToken,
}) })
if err != nil { if err != nil {
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{ c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{

View File

@ -99,7 +99,14 @@ func PreviewScrobble(c *gin.Context){
discovery := micropub.MicropubDiscoveryService{} discovery := micropub.MicropubDiscoveryService{}
discovery.Discover(currentUser.Me, currentUser.Token ) config, err := discovery.Discover(currentUser.Me, currentUser.Token )
if err != nil{
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
"message": err,
})
return
}
c.HTML(http.StatusOK, "preview.tmpl", gin.H{ c.HTML(http.StatusOK, "preview.tmpl", gin.H{
"user": currentUser, "user": currentUser,
@ -109,6 +116,7 @@ func PreviewScrobble(c *gin.Context){
"when": c.Request.Form.Get("when"), "when": c.Request.Form.Get("when"),
"rating": c.Request.Form.Get("rating"), "rating": c.Request.Form.Get("rating"),
"content": c.Request.Form.Get("content"), "content": c.Request.Form.Get("content"),
"config": config,
}) })
} }

View File

@ -7,12 +7,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func AuthMiddleware(requireValidUser bool) gin.HandlerFunc { func AuthMiddleware(requireValidUser bool, iam *controllers.IndieAuthManager) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// config := config.GetConfig() // config := config.GetConfig()
iam := controllers.NewIndieAuthManager()
currentUser := iam.GetCurrentUser(c) currentUser := iam.GetCurrentUser(c)
if requireValidUser && (currentUser == nil) { if requireValidUser && (currentUser == nil) {
@ -40,4 +38,3 @@ func AuthMiddleware(requireValidUser bool) gin.HandlerFunc {
c.Next() c.Next()
} }
} }

View File

@ -5,9 +5,10 @@ import (
"git.jamesravey.me/ravenscroftj/indiescrobble/controllers" "git.jamesravey.me/ravenscroftj/indiescrobble/controllers"
"git.jamesravey.me/ravenscroftj/indiescrobble/middlewares" "git.jamesravey.me/ravenscroftj/indiescrobble/middlewares"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
) )
func NewRouter() *gin.Engine { func NewRouter(db *gorm.DB) *gin.Engine {
router := gin.New() router := gin.New()
router.Use(gin.Logger()) router.Use(gin.Logger())
router.Use(gin.Recovery()) router.Use(gin.Recovery())
@ -16,12 +17,11 @@ func NewRouter() *gin.Engine {
health := new(controllers.HealthController) health := new(controllers.HealthController)
iam := controllers.NewIndieAuthManager() iam := controllers.NewIndieAuthManager(db)
router.GET("/health", health.Status) router.GET("/health", health.Status)
router.Use(middlewares.AuthMiddleware(false)) router.Use(middlewares.AuthMiddleware(false, iam))
router.GET("/", controllers.Index) router.GET("/", controllers.Index)
@ -33,14 +33,12 @@ func NewRouter() *gin.Engine {
router.GET("/auth", iam.LoginCallbackGet) router.GET("/auth", iam.LoginCallbackGet)
router.GET("/logout", iam.Logout) router.GET("/logout", iam.Logout)
authed := router.Use(middlewares.AuthMiddleware(true)) authed := router.Use(middlewares.AuthMiddleware(true, iam))
// add scrobble endpoints // add scrobble endpoints
authed.GET("/scrobble", controllers.Scrobble) authed.GET("/scrobble", controllers.Scrobble)
authed.POST("/scrobble/preview", controllers.PreviewScrobble) authed.POST("/scrobble/preview", controllers.PreviewScrobble)
// v1 := router.Group("v1") // v1 := router.Group("v1")
// { // {
// userGroup := v1.Group("user") // userGroup := v1.Group("user")

View File

@ -15,25 +15,23 @@ import (
func Init() { func Init() {
config := config.GetConfig() config := config.GetConfig()
var dialect gorm.Dialector var dialect gorm.Dialector
if config.GetString("server.database.driver") == "sqlite" { if config.GetString("server.database.driver") == "sqlite" {
dialect = sqlite.Open(config.GetString("server.database.dsn")) dialect = sqlite.Open(config.GetString("server.database.dsn"))
}else{ } else {
dialect = mysql.Open(config.GetString("server.database.dsn")) dialect = mysql.Open(config.GetString("server.database.dsn"))
} }
db, err := gorm.Open(dialect, &gorm.Config{}) db, err := gorm.Open(dialect, &gorm.Config{})
if err != nil{ if err != nil {
log.Fatalf("%v\n", err) log.Fatalf("%v\n", err)
} }
db.AutoMigrate(&models.User{}) db.AutoMigrate(&models.User{})
r := NewRouter(db)
r := NewRouter()
r.LoadHTMLGlob("templates/*.tmpl") r.LoadHTMLGlob("templates/*.tmpl")
r.Run( fmt.Sprintf("%v:%v", config.GetString("server.host"), config.GetString("server.port"))) r.Run(fmt.Sprintf("%v:%v", config.GetString("server.host"), config.GetString("server.port")))
} }

View File

@ -0,0 +1,5 @@
{{ define "footer.tmpl" }}
<footer>
IndieScrobble is a FLOSS service provided by <a href="https://brainsteam.co.uk">James Ravenscroft</a>. It is licensed under AGPL-3.0.
</footer>
{{end}}

View File

@ -6,7 +6,7 @@
<main> <main>
{{ if .user }} {{ if .user }}
Logged in as {{.user}} <a href="/logout"><button>Log Out</button></a> Logged in as {{.user.Me}} <a href="/logout"><button>Log Out</button></a>
<h2>Add A Scrobble</h2> <h2>Add A Scrobble</h2>
@ -32,5 +32,6 @@
</form> </form>
{{ end }} {{ end }}
</main> </main>
{{ template "footer.tmpl" . }}
</body> </body>
</html> </html>

View File

@ -9,7 +9,7 @@
{{ $scrobbleType := .scrobbleType }} {{ $scrobbleType := .scrobbleType }}
{{ if .user }} {{ if .user }}
Logged in as {{.user}} <a href="/logout"><button>Log Out</button></a> Logged in as {{.user.Me}} <a href="/logout"><button>Log Out</button></a>
{{end}} {{end}}
@ -38,6 +38,13 @@
<pre>{{.content}}</pre> <pre>{{.content}}</pre>
<input type="hidden" name="content" value="{{.content}}"/> <input type="hidden" name="content" value="{{.content}}"/>
{{if .config.SyndicateTargets}}
<h3>Syndication Options</h3>
{{ range $target := .config.SyndicateTargets}}
<label><input type="checkbox" name="mp-syndicate[]" value="{{$target.Uid}}" /> {{$target.Name}}</label><br />
{{end}}
{{end}}
<br/> <br/>
<button type="submit">Submit Post &gt; &gt;</button> <button type="submit">Submit Post &gt; &gt;</button>
@ -49,6 +56,7 @@
</main> </main>
{{ template "footer.tmpl" . }}
</body> </body>
</html> </html>
{{end}} {{end}}

View File

@ -80,10 +80,8 @@
</form> </form>
{{end}} {{end}}
</main> </main>
{{ template "footer.tmpl" . }}
</body> </body>
</html> </html>
{{end}} {{end}}