Implement initial login functionality
This commit is contained in:
parent
a3bd0db78c
commit
3aa79b0d23
36 changed files with 908 additions and 70 deletions
187
cmd/dis_user.go
Normal file
187
cmd/dis_user.go
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/cli"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DisUser is the 'dis_user' command
|
||||||
|
var DisUser wisski_distillery.Command = disUser{}
|
||||||
|
|
||||||
|
type disUser struct {
|
||||||
|
CreateUser bool `short:"c" long:"create" description:"create a new user"`
|
||||||
|
DeleteUser bool `short:"d" long:"delete" description:"delete a user"`
|
||||||
|
|
||||||
|
InfoUser bool `short:"i" long:"info" description:"show information about a user"`
|
||||||
|
ListUsers bool `short:"l" long:"list" description:"list all users"`
|
||||||
|
|
||||||
|
SetPassword bool `short:"s" long:"set-password" description:"interactively set a user password"`
|
||||||
|
UnsetPassword bool `short:"u" long:"unset-password" description:"delete a users password and block the account"`
|
||||||
|
CheckPassword bool `short:"p" long:"check-password" description:"interactively check a user password"`
|
||||||
|
|
||||||
|
Positionals struct {
|
||||||
|
User string `positional-arg-name:"USER" description:"username to manage. May be omitted for some actions"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (disUser) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: cli.Requirements{
|
||||||
|
NeedsDistillery: true,
|
||||||
|
},
|
||||||
|
Command: "dis_user",
|
||||||
|
Description: "manage distillery users",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errUserRequired = exit.Error{
|
||||||
|
Message: "`USER` argument is required",
|
||||||
|
ExitCode: exit.ExitCommandArguments,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (du disUser) AfterParse() error {
|
||||||
|
var counter int
|
||||||
|
for _, action := range []bool{
|
||||||
|
du.CreateUser,
|
||||||
|
du.InfoUser,
|
||||||
|
du.DeleteUser,
|
||||||
|
du.SetPassword,
|
||||||
|
du.UnsetPassword,
|
||||||
|
du.CheckPassword,
|
||||||
|
du.ListUsers,
|
||||||
|
} {
|
||||||
|
if action {
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if counter != 1 {
|
||||||
|
return errNoActionSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
if !du.ListUsers && du.Positionals.User == "" {
|
||||||
|
return errUserRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (du disUser) Run(context wisski_distillery.Context) error {
|
||||||
|
switch {
|
||||||
|
case du.InfoUser:
|
||||||
|
return du.runInfo(context)
|
||||||
|
case du.CreateUser:
|
||||||
|
return du.runCreate(context)
|
||||||
|
case du.DeleteUser:
|
||||||
|
return du.runDelete(context)
|
||||||
|
case du.SetPassword:
|
||||||
|
return du.runSetPassword(context)
|
||||||
|
case du.UnsetPassword:
|
||||||
|
return du.runUnsetPassword(context)
|
||||||
|
case du.CheckPassword:
|
||||||
|
return du.runCheckPassword(context)
|
||||||
|
case du.ListUsers:
|
||||||
|
return du.runListUsers(context)
|
||||||
|
}
|
||||||
|
panic("never reached")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (du disUser) runInfo(context wisski_distillery.Context) error {
|
||||||
|
user, err := context.Environment.Auth().User(context.Context, du.Positionals.User)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Println(user)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (du disUser) runCreate(context wisski_distillery.Context) error {
|
||||||
|
user, err := context.Environment.Auth().CreateUser(context.Context, du.Positionals.User)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Println(user)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (du disUser) runDelete(context wisski_distillery.Context) error {
|
||||||
|
user, err := context.Environment.Auth().User(context.Context, du.Positionals.User)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.Delete(context.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (du disUser) runSetPassword(context wisski_distillery.Context) error {
|
||||||
|
user, err := context.Environment.Auth().User(context.Context, du.Positionals.User)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwd string
|
||||||
|
{
|
||||||
|
context.Printf("Enter new password for user %s:", du.Positionals.User)
|
||||||
|
passwd1, err := context.IOStream.ReadPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
context.Println()
|
||||||
|
|
||||||
|
context.Printf("Enter the same password again:")
|
||||||
|
passwd, err = context.IOStream.ReadPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
context.Println()
|
||||||
|
|
||||||
|
if passwd != passwd1 {
|
||||||
|
return errPasswordsNotIdentical
|
||||||
|
}
|
||||||
|
if len(passwd) == 0 {
|
||||||
|
return errPasswordsNotIdentical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.SetPassword(context.Context, []byte(passwd))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (du disUser) runUnsetPassword(context wisski_distillery.Context) error {
|
||||||
|
user, err := context.Environment.Auth().User(context.Context, du.Positionals.User)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.UnsetPassword(context.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (du disUser) runCheckPassword(context wisski_distillery.Context) error {
|
||||||
|
user, err := context.Environment.Auth().User(context.Context, du.Positionals.User)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Printf("Enter password for %s:", du.Positionals.User)
|
||||||
|
|
||||||
|
candidate, err := context.IOStream.ReadPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
context.Println()
|
||||||
|
|
||||||
|
return user.CheckPassword(context.Context, []byte(candidate))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (du disUser) runListUsers(context wisski_distillery.Context) error {
|
||||||
|
users, err := context.Environment.Auth().Users(context.Context)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, user := range users {
|
||||||
|
context.Println(user)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -13,9 +13,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// DrupalUser is the 'drupal_user' setting
|
// DrupalUser is the 'drupal_user' setting
|
||||||
var DrupalUser wisski_distillery.Command = duser{}
|
var DrupalUser wisski_distillery.Command = drupalUser{}
|
||||||
|
|
||||||
type duser struct {
|
type drupalUser struct {
|
||||||
CheckCommonPasswords bool `short:"d" long:"check-common-passwords" description:"check for most common passwords. operates on all users concurrently."`
|
CheckCommonPasswords bool `short:"d" long:"check-common-passwords" description:"check for most common passwords. operates on all users concurrently."`
|
||||||
CheckPasswdInteractive bool `short:"c" long:"check-password" description:"interactively check user password"`
|
CheckPasswdInteractive bool `short:"c" long:"check-password" description:"interactively check user password"`
|
||||||
ResetPasswd bool `short:"r" long:"reset-password" description:"reset password for user"`
|
ResetPasswd bool `short:"r" long:"reset-password" description:"reset password for user"`
|
||||||
|
|
@ -26,7 +26,7 @@ type duser struct {
|
||||||
} `positional-args:"true"`
|
} `positional-args:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (duser) Description() wisski_distillery.Description {
|
func (drupalUser) Description() wisski_distillery.Description {
|
||||||
return wisski_distillery.Description{
|
return wisski_distillery.Description{
|
||||||
Requirements: cli.Requirements{
|
Requirements: cli.Requirements{
|
||||||
NeedsDistillery: true,
|
NeedsDistillery: true,
|
||||||
|
|
@ -46,7 +46,7 @@ var errUserParameter = exit.Error{
|
||||||
ExitCode: exit.ExitGeneric,
|
ExitCode: exit.ExitGeneric,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (du duser) AfterParse() error {
|
func (du drupalUser) AfterParse() error {
|
||||||
var count int
|
var count int
|
||||||
for _, s := range []bool{
|
for _, s := range []bool{
|
||||||
du.CheckCommonPasswords,
|
du.CheckCommonPasswords,
|
||||||
|
|
@ -74,7 +74,7 @@ var errPasswordsNotIdentical = exit.Error{
|
||||||
ExitCode: exit.ExitGeneric,
|
ExitCode: exit.ExitGeneric,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (du duser) Run(context wisski_distillery.Context) error {
|
func (du drupalUser) Run(context wisski_distillery.Context) error {
|
||||||
instance, err := context.Environment.Instances().WissKI(context.Context, du.Positionals.Slug)
|
instance, err := context.Environment.Instances().WissKI(context.Context, du.Positionals.Slug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -93,7 +93,7 @@ func (du duser) Run(context wisski_distillery.Context) error {
|
||||||
panic("never reached")
|
panic("never reached")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (du duser) login(context wisski_distillery.Context, instance *wisski.WissKI) error {
|
func (du drupalUser) login(context wisski_distillery.Context, instance *wisski.WissKI) error {
|
||||||
link, err := instance.Users().Login(context.Context, nil, du.Positionals.User)
|
link, err := instance.Users().Login(context.Context, nil, du.Positionals.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -107,7 +107,7 @@ var errPasswordFound = exit.Error{
|
||||||
ExitCode: 5,
|
ExitCode: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (du duser) checkCommonPassword(context wisski_distillery.Context, instance *wisski.WissKI) error {
|
func (du drupalUser) checkCommonPassword(context wisski_distillery.Context, instance *wisski.WissKI) error {
|
||||||
users := instance.Users()
|
users := instance.Users()
|
||||||
|
|
||||||
entities, err := users.All(context.Context, nil)
|
entities, err := users.All(context.Context, nil)
|
||||||
|
|
@ -132,7 +132,7 @@ func (du duser) checkCommonPassword(context wisski_distillery.Context, instance
|
||||||
}, entities)
|
}, entities)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (du duser) checkPasswordInteractive(context wisski_distillery.Context, instance *wisski.WissKI) error {
|
func (du drupalUser) checkPasswordInteractive(context wisski_distillery.Context, instance *wisski.WissKI) error {
|
||||||
validator, err := instance.Users().GetPasswordValidator(context.Context, du.Positionals.User)
|
validator, err := instance.Users().GetPasswordValidator(context.Context, du.Positionals.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -161,7 +161,7 @@ func (du duser) checkPasswordInteractive(context wisski_distillery.Context, inst
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (du duser) resetPassword(context wisski_distillery.Context, instance *wisski.WissKI) error {
|
func (du drupalUser) resetPassword(context wisski_distillery.Context, instance *wisski.WissKI) error {
|
||||||
context.Printf("Enter new password for user %s:", du.Positionals.User)
|
context.Printf("Enter new password for user %s:", du.Positionals.User)
|
||||||
passwd1, err := context.IOStream.ReadPassword()
|
passwd1, err := context.IOStream.ReadPassword()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ func init() {
|
||||||
wdcli.Register(cmd.DrupalSetting)
|
wdcli.Register(cmd.DrupalSetting)
|
||||||
wdcli.Register(cmd.DrupalUser)
|
wdcli.Register(cmd.DrupalUser)
|
||||||
|
|
||||||
|
// distillery auth
|
||||||
|
wdcli.Register(cmd.DisUser)
|
||||||
|
|
||||||
// backup & cron
|
// backup & cron
|
||||||
wdcli.Register(cmd.Snapshot)
|
wdcli.Register(cmd.Snapshot)
|
||||||
wdcli.Register(cmd.Backup)
|
wdcli.Register(cmd.Backup)
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -10,6 +10,7 @@ require (
|
||||||
github.com/gliderlabs/ssh v0.3.5
|
github.com/gliderlabs/ssh v0.3.5
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
|
github.com/gorilla/sessions v1.2.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
|
@ -25,6 +26,7 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
github.com/gosuri/uilive v0.0.4 // indirect
|
github.com/gosuri/uilive v0.0.4 // indirect
|
||||||
github.com/jessevdk/go-flags v1.5.0 // indirect
|
github.com/jessevdk/go-flags v1.5.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -16,6 +16,10 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||||
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
|
github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -87,6 +87,9 @@ type Config struct {
|
||||||
DisAdminUser string `env:"DIS_ADMIN_USER" default:"admin" parser:"nonempty"`
|
DisAdminUser string `env:"DIS_ADMIN_USER" default:"admin" parser:"nonempty"`
|
||||||
DisAdminPassword string `env:"DIS_ADMIN_PASSWORD" default:"" parser:"nonempty"`
|
DisAdminPassword string `env:"DIS_ADMIN_PASSWORD" default:"" parser:"nonempty"`
|
||||||
|
|
||||||
|
// session secret holds the secret for login
|
||||||
|
SessionSecret string `env:"SESSION_SECRET" default:"" parser:"nonempty"`
|
||||||
|
|
||||||
// name of docker network to use
|
// name of docker network to use
|
||||||
DockerNetworkName string `env:"DOCKER_NETWORK_NAME" default:"distillery" parser:"nonempty"`
|
DockerNetworkName string `env:"DOCKER_NETWORK_NAME" default:"distillery" parser:"nonempty"`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,9 +69,6 @@ GRAPHDB_ADMIN_PASSWORD=${GRAPHDB_ADMIN_PASSWORD}
|
||||||
MYSQL_ADMIN_USER=${MYSQL_ADMIN_USER}
|
MYSQL_ADMIN_USER=${MYSQL_ADMIN_USER}
|
||||||
MYSQL_ADMIN_PASSWORD=${MYSQL_ADMIN_PASSWORD}
|
MYSQL_ADMIN_PASSWORD=${MYSQL_ADMIN_PASSWORD}
|
||||||
|
|
||||||
# The admin user and password required to access the keycloak server and api
|
|
||||||
KEYCLOAK_ADMIN_USER=${KEYCLOAK_ADMIN_USER}
|
|
||||||
KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
|
|
||||||
|
|
||||||
# The admin user and password required to access the /dis/ server and api
|
# The admin user and password required to access the /dis/ server and api
|
||||||
DIS_ADMIN_USER=${DIS_ADMIN_USER}
|
DIS_ADMIN_USER=${DIS_ADMIN_USER}
|
||||||
|
|
@ -79,3 +76,6 @@ DIS_ADMIN_PASSWORD=${DIS_ADMIN_PASSWORD}
|
||||||
|
|
||||||
# the interval to run cron in
|
# the interval to run cron in
|
||||||
CRON_INTERVAL=10m
|
CRON_INTERVAL=10m
|
||||||
|
|
||||||
|
# The secret for sessions (for login etc)
|
||||||
|
SESSION_SECRET=${SESSION_SECRET}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ type Template struct {
|
||||||
DisAdminUsername string `env:"DIS_ADMIN_USER"`
|
DisAdminUsername string `env:"DIS_ADMIN_USER"`
|
||||||
DisAdminPassword string `env:"DIS_ADMIN_PASSWORD"`
|
DisAdminPassword string `env:"DIS_ADMIN_PASSWORD"`
|
||||||
DockerNetworkName string `env:"DOCKER_NETWORK_NAME"`
|
DockerNetworkName string `env:"DOCKER_NETWORK_NAME"`
|
||||||
|
SessionSecret string `env:"SESSION_SECRET"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults sets defaults on the template
|
// SetDefaults sets defaults on the template
|
||||||
|
|
@ -94,6 +95,13 @@ func (tpl *Template) SetDefaults(env environment.Environment) (err error) {
|
||||||
tpl.DockerNetworkName = `distillery-` + tpl.DockerNetworkName
|
tpl.DockerNetworkName = `distillery-` + tpl.DockerNetworkName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tpl.SessionSecret == "" {
|
||||||
|
tpl.SessionSecret, err = password.Password(100)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ func (dis *Distillery) init() {
|
||||||
lazy.RegisterPoolGroup[component.DistilleryFetcher](&dis.pool)
|
lazy.RegisterPoolGroup[component.DistilleryFetcher](&dis.pool)
|
||||||
lazy.RegisterPoolGroup[component.Installable](&dis.pool)
|
lazy.RegisterPoolGroup[component.Installable](&dis.pool)
|
||||||
lazy.RegisterPoolGroup[component.Provisionable](&dis.pool)
|
lazy.RegisterPoolGroup[component.Provisionable](&dis.pool)
|
||||||
lazy.RegisterPoolGroup[component.Servable](&dis.pool)
|
lazy.RegisterPoolGroup[component.Routeable](&dis.pool)
|
||||||
lazy.RegisterPoolGroup[component.Cronable](&dis.pool)
|
lazy.RegisterPoolGroup[component.Cronable](&dis.pool)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
internal/dis/component/auth/auth.go
Normal file
23
internal/dis/component/auth/auth.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Auth struct {
|
||||||
|
component.Base
|
||||||
|
Dependencies struct {
|
||||||
|
SQL *sql.SQL
|
||||||
|
}
|
||||||
|
|
||||||
|
storeOnce sync.Once
|
||||||
|
store sessions.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ component.Routeable = (*Auth)(nil)
|
||||||
|
)
|
||||||
31
internal/dis/component/auth/templates/login.html
Normal file
31
internal/dis/component/auth/templates/login.html
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{{ template "_base.html" . }}
|
||||||
|
{{ define "title" }}Login{{ end }}
|
||||||
|
|
||||||
|
{{ define "header/time" }}
|
||||||
|
<!-- no header/time -->
|
||||||
|
{{ end }}
|
||||||
|
{{ define "header"}}
|
||||||
|
<!-- no header -->
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<div class="pure-u-1">
|
||||||
|
|
||||||
|
{{ if .Message }}
|
||||||
|
<div>
|
||||||
|
{{ .Message }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<form class="pure-form" method="POST">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Login Required</legend>
|
||||||
|
|
||||||
|
<input type="text" name="username">
|
||||||
|
<input type="password" name="password">
|
||||||
|
<input type="submit" value="Login">
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
176
internal/dis/component/auth/user.go
Normal file
176
internal/dis/component/auth/user.go
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrUserNotFound is returned when a user is not found
|
||||||
|
var ErrUserNotFound = errors.New("user not found")
|
||||||
|
|
||||||
|
// Users returns all users in the database
|
||||||
|
func (auth *Auth) Users(ctx context.Context) (users []*AuthUser, err error) {
|
||||||
|
// query the user table
|
||||||
|
table, err := auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// find all the users
|
||||||
|
var dUsers []models.User
|
||||||
|
err = table.Find(&dUsers).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// and map them to high-level user objects
|
||||||
|
users = make([]*AuthUser, len(dUsers))
|
||||||
|
for i, user := range dUsers {
|
||||||
|
users[i] = &AuthUser{
|
||||||
|
User: user,
|
||||||
|
auth: auth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// User returns a single user.
|
||||||
|
// If the user does not exist, returns ErrUserNotFound.
|
||||||
|
func (auth *Auth) User(ctx context.Context, name string) (user *AuthUser, err error) {
|
||||||
|
// quick and dirty check for the empty username (which is not allowed)
|
||||||
|
if name == "" {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the user
|
||||||
|
table, err := auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user = &AuthUser{}
|
||||||
|
|
||||||
|
// find the user
|
||||||
|
res := table.Where(&models.User{User: name}).Find(&user.User)
|
||||||
|
err = res.Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the user was not found
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
user.auth = auth
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user and returns it.
|
||||||
|
// The user is not associated to any WissKIs, and has no password set.
|
||||||
|
func (auth *Auth) CreateUser(ctx context.Context, name string) (user *AuthUser, err error) {
|
||||||
|
// return the user
|
||||||
|
table, err := auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user = &AuthUser{
|
||||||
|
User: models.User{
|
||||||
|
User: name,
|
||||||
|
Enabled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the create statement
|
||||||
|
err = table.Create(&user.User).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.auth = auth
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthUser represents an authorized user
|
||||||
|
type AuthUser struct {
|
||||||
|
auth *Auth
|
||||||
|
models.User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (au AuthUser) String() string {
|
||||||
|
hasPassword := len(au.PasswordHash) > 0
|
||||||
|
return fmt.Sprintf("User{Name:%q,Enabled:%t,HasPassword:%t,Admin:%t}", au.User.User, au.User.Enabled, hasPassword, au.User.Admin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPassword sets the password for this user and turns the user on
|
||||||
|
func (au *AuthUser) SetPassword(ctx context.Context, password []byte) (err error) {
|
||||||
|
au.User.PasswordHash, err = bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
au.User.Enabled = true
|
||||||
|
return au.Save(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetPassword removes the password from this user, and disables them
|
||||||
|
func (au *AuthUser) UnsetPassword(ctx context.Context) error {
|
||||||
|
au.User.PasswordHash = nil
|
||||||
|
au.User.Enabled = false
|
||||||
|
return au.Save(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrUserDisabled = errors.New("user is disabled")
|
||||||
|
var ErrUserBlank = errors.New("user has no password set")
|
||||||
|
|
||||||
|
// CheckPassword checks if this user can login with the provided password.
|
||||||
|
// Returns nil on success, an error otherwise.
|
||||||
|
func (au *AuthUser) CheckPassword(ctx context.Context, password []byte) error {
|
||||||
|
if !au.User.Enabled {
|
||||||
|
return ErrUserDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(au.User.PasswordHash) == 0 {
|
||||||
|
return ErrUserDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return bcrypt.CompareHashAndPassword(au.User.PasswordHash, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeAdmin makes this user an admin, and saves the update in the database.
|
||||||
|
// If the user is already an admin, does not return an error.
|
||||||
|
func (au *AuthUser) MakeAdmin(ctx context.Context) error {
|
||||||
|
au.User.Admin = true
|
||||||
|
return au.Save(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRegular removes admin rights from this user.
|
||||||
|
// If this user is not an dmin, does not return an error.
|
||||||
|
func (au *AuthUser) MakeRegular(ctx context.Context) error {
|
||||||
|
au.User.Admin = true
|
||||||
|
return au.Save(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves the given user in the database
|
||||||
|
func (au *AuthUser) Save(ctx context.Context) error {
|
||||||
|
table, err := au.auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return table.Save(&au.User).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes the user from the database
|
||||||
|
func (au *AuthUser) Delete(ctx context.Context) error {
|
||||||
|
table, err := au.auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return table.Delete(&au.User).Error
|
||||||
|
}
|
||||||
260
internal/dis/component/auth/web.go
Normal file
260
internal/dis/component/auth/web.go
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (auth *Auth) Routes() []string {
|
||||||
|
return []string{"/auth/"}
|
||||||
|
}
|
||||||
|
|
||||||
|
type contextUserKey struct{}
|
||||||
|
|
||||||
|
var ctxUserKey = contextUserKey{}
|
||||||
|
|
||||||
|
const (
|
||||||
|
sessionCookieName = "distillery-session"
|
||||||
|
sessionUserKey = "user"
|
||||||
|
)
|
||||||
|
|
||||||
|
// session returns the session belonging to a request
|
||||||
|
func (auth *Auth) session(r *http.Request) (*sessions.Session, error) {
|
||||||
|
auth.storeOnce.Do(func() {
|
||||||
|
auth.store = sessions.NewCookieStore([]byte(auth.Config.SessionSecret))
|
||||||
|
})
|
||||||
|
return auth.store.Get(r, sessionCookieName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserOf returns the user logged into the given request.
|
||||||
|
// If there is no user associated with the given user, user and error will be nil.
|
||||||
|
//
|
||||||
|
// When no UserOf exists in the given session returns nil.
|
||||||
|
// An invalid session (for a UserOf)
|
||||||
|
func (auth *Auth) UserOf(r *http.Request) (user *AuthUser, err error) {
|
||||||
|
ctx := r.Context()
|
||||||
|
if user, ok := ctx.Value(ctxUserKey).(*AuthUser); ok && user != nil {
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// first read the session
|
||||||
|
sess, err := auth.session(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to read the name from the session
|
||||||
|
name, ok := sess.Values[sessionUserKey]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
nameS, ok := name.(string)
|
||||||
|
if !ok || nameS == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch the user, check if they still exist
|
||||||
|
user, err = auth.User(ctx, nameS)
|
||||||
|
if err == ErrUserNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// user isn't enabled
|
||||||
|
if !user.Enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the user
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeLogin marks the user as logged in on the given writer
|
||||||
|
func (auth *Auth) writeLogin(w http.ResponseWriter, r *http.Request, user *AuthUser) error {
|
||||||
|
sess, err := auth.session(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sess.Values[sessionUserKey] = user.User.User
|
||||||
|
return sess.Save(r, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeLogout logs out the user form the given session
|
||||||
|
func (auth *Auth) writeLogout(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
sess, err := auth.session(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sess.Options.MaxAge = -1
|
||||||
|
return sess.Save(r, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed "templates/login.html"
|
||||||
|
var loginHTMLStr string
|
||||||
|
var loginTemplate = static.AssetsAuthLogin.MustParseShared("login.html", loginHTMLStr)
|
||||||
|
|
||||||
|
var loginResponse = httpx.Response{
|
||||||
|
ContentType: "text/plain",
|
||||||
|
Body: []byte("user is signed in"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRoute returns the handler for the requested route
|
||||||
|
func (auth *Auth) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
|
||||||
|
router := httprouter.New()
|
||||||
|
|
||||||
|
router.Handler(http.MethodGet, route, auth.Protect(loginResponse, nil))
|
||||||
|
|
||||||
|
router.HandlerFunc(http.MethodGet, route+"login", auth.loginRoute)
|
||||||
|
router.HandlerFunc(http.MethodPost, route+"login", auth.loginRoute)
|
||||||
|
|
||||||
|
router.HandlerFunc(http.MethodGet, route+"logout", auth.logoutRoute)
|
||||||
|
|
||||||
|
return router, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type loginContext struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protect returns a new handler which requires a user to be logged in and pass the perm function.
|
||||||
|
//
|
||||||
|
// If an unauthenticated user attempts to access the returned handler, they are redirected to the login endpoint.
|
||||||
|
// When a user is logged in, and they pass the perm function (or the perm function is nil), the original handler is called.
|
||||||
|
func (auth *Auth) Protect(handler http.Handler, perm func(user *AuthUser, r *http.Request) (ok bool, err error)) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// load the user in the session
|
||||||
|
user, err := auth.UserOf(r)
|
||||||
|
if err != nil {
|
||||||
|
goto err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is no user in the session
|
||||||
|
// we need to login the user
|
||||||
|
if user == nil {
|
||||||
|
|
||||||
|
// we can't redirect anything other than GET
|
||||||
|
// (because it might be a form)
|
||||||
|
// => so we just return a forbidden
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
goto forbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect the user to the login endpoint, with the original URI as a return
|
||||||
|
dest := "/auth/login?next=" + url.QueryEscape(r.URL.RequestURI())
|
||||||
|
http.Redirect(w, r, dest, http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have a permission check, we need to call it
|
||||||
|
// to find out if the user is actually allowed to access the page
|
||||||
|
if perm != nil {
|
||||||
|
ok, err := perm(user, r)
|
||||||
|
if err != nil {
|
||||||
|
goto err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
goto forbidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the user into the session
|
||||||
|
r = r.WithContext(context.WithValue(r.Context(), ctxUserKey, user))
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
forbidden:
|
||||||
|
httpx.HTMLInterceptor.Intercept(w, r, httpx.ErrForbidden)
|
||||||
|
return
|
||||||
|
err:
|
||||||
|
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) loginRoute(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var message string
|
||||||
|
|
||||||
|
// try to read a user from the session
|
||||||
|
user, err := auth.UserOf(r)
|
||||||
|
if err != nil {
|
||||||
|
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user != nil {
|
||||||
|
goto success
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
default:
|
||||||
|
panic("never reached")
|
||||||
|
case http.MethodGet:
|
||||||
|
goto form
|
||||||
|
case http.MethodPost:
|
||||||
|
// parse the form!
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
message = "Login failed"
|
||||||
|
goto form
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the username and password
|
||||||
|
username := r.Form.Get("username")
|
||||||
|
password := r.Form.Get("password")
|
||||||
|
|
||||||
|
// make sure that the user exists
|
||||||
|
user, err := auth.User(r.Context(), username)
|
||||||
|
if err != nil {
|
||||||
|
message = "Login failed"
|
||||||
|
goto form
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the password (TODO: Support TOTP)
|
||||||
|
err = user.CheckPassword(r.Context(), []byte(password))
|
||||||
|
if err != nil {
|
||||||
|
message = "Login failed"
|
||||||
|
goto form
|
||||||
|
}
|
||||||
|
|
||||||
|
// and we logged the user in!
|
||||||
|
auth.writeLogin(w, r, user)
|
||||||
|
goto success
|
||||||
|
}
|
||||||
|
|
||||||
|
form:
|
||||||
|
httpx.WriteHTML(loginContext{
|
||||||
|
Message: message,
|
||||||
|
}, nil, loginTemplate, "", w, r)
|
||||||
|
return
|
||||||
|
success:
|
||||||
|
// get the destination
|
||||||
|
next := r.URL.Query().Get("next")
|
||||||
|
if next == "" || next[0] != '/' {
|
||||||
|
next = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// and redirect to it!
|
||||||
|
http.Redirect(w, r, next, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) logoutRoute(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// do the logout
|
||||||
|
auth.writeLogout(w, r)
|
||||||
|
|
||||||
|
// get the destination
|
||||||
|
next := r.URL.Query().Get("next")
|
||||||
|
if next == "" || next[0] != '/' {
|
||||||
|
next = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// and redirect to it!
|
||||||
|
http.Redirect(w, r, next, http.StatusSeeOther)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ import (
|
||||||
type Control struct {
|
type Control struct {
|
||||||
component.Base
|
component.Base
|
||||||
Dependencies struct {
|
Dependencies struct {
|
||||||
Servables []component.Servable
|
Routeables []component.Routeable
|
||||||
Cronables []component.Cronable
|
Cronables []component.Cronable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,12 @@ type Home struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ component.Servable = (*Home)(nil)
|
_ component.Routeable = (*Home)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (*Home) Routes() []string { return []string{"/"} }
|
func (*Home) Routes() []string { return []string{"/"} }
|
||||||
|
|
||||||
func (home *Home) Handler(ctx context.Context, route string) (http.Handler, error) {
|
func (home *Home) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
|
||||||
return home, nil
|
return home, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,12 @@ type Info struct {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ component.DistilleryFetcher = (*Info)(nil)
|
_ component.DistilleryFetcher = (*Info)(nil)
|
||||||
_ component.Servable = (*Info)(nil)
|
_ component.Routeable = (*Info)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (*Info) Routes() []string { return []string{"/dis/"} }
|
func (*Info) Routes() []string { return []string{"/dis/"} }
|
||||||
|
|
||||||
func (info *Info) Handler(ctx context.Context, route string) (handler http.Handler, err error) {
|
func (info *Info) HandleRoute(ctx context.Context, route string) (handler http.Handler, err error) {
|
||||||
|
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/cancel"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -12,20 +13,24 @@ import (
|
||||||
// The server may spawn background tasks, but these should be terminated once context closes.
|
// The server may spawn background tasks, but these should be terminated once context closes.
|
||||||
//
|
//
|
||||||
// Logging messages are directed to progress
|
// Logging messages are directed to progress
|
||||||
func (control *Control) Server(ctx context.Context, progress io.Writer) (*http.ServeMux, error) {
|
func (control *Control) Server(ctx context.Context, progress io.Writer) (http.Handler, error) {
|
||||||
// create a new mux
|
// create a new mux
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// add all the servable routes!
|
// add all the servable routes!
|
||||||
for _, s := range control.Dependencies.Servables {
|
for _, s := range control.Dependencies.Routeables {
|
||||||
for _, route := range s.Routes() {
|
for _, route := range s.Routes() {
|
||||||
zerolog.Ctx(ctx).Info().Str("component", s.Name()).Str("route", route).Msg("mounting route")
|
zerolog.Ctx(ctx).Info().Str("component", s.Name()).Str("route", route).Msg("mounting route")
|
||||||
handler, err := s.Handler(ctx, route)
|
handler, err := s.HandleRoute(ctx, route)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mux.Handle(route, handler)
|
mux.Handle(route, handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mux, nil
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r = r.WithContext(cancel.ValuesOf(r.Context(), ctx))
|
||||||
|
mux.ServeHTTP(w, r)
|
||||||
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ type Assets struct {
|
||||||
Styles string // <link> tags inserted by the asset
|
Styles string // <link> tags inserted by the asset
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:generate node build.mjs HomeHome ComponentsIndex ControlIndex ControlInstance InstanceComponentsIndex
|
//go:generate node build.mjs HomeHome ComponentsIndex ControlIndex ControlInstance InstanceComponentsIndex AuthLogin
|
||||||
|
|
||||||
// MustParse parses a new template from the given source
|
// MustParse parses a new template from the given source
|
||||||
// and calls [RegisterAssoc] on it.
|
// and calls [RegisterAssoc] on it.
|
||||||
|
|
|
||||||
|
|
@ -31,3 +31,9 @@ var AssetsInstanceComponentsIndex = Assets{
|
||||||
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/InstanceComponentsIndex.38d394c2.js"></script><script src="/static/InstanceComponentsIndex.38d394c2.js" nomodule="" defer></script>`,
|
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/InstanceComponentsIndex.38d394c2.js"></script><script src="/static/InstanceComponentsIndex.38d394c2.js" nomodule="" defer></script>`,
|
||||||
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/InstanceComponentsIndex.38d394c2.css">`,
|
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/InstanceComponentsIndex.38d394c2.css">`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssetsAuthLogin contains assets for the 'AuthLogin' entrypoint.
|
||||||
|
var AssetsAuthLogin = Assets{
|
||||||
|
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthLogin.38d394c2.js"></script><script src="/static/AuthLogin.38d394c2.js" nomodule="" defer></script>`,
|
||||||
|
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/AuthLogin.38d394c2.css">`,
|
||||||
|
}
|
||||||
|
|
|
||||||
0
internal/dis/component/control/static/dist/AuthLogin.38d394c2.css
vendored
Normal file
0
internal/dis/component/control/static/dist/AuthLogin.38d394c2.css
vendored
Normal file
0
internal/dis/component/control/static/dist/AuthLogin.38d394c2.js
vendored
Normal file
0
internal/dis/component/control/static/dist/AuthLogin.38d394c2.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/* nothing for now */
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// nothing for now
|
||||||
|
|
@ -15,7 +15,7 @@ type Static struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ component.Servable = (*Static)(nil)
|
_ component.Routeable = (*Static)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (*Static) Routes() []string { return []string{"/static/"} }
|
func (*Static) Routes() []string { return []string{"/static/"} }
|
||||||
|
|
@ -23,7 +23,7 @@ func (*Static) Routes() []string { return []string{"/static/"} }
|
||||||
//go:embed dist
|
//go:embed dist
|
||||||
var staticFS embed.FS
|
var staticFS embed.FS
|
||||||
|
|
||||||
func (static *Static) Handler(ctx context.Context, route string) (http.Handler, error) {
|
func (static *Static) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
|
||||||
// take the filesystem
|
// take the filesystem
|
||||||
fs, err := fs.Sub(staticFS, "dist")
|
fs, err := fs.Sub(staticFS, "dist")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,15 @@ type Resolver struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ component.Servable = (*Resolver)(nil)
|
_ component.Routeable = (*Resolver)(nil)
|
||||||
_ component.Cronable = (*Resolver)(nil)
|
_ component.Cronable = (*Resolver)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (resolver *Resolver) Routes() []string { return []string{"/go/", "/wisski/get/"} }
|
func (resolver *Resolver) Routes() []string { return []string{"/go/", "/wisski/get/"} }
|
||||||
|
|
||||||
func (resolver *Resolver) Handler(ctx context.Context, route string) (http.Handler, error) {
|
func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
|
||||||
|
logger := zerolog.Ctx(ctx)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
return resolver.handler.Get(func() (p wdresolve.ResolveHandler) {
|
return resolver.handler.Get(func() (p wdresolve.ResolveHandler) {
|
||||||
p.TrustXForwardedProto = true
|
p.TrustXForwardedProto = true
|
||||||
|
|
@ -47,13 +49,13 @@ func (resolver *Resolver) Handler(ctx context.Context, route string) (http.Handl
|
||||||
domainName := resolver.Config.DefaultDomain
|
domainName := resolver.Config.DefaultDomain
|
||||||
if domainName != "" {
|
if domainName != "" {
|
||||||
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domainName))] = fmt.Sprintf("https://$1.%s", domainName)
|
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domainName))] = fmt.Sprintf("https://$1.%s", domainName)
|
||||||
zerolog.Ctx(ctx).Info().Str("name", domainName).Msg("registering default domain")
|
logger.Info().Str("name", domainName).Msg("registering default domain")
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the extra domains!
|
// handle the extra domains!
|
||||||
for _, domain := range resolver.Config.SelfExtraDomains {
|
for _, domain := range resolver.Config.SelfExtraDomains {
|
||||||
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domain))] = fmt.Sprintf("https://$1.%s", domainName)
|
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domain))] = fmt.Sprintf("https://$1.%s", domainName)
|
||||||
zerolog.Ctx(ctx).Info().Str("name", domainName).Msg("registering legacy domain")
|
logger.Info().Str("name", domainName).Msg("registering legacy domain")
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve the prefixes
|
// resolve the prefixes
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Servable is a component that is servable
|
// Routeable is a component that is servable
|
||||||
type Servable interface {
|
type Routeable interface {
|
||||||
Component
|
Component
|
||||||
|
|
||||||
// Routes returns the routes served by this servable
|
// Routes returns the routes served by this servable
|
||||||
Routes() []string
|
Routes() []string
|
||||||
|
|
||||||
// Handler returns the handler for the requested route
|
// HandleRoute returns the handler for the requested route
|
||||||
Handler(ctx context.Context, route string) (http.Handler, error)
|
HandleRoute(ctx context.Context, route string) (http.Handler, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,11 @@ func (sql *SQL) Update(ctx context.Context, progress io.Writer) error {
|
||||||
&models.Lock{},
|
&models.Lock{},
|
||||||
models.LockTable,
|
models.LockTable,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"users",
|
||||||
|
&models.User{},
|
||||||
|
models.UserTable,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrate all of the tables!
|
// migrate all of the tables!
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/cron"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/cron"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/home"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/home"
|
||||||
|
|
@ -71,6 +72,9 @@ func (dis *Distillery) SQL() *sql.SQL {
|
||||||
func (dis *Distillery) SSH() *ssh2.SSH2 {
|
func (dis *Distillery) SSH() *ssh2.SSH2 {
|
||||||
return export[*ssh2.SSH2](dis)
|
return export[*ssh2.SSH2](dis)
|
||||||
}
|
}
|
||||||
|
func (dis *Distillery) Auth() *auth.Auth {
|
||||||
|
return export[*auth.Auth](dis)
|
||||||
|
}
|
||||||
|
|
||||||
func (dis *Distillery) Cron() *cron.Cron {
|
func (dis *Distillery) Cron() *cron.Cron {
|
||||||
return export[*cron.Cron](dis)
|
return export[*cron.Cron](dis)
|
||||||
|
|
@ -121,6 +125,9 @@ func (dis *Distillery) allComponents() []initFunc {
|
||||||
s.PollInterval = time.Second
|
s.PollInterval = time.Second
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// auth
|
||||||
|
auto[*auth.Auth],
|
||||||
|
|
||||||
// instances
|
// instances
|
||||||
auto[*instances.Instances],
|
auto[*instances.Instances],
|
||||||
auto[*meta.Meta],
|
auto[*meta.Meta],
|
||||||
|
|
|
||||||
15
internal/models/user.go
Normal file
15
internal/models/user.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
// UserTable is the name of the table the [`User`] model is stored in.
|
||||||
|
const UserTable = "users"
|
||||||
|
|
||||||
|
// User represents a distillery user
|
||||||
|
type User struct {
|
||||||
|
Pk uint `gorm:"column:pk;primaryKey"`
|
||||||
|
|
||||||
|
User string `gorm:"column:user;not null;unique"` // name of the user
|
||||||
|
PasswordHash []byte `gorm:"column:password"` // password of the user, hashed
|
||||||
|
|
||||||
|
Enabled bool `gorm:"enabled;not null"`
|
||||||
|
Admin bool `gorm:"column:admin;not null"`
|
||||||
|
}
|
||||||
24
pkg/cancel/values.go
Normal file
24
pkg/cancel/values.go
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package cancel
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// ValuesOf returns a new context that has the same deadline and cancelation behviour as parent.
|
||||||
|
// However when requesting values from the context, checks the values in context first.
|
||||||
|
func ValuesOf(parent, values context.Context) context.Context {
|
||||||
|
return &valuesOf{
|
||||||
|
Context: parent,
|
||||||
|
values: values,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type valuesOf struct {
|
||||||
|
context.Context
|
||||||
|
values context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vv *valuesOf) Value(key any) any {
|
||||||
|
if value := vv.values.Value(key); value != nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return vv.Context.Value(key)
|
||||||
|
}
|
||||||
|
|
@ -28,7 +28,7 @@ func (ei ErrInterceptor) Intercept(w http.ResponseWriter, r *http.Request, err e
|
||||||
res = ei.Fallback
|
res = ei.Fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
res.ServerHTTP(w, r)
|
res.ServeHTTP(w, r)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,13 +65,13 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
textInterceptor = StatusInterceptor("text/plain", func(code int, text string) ([]byte, error) {
|
TextInterceptor = StatusInterceptor("text/plain", func(code int, text string) ([]byte, error) {
|
||||||
return []byte(text), nil
|
return []byte(text), nil
|
||||||
})
|
})
|
||||||
jsonInterceptor = StatusInterceptor("application/json", func(code int, text string) ([]byte, error) {
|
JSONInterceptor = StatusInterceptor("application/json", func(code int, text string) ([]byte, error) {
|
||||||
return json.Marshal(map[string]any{"status": text, "code": code})
|
return json.Marshal(map[string]any{"status": text, "code": code})
|
||||||
})
|
})
|
||||||
htmlInterceptor = StatusInterceptor("text/html", func(code int, text string) ([]byte, error) {
|
HTMLInterceptor = StatusInterceptor("text/html", func(code int, text string) ([]byte, error) {
|
||||||
return []byte(`<!DOCTYPE HTML><title>` + text + `</title>` + text), nil
|
return []byte(`<!DOCTYPE HTML><title>` + text + `</title>` + text), nil
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,25 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WriteHTML writes a html response of type T to w.
|
||||||
|
// If an error occured, writes an error response instead.
|
||||||
|
func WriteHTML[T any](result T, err error, template *template.Template, templateName string, w http.ResponseWriter, r *http.Request) {
|
||||||
|
// intercept any errors
|
||||||
|
if HTMLInterceptor.Intercept(w, r, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write out the response as html
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
if templateName != "" {
|
||||||
|
template.ExecuteTemplate(w, templateName, result)
|
||||||
|
} else {
|
||||||
|
template.Execute(w, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type HTMLHandler[T any] struct {
|
type HTMLHandler[T any] struct {
|
||||||
Handler func(r *http.Request) (T, error)
|
Handler func(r *http.Request) (T, error)
|
||||||
|
|
||||||
|
|
@ -16,19 +35,5 @@ type HTMLHandler[T any] struct {
|
||||||
func (h HTMLHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h HTMLHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// call the function
|
// call the function
|
||||||
result, err := h.Handler(r)
|
result, err := h.Handler(r)
|
||||||
|
WriteHTML(result, err, h.Template, h.TemplateName, w, r)
|
||||||
// intercept any errors
|
|
||||||
if htmlInterceptor.Intercept(w, r, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// write out the response as json
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
|
|
||||||
if h.TemplateName != "" {
|
|
||||||
h.Template.ExecuteTemplate(w, h.TemplateName, result)
|
|
||||||
} else {
|
|
||||||
h.Template.Execute(w, result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ type Response struct {
|
||||||
StatusCode int // defaults to [http.StatusOK]
|
StatusCode int // defaults to [http.StatusOK]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (response Response) ServerHTTP(w http.ResponseWriter, r *http.Request) {
|
func (response Response) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if response.ContentType == "" {
|
if response.ContentType == "" {
|
||||||
response.ContentType = "text/plain"
|
response.ContentType = "text/plain"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,11 @@ func JSON[T any](f func(r *http.Request) (T, error)) JSONHandler[T] {
|
||||||
return JSONHandler[T](f)
|
return JSONHandler[T](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONHandler implements [http.Handler] by returning values as json to the caller.
|
// WriteJSON writes a JSON response of type T to w.
|
||||||
// In case of an error, a generic "internal server error" message is returned.
|
// If an error occured, writes an error response instead.
|
||||||
type JSONHandler[T any] func(r *http.Request) (T, error)
|
func WriteJSON[T any](result T, err error, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// ServeHTTP calls j(r) and returns json
|
|
||||||
func (j JSONHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// call the function
|
|
||||||
result, err := j(r)
|
|
||||||
|
|
||||||
// handle any errors
|
// handle any errors
|
||||||
if jsonInterceptor.Intercept(w, r, err) {
|
if JSONInterceptor.Intercept(w, r, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,3 +23,13 @@ func (j JSONHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(result)
|
json.NewEncoder(w).Encode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSONHandler implements [http.Handler] by returning values as json to the caller.
|
||||||
|
// In case of an error, a generic "internal server error" message is returned.
|
||||||
|
type JSONHandler[T any] func(r *http.Request) (T, error)
|
||||||
|
|
||||||
|
// ServeHTTP calls j(r) and returns json
|
||||||
|
func (j JSONHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
result, err := j(r)
|
||||||
|
WriteJSON(result, err, w, r)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ func (rh RedirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
url, code, err := rh(r)
|
url, code, err := rh(r)
|
||||||
|
|
||||||
// intercept the errors
|
// intercept the errors
|
||||||
if textInterceptor.Intercept(w, r, err) {
|
if TextInterceptor.Intercept(w, r, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue