Subject: [PATCH links] Adding GraphQL calls for audit logs.

From: Peter Sanchez <[email-masked]>
Date: Tue, 4 Mar 2025 07:43:01 -0600

GraphQL api version bump

Changelog-added: GraphQL calls for audit logs
Changelog-changed: GraphQL api version: minor version bump
---
This is the first step in adding auditlog to the user interface. Just
misisng now is a few handlers for cases where they are visible.

 api/gqlgen.yml                |    3 +
 api/graph/generated.go        | 1171 ++++++++++++++++++++++++++++-----
 api/graph/model/models_gen.go |   15 +
 api/graph/schema.graphqls     |   28 +
 api/graph/schema.resolvers.go |  681 ++++++++++++-------
 helpers.go                    |    4 +
 templates/restricted.html     |    2 +-
 7 files changed, 1529 insertions(+), 375 deletions(-)

diff --git a/api/gqlgen.yml b/api/gqlgen.yml
index 0c5c4f0..090a18f 100644
--- a/api/gqlgen.yml
+++ b/api/gqlgen.yml
@@ -64,3 +64,6 @@ models:
       - github.com/99designs/gqlgen/graphql.Int
       - github.com/99designs/gqlgen/graphql.Int64
       - github.com/99designs/gqlgen/graphql.Int32
+  AuditLog:
+    model:
+      - netlandish.com/x/gobwebs-auditlog.AuditLog
diff --git a/api/graph/generated.go b/api/graph/generated.go
index 633087f..1ebec44 100644
--- a/api/graph/generated.go
+++ b/api/graph/generated.go
@@ -19,6 +19,7 @@ import (
 	"github.com/99designs/gqlgen/graphql/introspection"
 	gqlparser "github.com/vektah/gqlparser/v2"
 	"github.com/vektah/gqlparser/v2/ast"
+	"netlandish.com/x/gobwebs-auditlog"
 )
 
 // region    ************************** generated!.gotpl **************************
