initial commit

This commit is contained in:
ston1th 2022-07-10 01:10:31 +02:00
commit f48fa210bb
49 changed files with 4058 additions and 0 deletions

525
pkg/server/handler.go Normal file
View file

@ -0,0 +1,525 @@
// Copyright (C) 2022 Marius Schellenberger
package server
import (
"net/http"
"time"
"git.giftfish.de/ston1th/goacc/pkg/otp"
"git.giftfish.de/ston1th/jwt/v3"
)
type notFoundHandler struct {
s *Server
}
func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
newContext(w, r, nf.s).NotFound()
}
func jwtHandler(h ctxHandler) ctxHandler {
return func(ctx *Context) {
if c, err := ctx.Request.Cookie(cookieName); err == nil {
t, err := jwt.DecodeToken(c.Value)
if err != nil {
ctx.LogSetCookie(err, "decode failed")
ctx.Redirect(root, http.StatusFound)
return
}
if err = ctx.Srv.JWT.Verify(t); err != nil {
ctx.LogSetCookie(err, "verify failed")
ctx.Redirect(root, http.StatusFound)
return
}
if t.Claims.GetString(totpClaim) != "" {
path := ctx.Path()
switch path {
case totp, login, logout:
ctx.Token = *t
h(ctx)
default:
ctx.Redirect(totp, http.StatusFound)
}
return
}
if !ctx.Srv.DB.LockedOut(t.Claims.GetString(userClaim), t.Claims.GetString(createdClaim)) {
ctx.Token = *t
h(ctx)
return
}
if err = ctx.Srv.JWT.Invalidate(t); err != nil {
ctx.log.Error(err, "invalidation failed")
}
}
ctx.SetCookie(nil, 0)
h(ctx)
}
}
func totpAuthHandler(h ctxHandler) ctxHandler {
return func(ctx *Context) {
if ctx.Totp() != "" {
h(ctx)
return
}
ctx.Redirect(root, http.StatusFound)
}
}
func adminAuthHandler(h ctxHandler) ctxHandler {
return func(ctx *Context) {
if ctx.Admin() {
h(ctx)
return
}
ctx.Forbidden()
}
}
func userAuthHandler(h ctxHandler) ctxHandler {
return func(ctx *Context) {
if ctx.Admin() {
h(ctx)
return
}
if ctx.User() == ctx.Var("user") {
h(ctx)
return
}
ctx.Forbidden()
}
}
func authHandler(h ctxHandler) ctxHandler {
return func(ctx *Context) {
if ctx.LoggedOn() {
h(ctx)
return
}
ctx.Forbidden()
}
}
func indexHandler(ctx *Context) {
ctx.Redirect(login, http.StatusFound)
}
func profileHandler(ctx *Context) {
ctx.Redirect(uroot+"edit/"+ctx.User(), http.StatusFound)
}
func loginHandler(ctx *Context) {
log := ctx.log.WithName("login")
var (
data loginData
err error
)
data.LoginChallenge = ctx.Var("login_challenge")
ctx.Template("loginHandler")
ctx.Data = webData{
Title: "Login",
BodyTitle: "Login",
}
if ctx.LoggedOn() {
rdr := profile
if data.LoginChallenge != "" {
rdr, err = ctx.ApproveLogin(ctx.User(), data.LoginChallenge)
if err != nil {
ctx.Error(err)
return
}
}
ctx.Redirect(rdr, http.StatusFound)
return
}
switch ctx.Method() {
case "GET":
ctx.Data.Data = data
ctx.Exec()
case "POST":
data.LoginChallenge = ctx.Form("login_challenge")
data.User = ctx.Form("user")
ctx.Data.Data = data
if !ctx.CheckXsrf() {
return
}
password := ctx.Form("password")
if data.User == "" || password == "" {
ctx.Error("empty username or password")
return
}
u, err := ctx.Srv.DB.Login(data.User, password)
if err != nil {
log.Error(err, "login failed", "user", data.User)
ctx.Error("bad username or password")
return
}
remember := ctx.Form("remember")
if u.Secret != "" {
ctx.SetCookie(jwt.Claims{
totpClaim: data.User,
rememberClaim: remember,
}, time.Minute)
rdr := totp
if data.LoginChallenge != "" {
rdr += "?login_challenge=" + data.LoginChallenge
}
ctx.Redirect(rdr, http.StatusFound)
return
}
log.Info("user logged in", "user", data.User)
ctx.Login(u, remember)
rdr := profile
if data.LoginChallenge != "" {
rdr, err = ctx.ApproveLogin(data.User, data.LoginChallenge)
if err != nil {
ctx.Error(err)
return
}
}
ctx.Redirect(rdr, http.StatusFound)
return
}
}
func loginTotpHandler(ctx *Context) {
if ctx.LoggedOn() {
ctx.Redirect(profile, http.StatusFound)
return
}
ctx.Template("loginTotpHandler")
ctx.Data = webData{
Title: "TOTP Verification",
BodyTitle: "TOTP Veriftcation",
}
var data loginData
switch ctx.Method() {
case "GET":
data.LoginChallenge = ctx.Var("login_challenge")
ctx.Data.Data = data
ctx.Exec()
case "POST":
data.LoginChallenge = ctx.Form("login_challenge")
ctx.Data.Data = data
if !ctx.CheckXsrf() {
return
}
pin := ctx.Form("pin")
if pin == "" {
ctx.Error("empty pin")
return
}
user := ctx.Totp()
log := ctx.log.WithValues("user", user)
u, err := ctx.Srv.DB.Totp(user, pin)
if err != nil {
log.Error(err, "totp failed")
ctx.Error(err)
return
}
log.Info("totp successful")
ctx.Login(u, ctx.Remember())
rdr := root
if data.LoginChallenge != "" {
rdr, err = ctx.ApproveLogin(user, data.LoginChallenge)
if err != nil {
ctx.Error(err)
return
}
}
ctx.Redirect(rdr, http.StatusFound)
}
}
func consentHandler(ctx *Context) {
ctx.Template("consentHandler")
ctx.Data = webData{
Title: "Consent",
BodyTitle: "Consent",
}
var data consentData
switch ctx.Method() {
case "GET":
data.ConsentChallenge = ctx.Var("consent_challenge")
ctx.Data.Data = data
ci, err := ctx.Consent(data.ConsentChallenge)
if err != nil {
ctx.Error(err)
return
}
data.Data = ci
ctx.Data.Data = data
ctx.Exec()
case "POST":
data.ConsentChallenge = ctx.Form("consent_challenge")
ctx.Data.Data = data
if !ctx.CheckXsrf() {
return
}
// TODO html form
approved := true
var err error
rdr := root
if approved {
rdr, err = ctx.ApproveConsent(data.ConsentChallenge)
if err != nil {
ctx.Error(err)
return
}
} else {
rdr, err = ctx.RejectConsent(data.ConsentChallenge)
if err != nil {
ctx.Error(err)
return
}
}
ctx.Redirect(rdr, http.StatusFound)
}
}
func usersHandler(ctx *Context) {
if !ctx.LoggedOn() {
ctx.NotFound()
return
}
ctx.Template("usersHandler")
ctx.Data = webData{
Title: "Users",
BodyTitle: "Users",
User: ctx.User(),
}
us, err := ctx.Srv.DB.GetUsers()
if err != nil {
ctx.Data.Msg = err.Error()
}
ctx.Data.Data = us
ctx.Exec()
}
func userNewHandler(ctx *Context) {
ctx.Template("userNewHandler")
ctx.Data = webData{
Title: "New User",
BodyTitle: "New User",
}
switch ctx.Method() {
case "GET":
ctx.Exec()
case "POST":
if !ctx.CheckXsrf() {
return
}
user := ctx.Form("user")
email := ctx.Form("email")
password := ctx.Form("password")
repeat := ctx.Form("repeat")
admin := ctx.Form("admin")
adm := false
if admin == "0" {
adm = true
}
if user == "" || email == "" || password == "" {
ctx.Error("empty user, email or password")
return
}
if password != repeat {
ctx.Error("passwords do not match")
return
}
if err := ctx.Srv.DB.CreateUser(user, email, password, adm); err != nil {
ctx.Error(err)
return
}
ctx.log.Info("user created", "user", user, "actor", ctx.User())
ctx.Redirect(users, http.StatusFound)
}
}
func userEditHandler(ctx *Context) {
ctx.Template("userEditHandler")
ctx.Data = webData{
Title: "Edit User",
BodyTitle: "Edit User",
}
u, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
if err != nil {
ctx.Error(err)
return
}
ctx.Data.Data = u
switch ctx.Method() {
case "GET":
ctx.Exec()
case "POST":
if !ctx.CheckXsrf() {
return
}
user := ctx.Var("user")
log := ctx.log.WithValues("user", user, "actor", ctx.User())
password := ctx.Form("password")
repeat := ctx.Form("repeat")
admin := ctx.Form("admin")
adm := false
if admin == "0" {
adm = true
}
if password != repeat {
ctx.Error("passwords do not match")
return
}
if ctx.Admin() {
if err := ctx.Srv.DB.AdminUpdateUser(user, password, adm); err != nil {
ctx.Error(err)
return
}
log.Info("user updated", "admin", adm)
ctx.Redirect(users, http.StatusFound)
return
}
if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil {
ctx.Error(err)
return
}
log.Info("user updated")
ctx.Redirect(root, http.StatusFound)
}
}
func userTotpHandler(ctx *Context) {
ctx.Template("userTotpHandler")
ctx.Data = webData{
Title: "TOTP",
BodyTitle: "TOTP",
}
req := otp.Request{Username: ctx.Var("user")}
u, err := ctx.Srv.DB.GetUser(req.Username)
if err != nil {
ctx.Error(err)
return
}
if u.Secret == "" {
switch ctx.Method() {
case "GET":
req, err = otp.New(req.Username, ctx.Srv.Config.Issuer)
if err != nil {
ctx.Error(err)
return
}
case "POST":
req.URL = ctx.Form("url")
req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL)
if err != nil {
ctx.Error(err)
return
}
}
}
switch ctx.Method() {
case "GET":
ctx.Data.Data = req
ctx.Exec()
case "POST":
if !ctx.CheckXsrf() {
return
}
pin := ctx.Form("pin")
if pin == "" {
ctx.Error("empty pin")
return
}
ctx.Data.Data = req
secret := req.Secret
if u.Secret != "" {
secret = u.Secret
}
valid := otp.Validate(pin, secret)
user := ctx.User()
if user != req.Username && ctx.Admin() {
valid = true
}
if !valid {
ctx.Error("validation failed")
return
}
if err := ctx.Srv.DB.UpdateUserSecret(req.Username, req.Secret); err != nil {
ctx.Error(err)
return
}
ctx.log.Info("totp changed", "user", user, "enabled", req.Secret != "")
ctx.Redirect(uroot+"edit/"+req.Username, http.StatusFound)
}
}
func userUnlockHandler(ctx *Context) {
user := ctx.Var("user")
log := ctx.log.WithValues("user", user, "actor", ctx.User())
err := ctx.Srv.DB.UnlockUser(user)
if err != nil {
log.Error(err, "user unlock failed")
} else {
log.Info("user unlocked")
}
ctx.Redirect(users, http.StatusFound)
}
func userDelHandler(ctx *Context) {
ctx.Template("userDelHandler")
ctx.Data = webData{
Title: "Delete User",
BodyTitle: "Delete User",
}
switch ctx.Method() {
case "GET":
user, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
if err != nil {
ctx.Error(err)
return
}
ctx.Data.Data = user
ctx.Exec()
case "POST":
if !ctx.CheckXsrf() {
return
}
self := ctx.User()
user := ctx.Var("user")
err := ctx.Srv.DB.DeleteUser(user)
log := ctx.log.WithValues("user", user, "actor", self)
if err != nil {
log.Error(err, "delete failed")
} else {
log.Info("delete successful")
}
if user == self {
ctx.Redirect(logout, http.StatusFound)
return
}
ctx.Redirect(users, http.StatusFound)
}
}
func logoutHandler(ctx *Context) {
totp := false
user := ctx.User()
if user == "" {
user = ctx.Totp()
if user != "" {
totp = true
}
}
if user != "" {
ctx.Srv.JWT.Invalidate(&ctx.Token)
} else {
user = empty
}
ctx.log.Info("logout", "user", user, "totp", totp)
ctx.SetCookie(nil, 0)
ctx.Redirect(root, http.StatusFound)
}