Added login function

This commit is contained in:
Magnus Åhall 2024-11-28 06:09:34 +01:00
parent a1a928e7cb
commit 9d08b056f3
2 changed files with 70 additions and 5 deletions

View File

@ -12,6 +12,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
@ -50,6 +51,7 @@ func NewManager(db *sqlx.DB, log *slog.Logger, secret string, expireDays int) (m
mngr.ExpireDays = expireDays mngr.ExpireDays = expireDays
return return
} // }}} } // }}}
func (mngr *Manager) GenerateToken(data map[string]any) (string, error) { // {{{ func (mngr *Manager) GenerateToken(data map[string]any) (string, error) { // {{{
// Create a new token object, specifying signing method and the claims // Create a new token object, specifying signing method and the claims
// you would like it to contain. // you would like it to contain.
@ -62,6 +64,30 @@ func (mngr *Manager) GenerateToken(data map[string]any) (string, error) { // {{{
// Sign and get the complete encoded token as a string using the secret. // Sign and get the complete encoded token as a string using the secret.
return token.SignedString(mngr.secret) return token.SignedString(mngr.secret)
} // }}} } // }}}
func (mngr *Manager) VerifyToken(tokenString string) (jwt.Claims, error) { // {{{
// Parse takes the token string and a function for looking up the key. The latter is especially
// useful if you use multiple keys for your application. The standard is to use 'kid' in the
// head of the token to identify which key to use, but the parsed token (head and claims) is provided
// to the callback, providing flexibility.
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
return mngr.secret, nil
})
if err != nil {
mngr.log.Error("jwt", "error", err)
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
return claims, nil
} else {
return nil, err
}
} // }}}
func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Request) { // {{{ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Request) { // {{{
var request struct { var request struct {
Username string Username string
@ -72,22 +98,38 @@ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Reques
err := json.Unmarshal(body, &request) err := json.Unmarshal(body, &request)
if err != nil { if err != nil {
httpError(w, err) httpError(w, err)
return
} }
// Verify username and password against the db user table. // Verify username and password against the db user table.
authenticated, user, err := mngr.Authenticate(request.Username, request.Password) authenticated, user, err := mngr.Authenticate(request.Username, request.Password)
if err != nil { if err != nil {
httpError(w, err) httpError(w, err)
return
} }
if !authenticated { if !authenticated {
httpError(w, errors.New("Authentication failed")) httpError(w, errors.New("Authentication failed"))
return
}
// A new token is generated with the information.
var token string
data := make(map[string]any)
data["uid"] = user.ID
data["login"] = user.Username
data["name"] = user.Name
token, err = mngr.GenerateToken(data)
if err != nil {
httpError(w, err)
return
} }
j, _ := json.Marshal(struct { j, _ := json.Marshal(struct {
OK bool OK bool
User User User User
}{true, user}) Token string
}{true, user, token})
w.Write(j) w.Write(j)
} // }}} } // }}}
@ -104,6 +146,15 @@ func (mngr *Manager) Authenticate(username, password string) (authenticated bool
password, password,
) )
err = row.Scan(&user.ID, &user.Username, &user.Name) err = row.Scan(&user.ID, &user.Username, &user.Name)
if err != nil && err.Error() == "sql: no rows in result set" {
err = nil
authenticated = false
return
}
if err != nil {
return
}
authenticated = user.ID > 0 authenticated = user.ID > 0
return return
} // }}} } // }}}

18
main.go
View File

@ -99,6 +99,7 @@ func main() { // {{{
} }
http.HandleFunc("/", rootHandler) http.HandleFunc("/", rootHandler)
http.HandleFunc("/login", pageLogin)
http.HandleFunc("/authenticate", AuthManager.AuthenticationHandler) http.HandleFunc("/authenticate", AuthManager.AuthenticationHandler)
http.HandleFunc("/service_worker.js", pageServiceWorker) http.HandleFunc("/service_worker.js", pageServiceWorker)
listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port) listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port)
@ -136,9 +137,8 @@ func pageServiceWorker(w http.ResponseWriter, r *http.Request) { // {{{
return return
} }
} // }}} } // }}}
func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{ func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{
// Index page is rendered since everything
// else are specifically handled.
page := HTMLTemplate.SimplePage{ page := HTMLTemplate.SimplePage{
Layout: "main", Layout: "main",
Page: "index", Page: "index",
@ -151,6 +151,20 @@ func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{
return return
} }
} // }}} } // }}}
func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{
page := HTMLTemplate.SimplePage{
Layout: "main",
Page: "login",
Version: VERSION,
}
err := Webengine.Render(page, w, r)
if err != nil {
w.Write([]byte(err.Error()))
return
}
} // }}}
func sessionFilter(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { // {{{ func sessionFilter(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { // {{{
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Filtered ") fmt.Printf("Filtered ")