@@ -41,6 +42,7 @@ type Config struct {
 }
 
 type ResolverRoot interface {
+	AuditLog() AuditLogResolver
 	BillingSettings() BillingSettingsResolver
 	Domain() DomainResolver
 	Mutation() MutationResolver
@@ -93,6 +95,20 @@ type ComplexityRoot struct {
 		Title          func(childComplexity int) int
 	}
 
+	AuditLog struct {
+		CreatedOn func(childComplexity int) int
+		Details   func(childComplexity int) int
+		EventType func(childComplexity int) int
+		IPAddress func(childComplexity int) int
+		Metadata  func(childComplexity int) int
+		UserID    func(childComplexity int) int
+	}
+
+	AuditLogCursor struct {
+		PageInfo func(childComplexity int) int
+		Result   func(childComplexity int) int
+	}
+
 	BaseURL struct {
 		Counter     func(childComplexity int) int
 		CreatedOn   func(childComplexity int) int
@@ -369,6 +385,7 @@ type ComplexityRoot struct {
 		GetAdminDomains       func(childComplexity int, input *model.GetAdminDomainInput) int
 		GetAdminOrgStats      func(childComplexity int, id int) int
 		GetAdminOrganizations func(childComplexity int, input *model.GetAdminOrganizationsInput) int
+		GetAuditLogs          func(childComplexity int, input *model.AuditLogInput) int
 		GetDomain             func(childComplexity int, id int) int
 		GetDomains            func(childComplexity int, orgSlug *string, service *model.DomainService) int
 		GetFeed               func(childComplexity int, input *model.GetFeedInput) int
@@ -437,6 +454,9 @@ type ComplexityRoot struct {
 	}
 }
 
+type AuditLogResolver interface {
+	Metadata(ctx context.Context, obj *auditlog.AuditLog) (map[string]interface{}, error)
+}
 type BillingSettingsResolver interface {
 	Status(ctx context.Context, obj *models.BillingSettings) (model.OrgBillingStatus, error)
 }
@@ -449,6 +469,7 @@ type DomainResolver interface {
 }
 type MutationResolver interface {
 	AddOrganization(ctx context.Context, input model.OrganizationInput) (*models.Organization, error)
+	UpdateOrganization(ctx context.Context, input *model.UpdateOrganizationInput) (*models.Organization, error)
 	AddLink(ctx context.Context, input *model.LinkInput) (*models.OrgLink, error)
 	UpdateLink(ctx context.Context, input *model.UpdateLinkInput) (*models.OrgLink, error)
 	DeleteLink(ctx context.Context, hash string) (*model.DeletePayload, error)
@@ -459,7 +480,6 @@ type MutationResolver interface {
 	Register(ctx context.Context, input *model.RegisterInput) (*models.User, error)
 	CompleteRegister(ctx context.Context, input *model.CompleteRegisterInput) (*models.User, error)
 	UpdateProfile(ctx context.Context, input *model.ProfileInput) (*models.User, error)
-	UpdateOrganization(ctx context.Context, input *model.UpdateOrganizationInput) (*models.Organization, error)
 	AddDomain(ctx context.Context, input model.DomainInput) (*models.Domain, error)
 	DeleteDomain(ctx context.Context, id int) (*model.DeletePayload, error)
 	AddLinkShort(ctx context.Context, input *model.LinkShortInput) (*models.LinkShort, error)
@@ -521,6 +541,7 @@ type QueryResolver interface {
 	Analytics(ctx context.Context, input model.AnalyticsInput) (*model.Analytics, error)
 	GetFeed(ctx context.Context, input *model.GetFeedInput) (*model.OrgLinkCursor, error)
 	GetFeedFollowing(ctx context.Context) ([]*models.Organization, error)
+	GetAuditLogs(ctx context.Context, input *model.AuditLogInput) (*model.AuditLogCursor, error)
 	GetUsers(ctx context.Context, input *model.GetUserInput) (*model.UserCursor, error)
 	GetUser(ctx context.Context, id int) (*models.User, error)
 	GetAdminOrganizations(ctx context.Context, input *model.GetAdminOrganizationsInput) (*model.OrganizationCursor, error)
@@ -691,6 +712,62 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Analytics.Title(childComplexity), true
 
+	case "AuditLog.createdOn":
+		if e.complexity.AuditLog.CreatedOn == nil {
+			break
+		}
+
+		return e.complexity.AuditLog.CreatedOn(childComplexity), true
+
+	case "AuditLog.details":
+		if e.complexity.AuditLog.Details == nil {
+			break
+		}
+
+		return e.complexity.AuditLog.Details(childComplexity), true
+
+	case "AuditLog.eventType":
+		if e.complexity.AuditLog.EventType == nil {
+			break
+		}
+
+		return e.complexity.AuditLog.EventType(childComplexity), true
+
+	case "AuditLog.ipAddress":
+		if e.complexity.AuditLog.IPAddress == nil {
+			break
+		}
+
+		return e.complexity.AuditLog.IPAddress(childComplexity), true
+
+	case "AuditLog.metadata":
+		if e.complexity.AuditLog.Metadata == nil {
+			break
+		}
+
+		return e.complexity.AuditLog.Metadata(childComplexity), true
+
+	case "AuditLog.userId":
+		if e.complexity.AuditLog.UserID == nil {
+			break
+		}
+
+		return e.complexity.AuditLog.UserID(childComplexity), true
+
+	case "AuditLogCursor.pageInfo":
+		if e.complexity.AuditLogCursor.PageInfo == nil {
+			break
+		}
+
+		return e.complexity.AuditLogCursor.PageInfo(childComplexity), true
+
+	case "AuditLogCursor.result":
+		if e.complexity.AuditLogCursor.Result == nil {
+			break
+		}
+
+		return e.complexity.AuditLogCursor.Result(childComplexity), true
+
 	case "BaseURL.counter":
 		if e.complexity.BaseURL.Counter == nil {
 			break
@@ -2192,6 +2269,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Query.GetAdminOrganizations(childComplexity, args["input"].(*model.GetAdminOrganizationsInput)), true
 
+	case "Query.getAuditLogs":
+		if e.complexity.Query.GetAuditLogs == nil {
+			break
+		}
+
+		args, err := ec.field_Query_getAuditLogs_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Query.GetAuditLogs(childComplexity, args["input"].(*model.AuditLogInput)), true
+
 	case "Query.getDomain":
 		if e.complexity.Query.GetDomain == nil {
 			break
@@ -2630,6 +2719,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
 		ec.unmarshalInputAdminBillingInput,
 		ec.unmarshalInputAdminDomainInput,
 		ec.unmarshalInputAnalyticsInput,
+		ec.unmarshalInputAuditLogInput,
 		ec.unmarshalInputCompleteRegisterInput,
 		ec.unmarshalInputDomainInput,
 		ec.unmarshalInputGetAdminDomainInput,
@@ -4134,6 +4224,38 @@ func (ec *executionContext) field_Query_getAdminOrganizations_argsInput(
 	return zeroVal, nil
 }
 
+func (ec *executionContext) field_Query_getAuditLogs_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	arg0, err := ec.field_Query_getAuditLogs_argsInput(ctx, rawArgs)
+	if err != nil {
+		return nil, err
+	}
+	args["input"] = arg0
+	return args, nil
+}
+func (ec *executionContext) field_Query_getAuditLogs_argsInput(
+	ctx context.Context,
+	rawArgs map[string]interface{},
+) (*model.AuditLogInput, error) {
+	// We won't call the directive if the argument is null.
+	// Set call_argument_directives_with_null to true to call directives
+	// even if the argument is null.
+	_, ok := rawArgs["input"]
+	if !ok {
+		var zeroVal *model.AuditLogInput
+		return zeroVal, nil
+	}
+
+	ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
+	if tmp, ok := rawArgs["input"]; ok {
+		return ec.unmarshalOAuditLogInput2ᚖlinksᚋapiᚋgraphᚋmodelᚐAuditLogInput(ctx, tmp)
+	}
+
+	var zeroVal *model.AuditLogInput
+	return zeroVal, nil
+}
+
 func (ec *executionContext) field_Query_getDomain_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -5949,6 +6071,371 @@ func (ec *executionContext) fieldContext_Analytics_qrList(_ context.Context, fie
 	return fc, nil
 }
 
+func (ec *executionContext) _AuditLog_userId(ctx context.Context, field graphql.CollectedField, obj *auditlog.AuditLog) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_AuditLog_userId(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.UserID, 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.(int)
+	fc.Result = res
+	return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_AuditLog_userId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "AuditLog",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _AuditLog_ipAddress(ctx context.Context, field graphql.CollectedField, obj *auditlog.AuditLog) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_AuditLog_ipAddress(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.IPAddress, 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.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_AuditLog_ipAddress(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "AuditLog",
+		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) _AuditLog_eventType(ctx context.Context, field graphql.CollectedField, obj *auditlog.AuditLog) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_AuditLog_eventType(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.EventType, 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.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_AuditLog_eventType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "AuditLog",
+		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) _AuditLog_details(ctx context.Context, field graphql.CollectedField, obj *auditlog.AuditLog) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_AuditLog_details(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.Details, 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.marshalOString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_AuditLog_details(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "AuditLog",
+		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) _AuditLog_metadata(ctx context.Context, field graphql.CollectedField, obj *auditlog.AuditLog) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_AuditLog_metadata(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 ec.resolvers.AuditLog().Metadata(rctx, obj)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(map[string]interface{})
+	fc.Result = res
+	return ec.marshalOMap2map(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_AuditLog_metadata(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "AuditLog",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Map does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _AuditLog_createdOn(ctx context.Context, field graphql.CollectedField, obj *auditlog.AuditLog) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_AuditLog_createdOn(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.CreatedOn, 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.(time.Time)
+	fc.Result = res
+	return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_AuditLog_createdOn(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "AuditLog",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Time does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _AuditLogCursor_result(ctx context.Context, field graphql.CollectedField, obj *model.AuditLogCursor) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_AuditLogCursor_result(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.Result, 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.([]*auditlog.AuditLog)
+	fc.Result = res
+	return ec.marshalNAuditLog2ᚕᚖnetlandishᚗcomᚋxᚋgobwebsᚑauditlogᚐAuditLog(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_AuditLogCursor_result(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "AuditLogCursor",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "userId":
+				return ec.fieldContext_AuditLog_userId(ctx, field)
+			case "ipAddress":
+				return ec.fieldContext_AuditLog_ipAddress(ctx, field)
+			case "eventType":
+				return ec.fieldContext_AuditLog_eventType(ctx, field)
+			case "details":
+				return ec.fieldContext_AuditLog_details(ctx, field)
+			case "metadata":
+				return ec.fieldContext_AuditLog_metadata(ctx, field)
+			case "createdOn":
+				return ec.fieldContext_AuditLog_createdOn(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type AuditLog", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _AuditLogCursor_pageInfo(ctx context.Context, field graphql.CollectedField, obj *model.AuditLogCursor) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_AuditLogCursor_pageInfo(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.PageInfo, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*model.PageInfo)
+	fc.Result = res
+	return ec.marshalOPageInfo2ᚖlinksᚋapiᚋgraphᚋmodelᚐPageInfo(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_AuditLogCursor_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "AuditLogCursor",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "cursor":
+				return ec.fieldContext_PageInfo_cursor(ctx, field)
+			case "hasNextPage":
+				return ec.fieldContext_PageInfo_hasNextPage(ctx, field)
+			case "hasPrevPage":
+				return ec.fieldContext_PageInfo_hasPrevPage(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name)
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _BaseURL_id(ctx context.Context, field graphql.CollectedField, obj *models.BaseURL) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_BaseURL_id(ctx, field)
 	if err != nil {
@@ -10334,7 +10821,120 @@ func (ec *executionContext) _Mutation_addOrganization(ctx context.Context, field
 	return ec.marshalNOrganization2ᚖlinksᚋmodelsᚐOrganization(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) fieldContext_Mutation_addOrganization(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+func (ec *executionContext) fieldContext_Mutation_addOrganization(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 "id":
+				return ec.fieldContext_Organization_id(ctx, field)
+			case "ownerId":
+				return ec.fieldContext_Organization_ownerId(ctx, field)
+			case "orgType":
+				return ec.fieldContext_Organization_orgType(ctx, field)
+			case "name":
+				return ec.fieldContext_Organization_name(ctx, field)
+			case "slug":
+				return ec.fieldContext_Organization_slug(ctx, field)
+			case "image":
+				return ec.fieldContext_Organization_image(ctx, field)
+			case "timezone":
+				return ec.fieldContext_Organization_timezone(ctx, field)
+			case "settings":
+				return ec.fieldContext_Organization_settings(ctx, field)
+			case "isActive":
+				return ec.fieldContext_Organization_isActive(ctx, field)
+			case "createdOn":
+				return ec.fieldContext_Organization_createdOn(ctx, field)
+			case "updatedOn":
+				return ec.fieldContext_Organization_updatedOn(ctx, field)
+			case "ownerName":
+				return ec.fieldContext_Organization_ownerName(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type Organization", 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_addOrganization_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Mutation_updateOrganization(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Mutation_updateOrganization(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().UpdateOrganization(rctx, fc.Args["input"].(*model.UpdateOrganizationInput))
+		}
+
+		directive1 := func(ctx context.Context) (interface{}, error) {
+			scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "ORGS")
+			if err != nil {
+				var zeroVal *models.Organization
+				return zeroVal, err
+			}
+			kind, err := ec.unmarshalNAccessKind2linksᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RW")
+			if err != nil {
+				var zeroVal *models.Organization
+				return zeroVal, err
+			}
+			if ec.directives.Access == nil {
+				var zeroVal *models.Organization
+				return zeroVal, errors.New("directive access is not implemented")
+			}
+			return ec.directives.Access(ctx, nil, directive0, scope, kind)
+		}
+
+		tmp, err := directive1(rctx)
+		if err != nil {
+			return nil, graphql.ErrorOnPath(ctx, err)
+		}
+		if tmp == nil {
+			return nil, nil
+		}
+		if data, ok := tmp.(*models.Organization); ok {
+			return data, nil
+		}
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/models.Organization`, 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.Organization)
+	fc.Result = res
+	return ec.marshalNOrganization2ᚖlinksᚋmodelsᚐOrganization(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_updateOrganization(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
 	fc = &graphql.FieldContext{
 		Object:     "Mutation",
 		Field:      field,
@@ -10377,7 +10977,7 @@ func (ec *executionContext) fieldContext_Mutation_addOrganization(ctx context.Co
 		}
 	}()
 	ctx = graphql.WithFieldContext(ctx, fc)
-	if fc.Args, err = ec.field_Mutation_addOrganization_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+	if fc.Args, err = ec.field_Mutation_updateOrganization_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
 		ec.Error(ctx, err)
 		return fc, err
 	}
@@ -11438,119 +12038,6 @@ func (ec *executionContext) fieldContext_Mutation_updateProfile(ctx context.Cont
 	return fc, nil
 }
 
-func (ec *executionContext) _Mutation_updateOrganization(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
-	fc, err := ec.fieldContext_Mutation_updateOrganization(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().UpdateOrganization(rctx, fc.Args["input"].(*model.UpdateOrganizationInput))
-		}
-
-		directive1 := func(ctx context.Context) (interface{}, error) {
-			scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "ORGS")
-			if err != nil {
-				var zeroVal *models.Organization
-				return zeroVal, err
-			}
-			kind, err := ec.unmarshalNAccessKind2linksᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RW")
-			if err != nil {
-				var zeroVal *models.Organization
-				return zeroVal, err
-			}
-			if ec.directives.Access == nil {
-				var zeroVal *models.Organization
-				return zeroVal, errors.New("directive access is not implemented")
-			}
-			return ec.directives.Access(ctx, nil, directive0, scope, kind)
-		}
-
-		tmp, err := directive1(rctx)
-		if err != nil {
-			return nil, graphql.ErrorOnPath(ctx, err)
-		}
-		if tmp == nil {
-			return nil, nil
-		}
-		if data, ok := tmp.(*models.Organization); ok {
-			return data, nil
-		}
-		return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/models.Organization`, 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.Organization)
-	fc.Result = res
-	return ec.marshalNOrganization2ᚖlinksᚋmodelsᚐOrganization(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) fieldContext_Mutation_updateOrganization(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 "id":
-				return ec.fieldContext_Organization_id(ctx, field)
-			case "ownerId":
-				return ec.fieldContext_Organization_ownerId(ctx, field)
-			case "orgType":
-				return ec.fieldContext_Organization_orgType(ctx, field)
-			case "name":
-				return ec.fieldContext_Organization_name(ctx, field)
-			case "slug":
-				return ec.fieldContext_Organization_slug(ctx, field)
-			case "image":
-				return ec.fieldContext_Organization_image(ctx, field)
-			case "timezone":
-				return ec.fieldContext_Organization_timezone(ctx, field)
-			case "settings":
-				return ec.fieldContext_Organization_settings(ctx, field)
-			case "isActive":
-				return ec.fieldContext_Organization_isActive(ctx, field)
-			case "createdOn":
-				return ec.fieldContext_Organization_createdOn(ctx, field)
-			case "updatedOn":
-				return ec.fieldContext_Organization_updatedOn(ctx, field)
-			case "ownerName":
-				return ec.fieldContext_Organization_ownerName(ctx, field)
-			}
-			return nil, fmt.Errorf("no field named %q was found under type Organization", 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_updateOrganization_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
-		ec.Error(ctx, err)
-		return fc, err
-	}
-	return fc, nil
-}
-
 func (ec *executionContext) _Mutation_addDomain(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Mutation_addDomain(ctx, field)
 	if err != nil {
@@ -19682,6 +20169,99 @@ func (ec *executionContext) fieldContext_Query_getFeedFollowing(_ context.Contex
 	return fc, nil
 }
 
+func (ec *executionContext) _Query_getAuditLogs(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Query_getAuditLogs(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.Query().GetAuditLogs(rctx, fc.Args["input"].(*model.AuditLogInput))
+		}
+
+		directive1 := func(ctx context.Context) (interface{}, error) {
+			scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "PROFILE")
+			if err != nil {
+				var zeroVal *model.AuditLogCursor
+				return zeroVal, err
+			}
+			kind, err := ec.unmarshalNAccessKind2linksᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RO")
+			if err != nil {
+				var zeroVal *model.AuditLogCursor
+				return zeroVal, err
+			}
+			if ec.directives.Access == nil {
+				var zeroVal *model.AuditLogCursor
+				return zeroVal, errors.New("directive access is not implemented")
+			}
+			return ec.directives.Access(ctx, nil, directive0, scope, kind)
+		}
+
+		tmp, err := directive1(rctx)
+		if err != nil {
+			return nil, graphql.ErrorOnPath(ctx, err)
+		}
+		if tmp == nil {
+			return nil, nil
+		}
+		if data, ok := tmp.(*model.AuditLogCursor); ok {
+			return data, nil
+		}
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/api/graph/model.AuditLogCursor`, 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.(*model.AuditLogCursor)
+	fc.Result = res
+	return ec.marshalNAuditLogCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐAuditLogCursor(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Query_getAuditLogs(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Query",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "result":
+				return ec.fieldContext_AuditLogCursor_result(ctx, field)
+			case "pageInfo":
+				return ec.fieldContext_AuditLogCursor_pageInfo(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type AuditLogCursor", 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_Query_getAuditLogs_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Query_getUsers(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Query_getUsers(ctx, field)
 	if err != nil {
@@ -23690,62 +24270,124 @@ func (ec *executionContext) unmarshalInputAdminDomainInput(ctx context.Context,
 	return it, nil
 }
 
-func (ec *executionContext) unmarshalInputAnalyticsInput(ctx context.Context, obj interface{}) (model.AnalyticsInput, error) {
-	var it model.AnalyticsInput
+func (ec *executionContext) unmarshalInputAnalyticsInput(ctx context.Context, obj interface{}) (model.AnalyticsInput, error) {
+	var it model.AnalyticsInput
+	asMap := map[string]interface{}{}
+	for k, v := range obj.(map[string]interface{}) {
+		asMap[k] = v
+	}
+
+	fieldsInOrder := [...]string{"id", "orgSlug", "type", "interval", "dateStart", "dateEnd"}
+	for _, k := range fieldsInOrder {
+		v, ok := asMap[k]
+		if !ok {
+			continue
+		}
+		switch k {
+		case "id":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
+			data, err := ec.unmarshalNInt2int(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.ID = data
+		case "orgSlug":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("orgSlug"))
+			data, err := ec.unmarshalNString2string(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.OrgSlug = data
+		case "type":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type"))
+			data, err := ec.unmarshalNString2string(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.Type = data
+		case "interval":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("interval"))
+			data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.Interval = data
+		case "dateStart":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dateStart"))
+			data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.DateStart = data
+		case "dateEnd":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dateEnd"))
+			data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.DateEnd = data
+		}
+	}
+
+	return it, nil
+}
+
+func (ec *executionContext) unmarshalInputAuditLogInput(ctx context.Context, obj interface{}) (model.AuditLogInput, error) {
+	var it model.AuditLogInput
 	asMap := map[string]interface{}{}
 	for k, v := range obj.(map[string]interface{}) {
 		asMap[k] = v
 	}
 
-	fieldsInOrder := [...]string{"id", "orgSlug", "type", "interval", "dateStart", "dateEnd"}
+	fieldsInOrder := [...]string{"userId", "orgSlug", "listingId", "after", "before", "limit"}
 	for _, k := range fieldsInOrder {
 		v, ok := asMap[k]
 		if !ok {
 			continue
 		}
 		switch k {
-		case "id":
-			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
-			data, err := ec.unmarshalNInt2int(ctx, v)
+		case "userId":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("userId"))
+			data, err := ec.unmarshalOInt2ᚖint(ctx, v)
 			if err != nil {
 				return it, err
 			}
-			it.ID = data
+			it.UserID = data
 		case "orgSlug":
 			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("orgSlug"))
-			data, err := ec.unmarshalNString2string(ctx, v)
+			data, err := ec.unmarshalOString2ᚖstring(ctx, v)
 			if err != nil {
 				return it, err
 			}
 			it.OrgSlug = data
-		case "type":
-			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type"))
-			data, err := ec.unmarshalNString2string(ctx, v)
+		case "listingId":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("listingId"))
+			data, err := ec.unmarshalOInt2ᚖint(ctx, v)
 			if err != nil {
 				return it, err
 			}
-			it.Type = data
-		case "interval":
-			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("interval"))
-			data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+			it.ListingID = data
+		case "after":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
+			data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v)
 			if err != nil {
 				return it, err
 			}
-			it.Interval = data
-		case "dateStart":
-			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dateStart"))
-			data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+			it.After = data
+		case "before":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
+			data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v)
 			if err != nil {
 				return it, err
 			}
-			it.DateStart = data
-		case "dateEnd":
-			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dateEnd"))
-			data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+			it.Before = data
+		case "limit":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("limit"))
+			data, err := ec.unmarshalOInt2ᚖint(ctx, v)
 			if err != nil {
 				return it, err
 			}
-			it.DateEnd = data
+			it.Limit = data
 		}
 	}
 
@@ -25726,6 +26368,136 @@ func (ec *executionContext) _Analytics(ctx context.Context, sel ast.SelectionSet
 	return out
 }
 
+var auditLogImplementors = []string{"AuditLog"}
+
+func (ec *executionContext) _AuditLog(ctx context.Context, sel ast.SelectionSet, obj *auditlog.AuditLog) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, auditLogImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("AuditLog")
+		case "userId":
+			out.Values[i] = ec._AuditLog_userId(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&out.Invalids, 1)
+			}
+		case "ipAddress":
+			out.Values[i] = ec._AuditLog_ipAddress(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&out.Invalids, 1)
+			}
+		case "eventType":
+			out.Values[i] = ec._AuditLog_eventType(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&out.Invalids, 1)
+			}
+		case "details":
+			out.Values[i] = ec._AuditLog_details(ctx, field, obj)
+		case "metadata":
+			field := field
+
+			innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._AuditLog_metadata(ctx, field, obj)
+				return res
+			}
+
+			if field.Deferrable != nil {
+				dfs, ok := deferred[field.Deferrable.Label]
+				di := 0
+				if ok {
+					dfs.AddField(field)
+					di = len(dfs.Values) - 1
+				} else {
+					dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+					deferred[field.Deferrable.Label] = dfs
+				}
+				dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+					return innerFunc(ctx, dfs)
+				})
+
+				// don't run the out.Concurrently() call below
+				out.Values[i] = graphql.Null
+				continue
+			}
+
+			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+		case "createdOn":
+			out.Values[i] = ec._AuditLog_createdOn(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&out.Invalids, 1)
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var auditLogCursorImplementors = []string{"AuditLogCursor"}
+
+func (ec *executionContext) _AuditLogCursor(ctx context.Context, sel ast.SelectionSet, obj *model.AuditLogCursor) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, auditLogCursorImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("AuditLogCursor")
+		case "result":
+			out.Values[i] = ec._AuditLogCursor_result(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "pageInfo":
+			out.Values[i] = ec._AuditLogCursor_pageInfo(ctx, field, obj)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var baseURLImplementors = []string{"BaseURL"}
 
 func (ec *executionContext) _BaseURL(ctx context.Context, sel ast.SelectionSet, obj *models.BaseURL) graphql.Marshaler {
@@ -26803,6 +27575,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 			if out.Values[i] == graphql.Null {
 				out.Invalids++
 			}
+		case "updateOrganization":
+			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+				return ec._Mutation_updateOrganization(ctx, field)
+			})
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
 		case "addLink":
 			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
 				return ec._Mutation_addLink(ctx, field)
@@ -26873,13 +27652,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 			if out.Values[i] == graphql.Null {
 				out.Invalids++
 			}
-		case "updateOrganization":
-			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
-				return ec._Mutation_updateOrganization(ctx, field)
-			})
-			if out.Values[i] == graphql.Null {
-				out.Invalids++
-			}
 		case "addDomain":
 			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
 				return ec._Mutation_addDomain(ctx, field)
@@ -28525,6 +29297,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
 					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
 			}
 
+			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
+		case "getAuditLogs":
+			field := field
+
+			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Query_getAuditLogs(ctx, field)
+				if res == graphql.Null {
+					atomic.AddUint32(&fs.Invalids, 1)
+				}
+				return res
+			}
+
+			rrm := func(ctx context.Context) graphql.Marshaler {
+				return ec.OperationContext.RootResolverMiddleware(ctx,
+					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+			}
+
 			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
 		case "getUsers":
 			field := field
@@ -29463,6 +30257,58 @@ func (ec *executionContext) unmarshalNAnalyticsInput2linksᚋapiᚋgraphᚋmodel
 	return res, graphql.ErrorOnPath(ctx, err)
 }
 
+func (ec *executionContext) marshalNAuditLog2ᚕᚖnetlandishᚗcomᚋxᚋgobwebsᚑauditlogᚐAuditLog(ctx context.Context, sel ast.SelectionSet, v []*auditlog.AuditLog) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalOAuditLog2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑauditlogᚐAuditLog(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	return ret
+}
+
+func (ec *executionContext) marshalNAuditLogCursor2linksᚋapiᚋgraphᚋmodelᚐAuditLogCursor(ctx context.Context, sel ast.SelectionSet, v model.AuditLogCursor) graphql.Marshaler {
+	return ec._AuditLogCursor(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNAuditLogCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐAuditLogCursor(ctx context.Context, sel ast.SelectionSet, v *model.AuditLogCursor) 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._AuditLogCursor(ctx, sel, v)
+}
+
 func (ec *executionContext) marshalNBaseURL2ᚕᚖlinksᚋmodelsᚐBaseURL(ctx context.Context, sel ast.SelectionSet, v []*models.BaseURL) graphql.Marshaler {
 	ret := make(graphql.Array, len(v))
 	var wg sync.WaitGroup
@@ -30737,6 +31583,21 @@ func (ec *executionContext) marshalOAnalyticData2ᚖlinksᚋapiᚋgraphᚋmodel
 	return ec._AnalyticData(ctx, sel, v)
 }
 
+func (ec *executionContext) marshalOAuditLog2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑauditlogᚐAuditLog(ctx context.Context, sel ast.SelectionSet, v *auditlog.AuditLog) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._AuditLog(ctx, sel, v)
+}
+
+func (ec *executionContext) unmarshalOAuditLogInput2ᚖlinksᚋapiᚋgraphᚋmodelᚐAuditLogInput(ctx context.Context, v interface{}) (*model.AuditLogInput, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := ec.unmarshalInputAuditLogInput(ctx, v)
+	return &res, graphql.ErrorOnPath(ctx, err)
+}
+
 func (ec *executionContext) marshalOBaseURL2ᚖlinksᚋmodelsᚐBaseURL(ctx context.Context, sel ast.SelectionSet, v *models.BaseURL) graphql.Marshaler {
 	if v == nil {
 		return graphql.Null
@@ -31041,6 +31902,22 @@ func (ec *executionContext) marshalOListingLink2ᚖlinksᚋmodelsᚐListingLink(
 	return ec._ListingLink(ctx, sel, v)
 }
 
+func (ec *executionContext) unmarshalOMap2map(ctx context.Context, v interface{}) (map[string]interface{}, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := graphql.UnmarshalMap(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOMap2map(ctx context.Context, sel ast.SelectionSet, v map[string]interface{}) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	res := graphql.MarshalMap(v)
+	return res
+}
+
 func (ec *executionContext) unmarshalOMemberInput2ᚖlinksᚋapiᚋgraphᚋmodelᚐMemberInput(ctx context.Context, v interface{}) (*model.MemberInput, error) {
 	if v == nil {
 		return nil, nil
diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go
index 3052017..d78ce72 100644
--- a/api/graph/model/models_gen.go
+++ b/api/graph/model/models_gen.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"github.com/99designs/gqlgen/graphql"
+	"netlandish.com/x/gobwebs-auditlog"
 )
 
 type AddListingInput struct {
@@ -105,6 +106,20 @@ type AnalyticsInput struct {
 	DateEnd   *string `json:"dateEnd,omitempty"`
 }
 
+type AuditLogCursor struct {
+	Result   []*auditlog.AuditLog `json:"result"`
+	PageInfo *PageInfo            `json:"pageInfo,omitempty"`
+}
+
+type AuditLogInput struct {
+	UserID    *int    `json:"userId,omitempty"`
+	OrgSlug   *string `json:"orgSlug,omitempty"`
+	ListingID *int    `json:"listingId,omitempty"`
+	After     *Cursor `json:"after,omitempty"`
+	Before    *Cursor `json:"before,omitempty"`
+	Limit     *int    `json:"limit,omitempty"`
+}
+
 type CompleteRegisterInput struct {
 	Name     string `json:"name"`
 	Username string `json:"username"`
diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls
index 79f72eb..2f54459 100644
--- a/api/graph/schema.graphqls
+++ b/api/graph/schema.graphqls
@@ -4,6 +4,7 @@
 scalar Time
 scalar Upload
 scalar Cursor
+scalar Map
 
 """
 This is used to decorate fields which are only accessible by superuser.
@@ -304,6 +305,15 @@ type Domain {
     updatedOn: Time! @access(scope: DOMAINS, kind: RO)
 }
 
+type AuditLog {
+    userId:    Int!
+    ipAddress: String!
+    eventType: String!
+    details: String
+    metadata: Map
+    createdOn: Time!
+}
+
 """
 This is used in various cursor objects for pagination
 """
@@ -372,6 +382,12 @@ type ListingLinkCursor {
     pageInfo: PageInfo
 }
 
+type AuditLogCursor {
+    result: [AuditLog]!
+    pageInfo: PageInfo
+}
+
+
 type AnalyticData {
     key: String!
     val: Int!
@@ -734,6 +750,15 @@ type FollowPayload {
     message: String!
 }
 
+input AuditLogInput {
+    userId: Int
+    orgSlug: String
+    listingId: Int
+    after: Cursor
+    before: Cursor
+    limit: Int
+}
+
 type Query {
     "Returns API version information."
     version: Version!
@@ -798,6 +823,9 @@ type Query {
     "Returns an array of organizations that the calling user follows"
     getFeedFollowing: [Organization!]! @access(scope: PROFILE, kind: RO)
 
+    "Returns audit log entries for given parameters"
+    getAuditLogs(input: AuditLogInput): AuditLogCursor! @access(scope: PROFILE, kind: RO)
+
     #
     # Admin only. Not open to public calls
     #
diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go
index 60ca18d..7bdc796 100644
--- a/api/graph/schema.resolvers.go
+++ b/api/graph/schema.resolvers.go
@@ -39,6 +39,7 @@ import (
 	"golang.org/x/image/draw"
 	"golang.org/x/net/idna"
 	"netlandish.com/x/gobwebs"
+	auditlog "netlandish.com/x/gobwebs-auditlog"
 	oauth2 "netlandish.com/x/gobwebs-oauth2"
 	gaccounts "netlandish.com/x/gobwebs/accounts"
 	gcore "netlandish.com/x/gobwebs/core"
@@ -51,6 +52,11 @@ import (
 	"netlandish.com/x/gobwebs/validate"
 )
 
+// Metadata is the resolver for the metadata field.
+func (r *auditLogResolver) Metadata(ctx context.Context, obj *auditlog.AuditLog) (map[string]interface{}, error) {
+	return obj.Metadata, nil
+}
+
 // Status is the resolver for the status field.
 func (r *billingSettingsResolver) Status(ctx context.Context, obj *models.BillingSettings) (model.OrgBillingStatus, error) {
 	return model.OrgBillingStatus(obj.Status), nil
@@ -199,6 +205,200 @@ func (r *mutationResolver) AddOrganization(ctx context.Context, input model.Orga
 	return org, nil
 }
 
+// UpdateOrganization is the resolver for the updateOrganization field.
+func (r *mutationResolver) UpdateOrganization(ctx context.Context, input *model.UpdateOrganizationInput) (*models.Organization, error) {
+	tokenUser := oauth2.ForContext(ctx)
+	if tokenUser == nil {
+		return nil, valid.ErrAuthorization
+	}
+	user := tokenUser.User.(*models.User)
+	lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
+	lt := localizer.GetLocalizer(lang)
+
+	validator := valid.New(ctx)
+	validator.Expect(input.CurrentSlug != "", "%s", lt.Translate("CurrentSlug is required")).
+		WithField("currentSlug").
+		WithCode(valid.ErrValidationCode)
+	validator.Expect(input.Name != "", "%s", lt.Translate("Name is required")).
+		WithField("name").
+		WithCode(valid.ErrValidationCode)
+	validator.Expect(len(input.Name) < 150, "%s", lt.Translate("Name may not exceed 150 characters")).
+		WithField("name").
+		WithCode(valid.ErrValidationCode)
+	validator.Expect(input.Slug != "", "%s", lt.Translate("Slug is required")).
+		WithField("slug").
+		WithCode(valid.ErrValidationCode)
+	validator.Expect(len(input.Slug) < 150, "%s", lt.Translate("Slug may not exceed 150 characters")).
+		WithField("slug").
+		WithCode(valid.ErrValidationCode)
+
+	if !validator.Ok() {
+		return nil, nil
+	}
+
+	org, err := user.GetOrgsSlug(ctx, models.OrgUserPermissionAdminWrite, input.CurrentSlug)
+	if err != nil {
+		return nil, err
+	}
+	if org == nil {
+		validator.Error(
+			"%s", lt.Translate("Organization Not Found")).
+			WithField("name").
+			WithCode(valid.ErrNotFoundCode)
+		return nil, nil
+	}
+
+	// If the org name changed, validate it
+	var (
+		opts *database.FilterOptions
+		orgs []*models.Organization
+	)
+	if input.Name != org.Name {
+		opts = &database.FilterOptions{
+			Filter: sq.And{
+				sq.Eq{"o.name": input.Name},
+				sq.Eq{"o.owner_id": user.ID},
+			},
+			Limit: 1,
+		}
+		orgs, err = models.GetOrganizations(ctx, opts)
+		if err != nil {
+			return nil, err
+		}
+
+		if len(orgs) > 0 {
+			validator.Error(
+				"%s", lt.Translate("This organization name is already registered")).
+				WithField("name").
+				WithCode(valid.ErrValidationCode)
+			return nil, nil
+		}
+
+	}
+
+	// If the org slug changed, validate it
+	slug := links.Slugify(input.Slug)
+	if slug != org.Slug {
+		b := &gaccounts.BlacklistValidator{}
+		if !b.UsernameSafePlus(links.InvalidSlugs, slug) {
+			validator.Error("%s", lt.Translate("This slug can not be used. Please chose another one")).
+				WithField("slug").
+				WithCode(valid.ErrValidationCode)
+			return nil, nil
+		}
+
+		opts = &database.FilterOptions{
+			Filter: sq.Eq{"o.slug": slug},
+			Limit:  1,
+		}
+		orgs, err = models.GetOrganizations(ctx, opts)
+		if err != nil {
+			return nil, err
+		}
+
+		if len(orgs) > 0 {
+			validator.Error(
+				"%s", lt.Translate("This organization slug is already registered")).
+				WithField("slug").
+				WithCode(valid.ErrValidationCode)
+			return nil, nil
+		}
+	}
+
+	org.Name = input.Name
+	org.Slug = slug
+
+	// If the param was sent and if it is true
+	if input.DeleteImg != nil && *input.DeleteImg {
+		org.Image = ""
+	} else if input.Image != nil {
+		path, err := links.StoreImage(ctx, input.Image)
+		if err != nil {
+			return nil, err
+		}
+		org.Image = path
+	}
+
+	// The org is going to be disabled or enabled
+	if input.IsActive != nil {
+		if org.OrgType == models.OrgTypeUser {
+			validator.Error("%s", lt.Translate("You are not allowed to enable/disabled user type organization")).
+				WithField("isActive").
+				WithCode(valid.ErrValidationCode)
+			return nil, nil
+		}
+
+		if *input.IsActive {
+			// If we want re-enable it, we have to check the limit of free orgs
+			opts := &database.FilterOptions{
+				Filter: sq.And{
+					sq.NotEq{"o.id": org.ID},
+					sq.Eq{"o.owner_id": user.ID},
+					sq.Eq{"o.is_active": true},
+					sq.Eq{"(o.settings->'billing'->>'status')": models.BillingStatusFree},
+				},
+			}
+
+			freeOrgs, err := models.GetOrganizations(ctx, opts)
+			if err != nil {
+				return nil, err
+			}
+			if len(freeOrgs) > 1 {
+				validator.Error("%s", lt.Translate("You are not allowed to have more than two free organizations. Please upgrade")).
+					WithCode(valid.ErrValidationGlobalCode)
+				return nil, nil
+			}
+		} else if org.Settings.Billing.Status == models.BillingStatusPersonal ||
+			org.Settings.Billing.Status == models.BillingStatusBusiness {
+			validator.Error("%s", lt.Translate("This organization has an active subscription. Please cancel it first")).
+				WithCode(valid.ErrValidationGlobalCode)
+			return nil, nil
+		}
+		org.IsActive = *input.IsActive
+	}
+
+	if input.DefaultPerm != nil {
+		if org.Settings.Billing.Status == models.BillingStatusFree &&
+			string(*input.DefaultPerm) == models.OrgLinkVisibilityPrivate {
+			validator.Error("%s", lt.Translate(
+				"Free organizations can not use Private permission. Please upgrade to use "+
+					"Private permission")).
+				WithField("defaultPerm").
+				WithCode(valid.ErrValidationCode)
+			return nil, nil
+		}
+		if !models.ValidateLinkVisibility(string(*input.DefaultPerm)) {
+			validator.Error("%s", lt.Translate("Invalid default permission value")).
+				WithField("defaultPerm").
+				WithCode(valid.ErrValidationCode)
+			return nil, nil
+		}
+		org.Settings.DefaultPerm = string(*input.DefaultPerm)
+	}
+
+	err = org.Store(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	c := server.EchoForContext(ctx)
+	mdata := make(map[string]any)
+	mdata["org_id"] = org.ID
+	err = models.RecordAuditLog(
+		ctx,
+		int(user.ID),
+		c.RealIP(),
+		models.LOG_ORG_UPDATED,
+		fmt.Sprintf("Updated organization '%s' (%d)", org.Slug, org.ID),
+		mdata,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return org, nil
+}
+
 // AddLink is the resolver for the addLink field.
 func (r *mutationResolver) AddLink(ctx context.Context, input *model.LinkInput) (*models.OrgLink, error) {
 	tokenUser := oauth2.ForContext(ctx)
@@ -1660,257 +1860,63 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input *model.Profi
 	user.Settings.Account.Timezone = input.Timezone
 
 	err := user.Store(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	opts := &database.FilterOptions{
-		Filter: sq.And{
-			sq.Eq{"o.owner_id": user.ID},
-			sq.Eq{"o.org_type": models.OrgTypeUser},
-		},
-		Limit: 1,
-	}
-
-	personalOrgs, err := models.GetOrganizations(ctx, opts)
-	if err != nil {
-		return nil, err
-	}
-
-	if len(personalOrgs) == 0 {
-		return nil, fmt.Errorf("%s", lt.Translate("No personal organization found for this user"))
-	}
-	personalOrg := personalOrgs[0]
-	var updateOrg bool
-	if input.DeleteImg != nil && *input.DeleteImg {
-		personalOrg.Image = ""
-		updateOrg = true
-	} else if input.Image != nil {
-		path, err := links.StoreImage(ctx, input.Image)
-		if err != nil {
-			return nil, err
-		}
-		personalOrg.Image = path
-		updateOrg = true
-	}
-
-	if updateOrg {
-		err = personalOrg.Store(ctx)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	c := server.EchoForContext(ctx)
-	mdata := make(map[string]any)
-	mdata["org_id"] = personalOrg.ID
-	err = models.RecordAuditLog(
-		ctx,
-		int(user.ID),
-		c.RealIP(),
-		models.LOG_PROFILE_UPDATED,
-		fmt.Sprintf("Updated profile"),
-		mdata,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	return user, nil
-}
-
-// UpdateOrganization is the resolver for the updateOrganization field.
-func (r *mutationResolver) UpdateOrganization(ctx context.Context, input *model.UpdateOrganizationInput) (*models.Organization, error) {
-	tokenUser := oauth2.ForContext(ctx)
-	if tokenUser == nil {
-		return nil, valid.ErrAuthorization
-	}
-	user := tokenUser.User.(*models.User)
-	lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
-	lt := localizer.GetLocalizer(lang)
-
-	validator := valid.New(ctx)
-	validator.Expect(input.CurrentSlug != "", "%s", lt.Translate("CurrentSlug is required")).
-		WithField("currentSlug").
-		WithCode(valid.ErrValidationCode)
-	validator.Expect(input.Name != "", "%s", lt.Translate("Name is required")).
-		WithField("name").
-		WithCode(valid.ErrValidationCode)
-	validator.Expect(len(input.Name) < 150, "%s", lt.Translate("Name may not exceed 150 characters")).
-		WithField("name").
-		WithCode(valid.ErrValidationCode)
-	validator.Expect(input.Slug != "", "%s", lt.Translate("Slug is required")).
-		WithField("slug").
-		WithCode(valid.ErrValidationCode)
-	validator.Expect(len(input.Slug) < 150, "%s", lt.Translate("Slug may not exceed 150 characters")).
-		WithField("slug").
-		WithCode(valid.ErrValidationCode)
-
-	if !validator.Ok() {
-		return nil, nil
-	}
-
-	org, err := user.GetOrgsSlug(ctx, models.OrgUserPermissionAdminWrite, input.CurrentSlug)
-	if err != nil {
-		return nil, err
-	}
-	if org == nil {
-		validator.Error(
-			"%s", lt.Translate("Organization Not Found")).
-			WithField("name").
-			WithCode(valid.ErrNotFoundCode)
-		return nil, nil
-	}
-
-	// If the org name changed, validate it
-	var (
-		opts *database.FilterOptions
-		orgs []*models.Organization
-	)
-	if input.Name != org.Name {
-		opts = &database.FilterOptions{
-			Filter: sq.And{
-				sq.Eq{"o.name": input.Name},
-				sq.Eq{"o.owner_id": user.ID},
-			},
-			Limit: 1,
-		}
-		orgs, err = models.GetOrganizations(ctx, opts)
-		if err != nil {
-			return nil, err
-		}
-
-		if len(orgs) > 0 {
-			validator.Error(
-				"%s", lt.Translate("This organization name is already registered")).
-				WithField("name").
-				WithCode(valid.ErrValidationCode)
-			return nil, nil
-		}
-
-	}
-
-	// If the org slug changed, validate it
-	slug := links.Slugify(input.Slug)
-	if slug != org.Slug {
-		b := &gaccounts.BlacklistValidator{}
-		if !b.UsernameSafePlus(links.InvalidSlugs, slug) {
-			validator.Error("%s", lt.Translate("This slug can not be used. Please chose another one")).
-				WithField("slug").
-				WithCode(valid.ErrValidationCode)
-			return nil, nil
-		}
-
-		opts = &database.FilterOptions{
-			Filter: sq.Eq{"o.slug": slug},
-			Limit:  1,
-		}
-		orgs, err = models.GetOrganizations(ctx, opts)
-		if err != nil {
-			return nil, err
-		}
+	if err != nil {
+		return nil, err
+	}
 
-		if len(orgs) > 0 {
-			validator.Error(
-				"%s", lt.Translate("This organization slug is already registered")).
-				WithField("slug").
-				WithCode(valid.ErrValidationCode)
-			return nil, nil
-		}
+	opts := &database.FilterOptions{
+		Filter: sq.And{
+			sq.Eq{"o.owner_id": user.ID},
+			sq.Eq{"o.org_type": models.OrgTypeUser},
+		},
+		Limit: 1,
 	}
 
-	org.Name = input.Name
-	org.Slug = slug
+	personalOrgs, err := models.GetOrganizations(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
 
-	// If the param was sent and if it is true
+	if len(personalOrgs) == 0 {
+		return nil, fmt.Errorf("%s", lt.Translate("No personal organization found for this user"))
+	}
+	personalOrg := personalOrgs[0]
+	var updateOrg bool
 	if input.DeleteImg != nil && *input.DeleteImg {
-		org.Image = ""
+		personalOrg.Image = ""
+		updateOrg = true
 	} else if input.Image != nil {
 		path, err := links.StoreImage(ctx, input.Image)
 		if err != nil {
 			return nil, err
 		}
-		org.Image = path
-	}
-
-	// The org is going to be disabled or enabled
-	if input.IsActive != nil {
-		if org.OrgType == models.OrgTypeUser {
-			validator.Error("%s", lt.Translate("You are not allowed to enable/disabled user type organization")).
-				WithField("isActive").
-				WithCode(valid.ErrValidationCode)
-			return nil, nil
-		}
-
-		if *input.IsActive {
-			// If we want re-enable it, we have to check the limit of free orgs
-			opts := &database.FilterOptions{
-				Filter: sq.And{
-					sq.NotEq{"o.id": org.ID},
-					sq.Eq{"o.owner_id": user.ID},
-					sq.Eq{"o.is_active": true},
-					sq.Eq{"(o.settings->'billing'->>'status')": models.BillingStatusFree},
-				},
-			}
-
-			freeOrgs, err := models.GetOrganizations(ctx, opts)
-			if err != nil {
-				return nil, err
-			}
-			if len(freeOrgs) > 1 {
-				validator.Error("%s", lt.Translate("You are not allowed to have more than two free organizations. Please upgrade")).
-					WithCode(valid.ErrValidationGlobalCode)
-				return nil, nil
-			}
-		} else if org.Settings.Billing.Status == models.BillingStatusPersonal ||
-			org.Settings.Billing.Status == models.BillingStatusBusiness {
-			validator.Error("%s", lt.Translate("This organization has an active subscription. Please cancel it first")).
-				WithCode(valid.ErrValidationGlobalCode)
-			return nil, nil
-		}
-		org.IsActive = *input.IsActive
+		personalOrg.Image = path
+		updateOrg = true
 	}
 
-	if input.DefaultPerm != nil {
-		if org.Settings.Billing.Status == models.BillingStatusFree &&
-			string(*input.DefaultPerm) == models.OrgLinkVisibilityPrivate {
-			validator.Error("%s", lt.Translate(
-				"Free organizations can not use Private permission. Please upgrade to use "+
-					"Private permission")).
-				WithField("defaultPerm").
-				WithCode(valid.ErrValidationCode)
-			return nil, nil
-		}
-		if !models.ValidateLinkVisibility(string(*input.DefaultPerm)) {
-			validator.Error("%s", lt.Translate("Invalid default permission value")).
-				WithField("defaultPerm").
-				WithCode(valid.ErrValidationCode)
-			return nil, nil
+	if updateOrg {
+		err = personalOrg.Store(ctx)
+		if err != nil {
+			return nil, err
 		}
-		org.Settings.DefaultPerm = string(*input.DefaultPerm)
-	}
-
-	err = org.Store(ctx)
-	if err != nil {
-		return nil, err
 	}
 
 	c := server.EchoForContext(ctx)
 	mdata := make(map[string]any)
-	mdata["org_id"] = org.ID
+	mdata["org_id"] = personalOrg.ID
 	err = models.RecordAuditLog(
 		ctx,
 		int(user.ID),
 		c.RealIP(),
-		models.LOG_ORG_UPDATED,
-		fmt.Sprintf("Updated organization '%s' (%d)", org.Slug, org.ID),
+		models.LOG_PROFILE_UPDATED,
+		fmt.Sprintf("Updated profile"),
 		mdata,
 	)
 	if err != nil {
 		return nil, err
 	}
 
-	return org, nil
+	return user, nil
 }
 
 // AddDomain is the resolver for the addDomain field.
@@ -4740,8 +4746,8 @@ func (r *qRCodeResolver) CodeType(ctx context.Context, obj *models.QRCode) (mode
 func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {
 	return &model.Version{
 		Major:           0,
-		Minor:           1,
-		Patch:           2,
+		Minor:           2,
+		Patch:           0,
 		DeprecationDate: nil,
 	}, nil
 }
@@ -6170,7 +6176,7 @@ func (r *queryResolver) Analytics(ctx context.Context, input model.AnalyticsInpu
 
 	if !org.CanRead(ctx, user) {
 		validator.Error("%s", lt.Translate("Access Restricted")).
-			WithCode(valid.ErrRestrictedCode)
+			WithCode(valid.ErrValidationCode)
 		return nil, nil
 	}
 
@@ -6629,6 +6635,223 @@ func (r *queryResolver) GetFeedFollowing(ctx context.Context) ([]*models.Organiz
 	return orgs, nil
 }
 
+// GetAuditLog is the resolver for the getAuditLog field.
+func (r *queryResolver) GetAuditLogs(ctx context.Context, input *model.AuditLogInput) (*model.AuditLogCursor, error) {
+	tokenUser := oauth2.ForContext(ctx)
+	if tokenUser == nil {
+		return nil, valid.ErrAuthorization
+	}
+	user := tokenUser.User.(*models.User)
+	lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
+	lt := localizer.GetLocalizer(lang)
+
+	validator := valid.New(ctx)
+	if input.After != nil && input.Before != nil {
+		validator.Error("%s", lt.Translate("You can not send both after and before cursors")).
+			WithCode(valid.ErrValidationGlobalCode)
+		return nil, nil
+	}
+
+	var org *models.Organization
+	var err error
+	opts := &database.FilterOptions{
+		Filter: sq.And{},
+	}
+
+	if input.OrgSlug != nil && *input.OrgSlug != "" {
+		if !user.IsSuperUser() {
+			org, err = user.GetOrgsSlug(ctx, models.OrgUserPermissionAdminWrite, *input.OrgSlug)
+			if err != nil {
+				return nil, err
+			}
+			if org == nil {
+				validator.Error("%s", lt.Translate("Access denied.")).
+					WithCode(valid.ErrValidationCode)
+				return nil, nil
+			}
+		} else {
+			orgOpts := &database.FilterOptions{
+				Filter: sq.Eq{"o.slug": strings.ToLower(*input.OrgSlug)},
+			}
+			orgs, err := models.GetOrganizations(ctx, orgOpts)
+			if err != nil {
+				return nil, err
+			}
+			if len(orgs) == 0 {
+				validator.Error("%s", lt.Translate("Organization not found.")).
+					WithField("orgSlug").
+					WithCode(valid.ErrNotFoundCode)
+				return nil, nil
+			}
+			org = orgs[0]
+		}
+		opts.Filter = sq.And{
+			opts.Filter,
+			sq.Eq{"(al.metadata->>'org_id')": org.ID},
+		}
+	}
+
+	// Verify listing permisison
+	if input.ListingID != nil && *input.ListingID > 0 {
+		var listing *models.Listing
+		lopts := &database.FilterOptions{
+			Filter: sq.Eq{"l.id": *input.ListingID},
+		}
+		listings, err := models.GetListings(ctx, lopts)
+		if err != nil {
+			return nil, err
+		}
+
+		if len(listings) == 0 {
+			validator.Error("%s", lt.Translate("Invalid listing ID provided")).
+				WithField("listingId").
+				WithCode(valid.ErrNotFoundCode)
+			return nil, nil
+		}
+		listing = listings[0]
+
+		if org != nil {
+			// Verify listing belongs specified org
+			if listing.OrgID != org.ID {
+				validator.Error("%s", lt.Translate("Invalid listing for specified organization")).
+					WithField("listingId").
+					WithCode(valid.ErrValidationCode)
+				return nil, nil
+			}
+		} else {
+			// No org speciifed. Let's fetch associated org to verify user permissions
+			org, err = user.GetOrgsID(ctx, models.OrgUserPermissionAdminWrite, listing.OrgID)
+			if err != nil {
+				return nil, err
+			}
+			if org == nil {
+				validator.Error("%s", lt.Translate("Access denied.")).
+					WithField("listingId").
+					WithCode(valid.ErrValidationCode)
+				return nil, nil
+			}
+			opts.Filter = sq.And{
+				opts.Filter,
+				sq.Eq{"(al.metadata->>'org_id')": org.ID},
+			}
+		}
+		opts.Filter = sq.And{
+			opts.Filter,
+			sq.Eq{"(al.metadata->>'list_id')": listing.ID},
+		}
+	}
+
+	if !validator.Ok() {
+		return nil, nil
+	}
+
+	var userId int
+	if org == nil && !user.IsSuperUser() {
+		userId = int(user.ID)
+	}
+
+	if input.UserID != nil && *input.UserID > 0 {
+		if user.IsSuperUser() {
+			userId = *input.UserID
+		} else if *input.UserID != int(user.ID) {
+			if org != nil {
+				// If the given user was never part of the organization, no records would be returned.
+				// However if a user is removed from an organization we still want to be able to
+				// view their audit logs
+				userId = *input.UserID
+			}
+		}
+	}
+
+	if userId > 0 {
+		opts.Filter = sq.And{
+			opts.Filter,
+			sq.Eq{"al.user_id": userId},
+		}
+	}
+
+	numElements := model.PaginationDefault
+	var hasPrevPage, hasNextPage bool
+	if input.After != nil {
+		opts.Filter = sq.And{
+			opts.Filter,
+			sq.Expr("al.id <= ?", input.After.After),
+		}
+		numElements = input.After.Limit
+	} else if input.Before != nil {
+		opts.Filter = sq.And{
+			opts.Filter,
+			sq.Expr("ol.id >= ?", input.Before.Before),
+		}
+		numElements = input.Before.Limit
+	}
+
+	// If limit specifically set, it overrides any cursor limit
+	if input.Limit != nil && *input.Limit > 0 {
+		numElements = *input.Limit
+	}
+	if numElements > model.PaginationMax {
+		numElements = model.PaginationMax
+	}
+	opts.Limit = numElements + 2
+
+	alogs, err := auditlog.GetAuditLogs(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	c := model.Cursor{Limit: numElements}
+	count := len(alogs)
+	if count > 0 {
+		// Checking for previous page
+		if input.Before != nil {
+			if alogs[0].ID == input.Before.Before {
+				hasPrevPage = true
+				alogs = alogs[1:]
+				count--
+			}
+		} else if input.After != nil {
+			if alogs[0].ID == input.After.After {
+				hasPrevPage = true
+				alogs = alogs[1:]
+				count--
+			}
+		}
+		if count == opts.Limit {
+			// No previous page
+			alogs = alogs[:count-1]
+			count--
+		}
+
+		if count > numElements {
+			hasNextPage = true
+			alogs = alogs[:count-1]
+			count--
+		}
+		if count > 0 {
+			if input.Before != nil {
+				tmp := hasPrevPage
+				hasPrevPage = hasNextPage
+				hasNextPage = tmp
+				c.After = alogs[0].ID
+				c.Before = alogs[count-1].ID
+			} else {
+				c.After = alogs[count-1].ID
+				c.Before = alogs[0].ID
+			}
+		} else {
+			hasPrevPage = false
+		}
+	}
+
+	pageInfo := &model.PageInfo{
+		Cursor:      c,
+		HasNextPage: hasNextPage,
+		HasPrevPage: hasPrevPage,
+	}
+	return &model.AuditLogCursor{Result: alogs, PageInfo: pageInfo}, nil
+}
+
 // GetUsers is the resolver for the ADMIN AREA getUsers field.
 func (r *queryResolver) GetUsers(ctx context.Context, input *model.GetUserInput) (*model.UserCursor, error) {
 	if input == nil {
@@ -7224,6 +7447,9 @@ func (r *userResolver) ID(ctx context.Context, obj *models.User) (int, error) {
 	return int(obj.ID), nil
 }
 
+// AuditLog returns AuditLogResolver implementation.
+func (r *Resolver) AuditLog() AuditLogResolver { return &auditLogResolver{r} }
+
 // BillingSettings returns BillingSettingsResolver implementation.
 func (r *Resolver) BillingSettings() BillingSettingsResolver { return &billingSettingsResolver{r} }
 
@@ -7253,6 +7479,7 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
 // User returns UserResolver implementation.
 func (r *Resolver) User() UserResolver { return &userResolver{r} }
 
+type auditLogResolver struct{ *Resolver }
 type billingSettingsResolver struct{ *Resolver }
 type domainResolver struct{ *Resolver }
 type mutationResolver struct{ *Resolver }
diff --git a/helpers.go b/helpers.go
index c917207..a5a8d47 100644
--- a/helpers.go
+++ b/helpers.go
@@ -94,6 +94,7 @@ func ParseInputErrors(c echo.Context, graphError *gqlclient.Error, fMap gobwebs.
 	}
 
 	if ext.Code == valid.ErrRestrictedCode {
+		c.Set("pass_msg", graphError.Message)
 		return RenderRestrictedTemplate(c)
 	}
 
@@ -552,6 +553,9 @@ func RenderRestrictedTemplate(c echo.Context) error {
 	gmap := gobwebs.Map{
 		"pd": pd,
 	}
+	if pass_msg, ok := c.Get("pass_msg").(string); ok {
+		gmap["pass_msg"] = pass_msg
+	}
 	curSlug := PullOrgSlug(c)
 	if curSlug != "" {
 		gmap["orgSlug"] = curSlug
diff --git a/templates/restricted.html b/templates/restricted.html
index f27d2c5..02876ef 100644
--- a/templates/restricted.html
+++ b/templates/restricted.html
@@ -1,7 +1,7 @@
 {{template "base" .}}
 {{ define "title" }}{{ .pd.Title }}{{ end }}
 <section class="card shadow-card">
-    <p class="text-center">{{.pd.Data.message}}</p>
+    <p class="text-center">{{ if .pass_msg }}{{ .pass_msg }}: {{ end }}{{.pd.Data.message}}</p>
     {{if .orgSlug}}
     <p class="text-center"><a href="{{reverse "billing:create_subscription" .orgSlug}}">{{.pd.Data.continue}}</a></p>
     {{end}}
-- 
2.47.2