Initial commit with user management
This commit is contained in:
commit
a1a928e7cb
98 changed files with 13042 additions and 0 deletions
192
authentication/pkg.go
Normal file
192
authentication/pkg.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
// External
|
||||
_ "git.gibonuddevalla.se/go/wrappederror"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
|
||||
// Standard
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
db *sqlx.DB
|
||||
log *slog.Logger
|
||||
secret []byte
|
||||
ExpireDays int
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Username string
|
||||
Name string
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, err error) { // {{{
|
||||
j, _ := json.Marshal(struct {
|
||||
OK bool
|
||||
Error string
|
||||
}{
|
||||
false,
|
||||
err.Error(),
|
||||
})
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
|
||||
func NewManager(db *sqlx.DB, log *slog.Logger, secret string, expireDays int) (mngr Manager, err error) { // {{{
|
||||
mngr.db = db
|
||||
mngr.log = log
|
||||
|
||||
mngr.secret, err = hex.DecodeString(secret)
|
||||
mngr.ExpireDays = expireDays
|
||||
return
|
||||
} // }}}
|
||||
func (mngr *Manager) GenerateToken(data map[string]any) (string, error) { // {{{
|
||||
// Create a new token object, specifying signing method and the claims
|
||||
// you would like it to contain.
|
||||
now := time.Now()
|
||||
data["iat"] = now.Unix()
|
||||
data["exp"] = now.Add(time.Hour * 24 * time.Duration(mngr.ExpireDays)).Unix()
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(data))
|
||||
|
||||
// Sign and get the complete encoded token as a string using the secret.
|
||||
return token.SignedString(mngr.secret)
|
||||
} // }}}
|
||||
func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
var request struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err := json.Unmarshal(body, &request)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
}
|
||||
|
||||
// Verify username and password against the db user table.
|
||||
authenticated, user, err := mngr.Authenticate(request.Username, request.Password)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
}
|
||||
|
||||
if !authenticated {
|
||||
httpError(w, errors.New("Authentication failed"))
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(struct {
|
||||
OK bool
|
||||
User User
|
||||
}{true, user})
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
|
||||
func (mngr *Manager) Authenticate(username, password string) (authenticated bool, user User, err error) { // {{{
|
||||
var row *sql.Row
|
||||
row = mngr.db.QueryRow(`
|
||||
SELECT id, username, name
|
||||
FROM public.user
|
||||
WHERE
|
||||
LOWER(username) = LOWER($1) AND
|
||||
password = password_hash(SUBSTRING(password FROM 1 FOR 32), $2::bytea)
|
||||
`,
|
||||
username,
|
||||
password,
|
||||
)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.Name)
|
||||
authenticated = user.ID > 0
|
||||
return
|
||||
} // }}}
|
||||
func (mngr *Manager) CreateUser(username, password, name string) (alreadyExists bool, err error) { // {{{
|
||||
_, err = mngr.db.Exec(`
|
||||
INSERT INTO public.user(username, password, name, totp)
|
||||
VALUES(
|
||||
$1,
|
||||
public.password_hash(
|
||||
/* salt in hex */
|
||||
ENCODE(public.gen_random_bytes(16), 'hex'),
|
||||
|
||||
/* password */
|
||||
$2::bytea
|
||||
),
|
||||
$3,
|
||||
''
|
||||
)
|
||||
`,
|
||||
username,
|
||||
password,
|
||||
name,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" {
|
||||
err = errors.New("User already exists")
|
||||
alreadyExists = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
} // }}}
|
||||
func (mngr *Manager) ChangePassword(username, currentPassword, newPassword string, forceChange bool) (changed bool, err error) { // {{{
|
||||
var res sql.Result
|
||||
|
||||
if forceChange {
|
||||
res, err = mngr.db.Exec(`
|
||||
UPDATE public.user
|
||||
SET
|
||||
"password" = public.password_hash(
|
||||
/* salt in hex */
|
||||
ENCODE(public.gen_random_bytes(16), 'hex'),
|
||||
|
||||
/* password */
|
||||
$2::bytea
|
||||
)
|
||||
WHERE
|
||||
username = $1
|
||||
|
||||
`,
|
||||
username,
|
||||
newPassword,
|
||||
)
|
||||
} else {
|
||||
res, err = mngr.db.Exec(`
|
||||
UPDATE public.user
|
||||
SET
|
||||
"password" = public.password_hash(
|
||||
/* salt in hex */
|
||||
ENCODE(public.gen_random_bytes(16), 'hex'),
|
||||
|
||||
/* password */
|
||||
$3::bytea
|
||||
)
|
||||
WHERE
|
||||
username = $1 AND
|
||||
"password" = public.password_hash(SUBSTRING(password FROM 1 FOR 32), $2::bytea)
|
||||
|
||||
`,
|
||||
username,
|
||||
currentPassword,
|
||||
newPassword,
|
||||
)
|
||||
}
|
||||
|
||||
var rowsAffected int64
|
||||
rowsAffected, err = res.RowsAffected()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
changed = (rowsAffected == 1)
|
||||
return
|
||||
} // }}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue