1
Fork 0

Implement self service user password change

This commit is contained in:
jordy2254 2024-03-19 18:11:03 +00:00
parent 8cdc4cd136
commit a3b25f4afa
11 changed files with 508 additions and 11 deletions

View File

@ -33,6 +33,7 @@ require (
require (
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
@ -46,9 +47,18 @@ require (
github.com/jackc/pgx/v4 v4.16.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/matryer/moq v0.2.7 // indirect
github.com/mattn/go-sqlite3 v1.14.14 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/urfave/cli/v2 v2.8.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -21,6 +21,7 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -123,6 +124,7 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
github.com/matryer/moq v0.2.7 h1:RtpiPUM8L7ZSCbSwK+QcZH/E9tgqAkFjKQxsRs25b4w=
github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@ -153,6 +155,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
@ -179,6 +182,7 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/strukturag/libheif v1.12.0 h1:Z5V5lCC5xbI59V77b4kGXcHsOohA6tQIvzLGIjhLsfY=
github.com/strukturag/libheif v1.12.0/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks=
github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
github.com/vektah/gqlparser/v2 v2.4.6 h1:Yjzp66g6oVq93Jihbi0qhGnf/6zIWjcm8H6gA27zstE=
github.com/vektah/gqlparser/v2 v2.4.6/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
@ -186,6 +190,7 @@ github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlV
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
github.com/xor-gate/goexif2 v1.1.0 h1:OvTZ5iEvsDhRWFjV5xY3wT7uHFna28nSSP7ucau+cXQ=
github.com/xor-gate/goexif2 v1.1.0/go.mod h1:eRjn3VSkAwpNpxEx/CGmd0zg0JFGL3akrSMxnJ581AY=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@ -219,6 +224,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -243,6 +249,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -263,12 +270,14 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

View File

@ -173,6 +173,7 @@ type ComplexityRoot struct {
SetThumbnailDownsampleMethod func(childComplexity int, method models.ThumbnailFilter) int
ShareAlbum func(childComplexity int, albumID int, expire *time.Time, password *string) int
ShareMedia func(childComplexity int, mediaID int, expire *time.Time, password *string) int
UpdatePassword func(childComplexity int, currentPassword string, newPassword string) int
UpdateUser func(childComplexity int, id int, username *string, password *string, admin *bool) int
UserAddRootPath func(childComplexity int, id int, rootPath string) int
UserRemoveRootAlbum func(childComplexity int, userID int, albumID int) int
@ -189,6 +190,11 @@ type ComplexityRoot struct {
Type func(childComplexity int) int
}
PasswordChangeResult struct {
Message func(childComplexity int) int
Success func(childComplexity int) int
}
Query struct {
Album func(childComplexity int, id int, tokenCredentials *models.ShareTokenCredentials) int
FaceGroup func(childComplexity int, id int) int
@ -322,6 +328,7 @@ type MutationResolver interface {
ProtectShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error)
FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error)
UpdateUser(ctx context.Context, id int, username *string, password *string, admin *bool) (*models.User, error)
UpdatePassword(ctx context.Context, currentPassword string, newPassword string) (*models.PasswordChangeResult, error)
CreateUser(ctx context.Context, username string, password *string, admin bool) (*models.User, error)
DeleteUser(ctx context.Context, id int) (*models.User, error)
UserAddRootPath(ctx context.Context, id int, rootPath string) (*models.Album, error)
@ -1096,6 +1103,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.ShareMedia(childComplexity, args["mediaId"].(int), args["expire"].(*time.Time), args["password"].(*string)), true
case "Mutation.updatePassword":
if e.complexity.Mutation.UpdatePassword == nil {
break
}
args, err := ec.field_Mutation_updatePassword_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UpdatePassword(childComplexity, args["currentPassword"].(string), args["newPassword"].(string)), true
case "Mutation.updateUser":
if e.complexity.Mutation.UpdateUser == nil {
break
@ -1188,6 +1207,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Notification.Type(childComplexity), true
case "PasswordChangeResult.message":
if e.complexity.PasswordChangeResult.Message == nil {
break
}
return e.complexity.PasswordChangeResult.Message(childComplexity), true
case "PasswordChangeResult.success":
if e.complexity.PasswordChangeResult.Success == nil {
break
}
return e.complexity.PasswordChangeResult.Success(childComplexity), true
case "Query.album":
if e.complexity.Query.Album == nil {
break
@ -2259,6 +2292,30 @@ func (ec *executionContext) field_Mutation_shareMedia_args(ctx context.Context,
return args, nil
}
func (ec *executionContext) field_Mutation_updatePassword_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 string
if tmp, ok := rawArgs["currentPassword"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("currentPassword"))
arg0, err = ec.unmarshalNString2string(ctx, tmp)
if err != nil {
return nil, err
}
}
args["currentPassword"] = arg0
var arg1 string
if tmp, ok := rawArgs["newPassword"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("newPassword"))
arg1, err = ec.unmarshalNString2string(ctx, tmp)
if err != nil {
return nil, err
}
}
args["newPassword"] = arg1
return args, nil
}
func (ec *executionContext) field_Mutation_updateUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -6683,6 +6740,87 @@ func (ec *executionContext) fieldContext_Mutation_updateUser(ctx context.Context
return fc, nil
}
func (ec *executionContext) _Mutation_updatePassword(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_updatePassword(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().UpdatePassword(rctx, fc.Args["currentPassword"].(string), fc.Args["newPassword"].(string))
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAuthorized == nil {
return nil, errors.New("directive isAuthorized is not implemented")
}
return ec.directives.IsAuthorized(ctx, nil, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(*models.PasswordChangeResult); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.PasswordChangeResult`, tmp)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*models.PasswordChangeResult)
fc.Result = res
return ec.marshalNPasswordChangeResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPasswordChangeResult(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_updatePassword(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "success":
return ec.fieldContext_PasswordChangeResult_success(ctx, field)
case "message":
return ec.fieldContext_PasswordChangeResult_message(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type PasswordChangeResult", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_updatePassword_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return
}
return fc, nil
}
func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_createUser(ctx, field)
if err != nil {
@ -8305,6 +8443,91 @@ func (ec *executionContext) fieldContext_Notification_timeout(ctx context.Contex
return fc, nil
}
func (ec *executionContext) _PasswordChangeResult_success(ctx context.Context, field graphql.CollectedField, obj *models.PasswordChangeResult) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_PasswordChangeResult_success(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Success, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_PasswordChangeResult_success(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "PasswordChangeResult",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Boolean does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _PasswordChangeResult_message(ctx context.Context, field graphql.CollectedField, obj *models.PasswordChangeResult) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_PasswordChangeResult_message(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Message, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_PasswordChangeResult_message(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "PasswordChangeResult",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Query_siteInfo(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_siteInfo(ctx, field)
if err != nil {
@ -14755,6 +14978,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
return ec._Mutation_updateUser(ctx, field)
})
if out.Values[i] == graphql.Null {
invalids++
}
case "updatePassword":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_updatePassword(ctx, field)
})
if out.Values[i] == graphql.Null {
invalids++
}
@ -14969,6 +15201,38 @@ func (ec *executionContext) _Notification(ctx context.Context, sel ast.Selection
return out
}
var passwordChangeResultImplementors = []string{"PasswordChangeResult"}
func (ec *executionContext) _PasswordChangeResult(ctx context.Context, sel ast.SelectionSet, obj *models.PasswordChangeResult) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, passwordChangeResultImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("PasswordChangeResult")
case "success":
out.Values[i] = ec._PasswordChangeResult_success(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "message":
out.Values[i] = ec._PasswordChangeResult_message(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var queryImplementors = []string{"Query"}
func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@ -16682,6 +16946,20 @@ func (ec *executionContext) marshalNNotificationType2githubᚗcomᚋphotoviewᚋ
return v
}
func (ec *executionContext) marshalNPasswordChangeResult2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPasswordChangeResult(ctx context.Context, sel ast.SelectionSet, v models.PasswordChangeResult) graphql.Marshaler {
return ec._PasswordChangeResult(ctx, sel, &v)
}
func (ec *executionContext) marshalNPasswordChangeResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPasswordChangeResult(ctx context.Context, sel ast.SelectionSet, v *models.PasswordChangeResult) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._PasswordChangeResult(ctx, sel, v)
}
func (ec *executionContext) marshalNScannerResult2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐScannerResult(ctx context.Context, sel ast.SelectionSet, v models.ScannerResult) graphql.Marshaler {
return ec._ScannerResult(ctx, sel, &v)
}

View File

@ -63,6 +63,11 @@ type Pagination struct {
Offset *int `json:"offset"`
}
type PasswordChangeResult struct {
Success bool `json:"success"`
Message *string `json:"message"`
}
type ScannerResult struct {
Finished bool `json:"finished"`
Success bool `json:"success"`

View File

@ -73,6 +73,21 @@ func (u *UserPreferences) BeforeSave(tx *gorm.DB) error {
var ErrorInvalidUserCredentials = errors.New("invalid credentials")
func ValidateUserPassword(user *User, password string) error {
if user.Password == nil {
return errors.New("user does not have a password")
}
if err := bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)); err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return ErrorInvalidUserCredentials
} else {
return errors.Wrap(err, "compare user password hash")
}
}
return nil
}
func AuthorizeUser(db *gorm.DB, username string, password string) (*User, error) {
var user User
@ -84,16 +99,9 @@ func AuthorizeUser(db *gorm.DB, username string, password string) (*User, error)
return nil, errors.Wrap(result.Error, "failed to get user by username when authorizing")
}
if user.Password == nil {
return nil, errors.New("user does not have a password")
}
if err := bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)); err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {
return nil, ErrorInvalidUserCredentials
} else {
return nil, errors.Wrap(err, "compare user password hash")
}
err := ValidateUserPassword(&user, password)
if err != nil {
return nil, err
}
return &user, nil

View File

@ -200,7 +200,6 @@ func (r *mutationResolver) ChangeUserPreferences(ctx context.Context, language *
return &userPref, nil
}
// Admin queries
func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *string, password *string, admin *bool) (*models.User, error) {
db := r.DB(ctx)
@ -238,6 +237,39 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *str
return &user, nil
}
func (r *mutationResolver) UpdatePassword(ctx context.Context, currentPassword, password string) (*models.PasswordChangeResult, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if err := models.ValidateUserPassword(user, currentPassword); err != nil {
return createPasswordChangeErrorResult("Invalid current password")
}
hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return nil, err
}
hashedPass := string(hashedPassBytes)
user.Password = &hashedPass
if err := db.Save(&user).Error; err != nil {
return createPasswordChangeErrorResult("Failed to update user your password has not changed")
}
return &models.PasswordChangeResult{
Success: true,
}, nil
}
func createPasswordChangeErrorResult(msg string) (*models.PasswordChangeResult, error) {
result := &models.PasswordChangeResult{
Success: false,
Message: &msg,
}
return result, nil
}
func (r *mutationResolver) CreateUser(ctx context.Context, username string, password *string, admin bool) (*models.User, error) {
var user *models.User

View File

@ -136,6 +136,7 @@ type Mutation {
password: String
admin: Boolean
): User! @isAdmin
updatePassword(currentPassword: String!, newPassword: String!): PasswordChangeResult! @isAuthorized
"Create a new user"
createUser(
username: String!
@ -186,6 +187,12 @@ type Mutation {
detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup! @isAuthorized
}
type PasswordChangeResult {
success: Boolean!
message: String
}
type Subscription {
notification: Notification!
}

8
api/tools/tools.go Normal file
View File

@ -0,0 +1,8 @@
//go:build tools
// +build tools
package tools
import (
_ "github.com/99designs/gqlgen"
)

View File

@ -9,6 +9,7 @@ import ThumbnailPreferences from './ThumbnailPreferences'
import UsersTable from './Users/UsersTable'
import VersionInfo from './VersionInfo'
import classNames from 'classnames'
import PasswordChange from './Users/PasswordChange'
type SectionTitleProps = {
children: string
@ -43,6 +44,7 @@ const SettingsPage = () => {
return (
<Layout title={t('title.settings', 'Settings')}>
<UserPreferences />
{!isAdmin && <PasswordChange />}
{isAdmin && (
<>
<ScannerSection />

View File

@ -0,0 +1,115 @@
import React, { useState } from 'react'
import { Button, TextField } from '../../../primitives/form/Input'
import { ApolloError, gql, useMutation } from '@apollo/client'
import {
updatePassword,
updatePasswordVariables,
} from './__generated__/updatePassword'
import { SectionTitle } from '../SettingsPage'
export const USER_CHANGE_PASSWORD_MUTATION = gql`
mutation updatePassword($currentPassword: String!, $newPassword: String!) {
updatePassword(
currentPassword: $currentPassword
newPassword: $newPassword
) {
success
message
}
}
`
const initialState = {
password1: '',
password2: '',
currentPassword: '',
}
const errorMessage = (
data: updatePassword | null | undefined,
error: ApolloError | undefined
) => {
return (
<div>
{error && <span>Something went wrong</span>}
{data &&
data.updatePassword &&
(data.updatePassword.success ? (
<span>Successfully updated password</span>
) : (
<span style={{ color: 'red' }}>{data.updatePassword.message}</span>
))}
</div>
)
}
const PasswordChange = () => {
const [state, setState] = useState(initialState)
const [updatePassword, { data, error }] = useMutation<
updatePassword,
updatePasswordVariables
>(USER_CHANGE_PASSWORD_MUTATION, {
onCompleted: data => {
if (data?.updatePassword.success) {
setState(initialState)
}
},
})
function updateInput(
event: React.ChangeEvent<HTMLInputElement>,
key: string
) {
setState({
...state,
[key]: event.target.value,
})
}
return (
<div>
{/* TODO Add password confirmation field. */}
<SectionTitle nospace>Change Password</SectionTitle>
<TextField
type="password"
label="current password"
value={state.currentPassword}
onChange={e => updateInput(e, 'currentPassword')}
/>
<TextField
type="password"
label="new password"
value={state.password1}
onChange={e => updateInput(e, 'password1')}
/>
<TextField
type="password"
label="confirm password"
value={state.password2}
onChange={e => updateInput(e, 'password2')}
/>
{(state.password1 !== state.password2 && (
<span style={{ color: 'red' }}>Passwords do not match</span>
)) ||
(state.password1 !== '' && (
<Button
style={{ marginTop: '5px' }}
onClick={() =>
updatePassword({
variables: {
currentPassword: state.currentPassword,
newPassword: state.password1,
},
})
}
>
Update Password
</Button>
))}
{errorMessage(data, error)}
</div>
)
}
export default PasswordChange

View File

@ -0,0 +1,23 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: updatePassword
// ====================================================
export interface updatePassword_updatePassword {
__typename: 'PasswordChangeResult'
success: boolean
message: string | null
}
export interface updatePassword {
updatePassword: updatePassword_updatePassword
}
export interface updatePasswordVariables {
currentPassword: string
newPassword: string
}