pa

package module
v0.0.0-...-375f364 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 17, 2022 License: Apache-2.0 Imports: 5 Imported by: 0

README

patrickarvatu.com Version Go Backend Test

patrickarvatu.com is my open source personal website!

State

patrickarvatu.com isnt yet in production, however it is under active development.

Things Done:
  • SQL logic implemented.
  • Implement sql code in sql package
  • HTTP exposure to the sql package
  • OAuth github implementation
  • Event Service implemented using asynq
  • CLI start upp
TODO:
  • Dockerize app
  • Node api to interact with frontend fs
  • Finish static pages on frontend (about, index)
  • Profile component
  • Near future: Support more OAuth providers
  • Write tests: sql package
  • Write tests: http package

Backend

Frontend

  • Built with react.js
  • Using next.js on top of react.js
  • Styling done with tailwind css

Config File

The config file uses the toml formant.

Fields:

Field Description Under
client-id Client ID of github oath 2.0 app [github]
client-secret Client Secret of github oauth 2.0 app [github]
admin-user-email the email of the admin, used to recognize admin user [github]
addr the address of the server (specify only port in development) [http]
domain the domain of the server (leave this empty in development) [http]
block-key key used for secure cookie encryption (see more) [http]
hash-key key used for secure cookie encryption (see more) [http]
frontend-url URL to frontend (ex: http://localhost:3000) [http]
sqlite-dsn path to sqlite database [database]
redis-dsn redis data source name (ex: 127.0.0.1:6379) [database]
addr address of the smtp server [smtp]
identity refer: godoc [smtp]
username refer: godoc [smtp]
password refer: godoc [smtp]
host refer: godoc [smtp]
blog-images-dir path to the http served file structure for blogs (used to store images) [file-structure]
project-images-dir path to the http served file structure for projects (used to store images) [file-structure]

Run:

GO 1.16 or higher is required

Currently the app isnt dockerized but you can run the go backend using go command line tool.

go install github.com/Lambels/patrickarvatu.com/cmd

If you have your GOBIN set to your path run the installed binary with the serve sub command and --config flag

bin_name serve --config ./path/to/config/file.toml

After you should have a running server on the address and domain specified in the config file.

Documentation

Index

Constants

View Source
const (
	ECONFLICT       = "conflict"
	EINTERNAL       = "internal"
	EINVALID        = "invalid"
	ENOTFOUND       = "not_found"
	ENOTIMPLEMENTED = "not_implemented"
	EUNAUTHORIZED   = "unauthorized"
)

Error codes which map good to http errors.

View Source
const (
	// Sub blogs are branched under blogs.
	EventTopicNewSubBlog = "blog:sub_blog:new"

	// Comments are branched under sub blogs.
	EventTopicNewComment = "blog:sub_blog:comment:new"
)

Event topics.

View Source
const (
	AuthSourceGitHub = "github"
)

auth sources represent different OAuth providers, the system is currently supporting only github as a provider but its implemented with this issue taken in mind.

View Source
const SessionCookieName = "session"

SessionCookieName represents the name of the session cookie.

Variables

This section is empty.

Functions

func ErrorCode

func ErrorCode(err error) string

ErrorCode is a helper function to retrieve the error code from a pa.Error. returns an empty string if err is nil. returns EINTERNAL if the error isnt a pa.Error.

func ErrorMessage

func ErrorMessage(err error) string

ErrorMessage is a helper function to retrieve the error message from pa.Error. returns an empty string if err is nil. returns "Internal error." if the error isnt a pa.Error.

func IsAdminContext

func IsAdminContext(ctx context.Context) bool

IsAdminContext is a helper function to check if context: ctx is an admin context.

func NewContextWithUser

func NewContextWithUser(ctx context.Context, user *User) context.Context

NewContextWithUser enriches the context ctx with the user: user under the key userContextKey.

func UserIDFromContext

func UserIDFromContext(ctx context.Context) int

UserIDFromContext is a helper function which returns only the id of the user under ctx. To only be used when checking id with id.

Types

type Auth

type Auth struct {
	// the pk of the auth.
	ID int `json:"id"`

	// fields linking the auth object back to the user.
	UserID int   `json:"userID"`
	User   *User `json:"user"`

	// the source from where the OAuth object comes from, ie: "github".
	Source   string `json:"source"`
	SourceID string `json:"sourceID"`

	// OAuth credentials provided by the OAuth source.
	AccessToken  string     `json:"-"`
	RefreshToken string     `json:"-"`
	Expiry       *time.Time `json:"-"`

	// timestamps.
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

Auth represents an OAuth object in the system.

func (*Auth) AvatarURL

func (a *Auth) AvatarURL(size int) string

AvatarURL returns a URL to the avatar image provided by the OAuth source. returns an emtpy string if no source is identified.

func (*Auth) Validate

func (a *Auth) Validate() error

Validate performs basic validation on Auth. returns EINVALID if any error is found.

type AuthFilter

type AuthFilter struct {
	// fields to filter on.
	ID       *int    `json:"id"`
	UserID   *int    `json:"userID"`
	Source   *string `json:"source"`
	SourceID *string `json:"sourceID"`

	// restrictions on the result set, used for pagination and set limits.
	Offset int `json:"offset"`
	Limit  int `json:"limit"`
}

AuthFilter represents a filter used by FindAuths to filter the response.

type AuthService

type AuthService interface {
	// FindAuthByID returns a auth based on the id.
	// returns ENOTFOUND if the auth doesent exist.
	FindAuthByID(ctx context.Context, id int) (*Auth, error)

	// FindAuths returns a range of auths and the length of the range. If filter
	// is specified FindAuths will apply the filter to return set response.
	FindAuths(ctx context.Context, filter AuthFilter) ([]*Auth, int, error)

	// CreateAuth creates a auth. Main entry point when creating a user.
	// The creation will only go through if the linking fields are attached or the object passes the validation.
	// On creation the auth will be linked to a user if found, otherwise the user gets created and the auth gets
	// linked to the user and the user linked to the auth through the linking fields.
	CreateAuth(ctx context.Context, auth *Auth) error

	// DeleteAuth permanently deletes a auth. The linked user wont be deleted bu will appear as
	// not validated.
	DeleteAuth(ctx context.Context, id int) error
}

AuthService represents a service which manages auth in the system.

type Blog

type Blog struct {
	// the pk of the blog.
	ID int `json:"id"`

	// the descriptive fields of the blog.
	Title       string     `json:"title"`
	Description string     `json:"description"`
	SubBlogs    []*SubBlog `json:"subBlogs"` // the list of sub blogs contained by the blog.

	// timestamps.
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

Blog represents an blog object in the system. Blog has no reason to store any user ID as the admin user is the only one who can interact with BlogService.CreateBlog().

func (*Blog) Validate

func (b *Blog) Validate() error

Validate performs basic validation on the blog. returns EINVALID if any error is found.

type BlogFilter

type BlogFilter struct {
	// fields to filter on.
	ID    *int    `json:"id"`
	Title *string `json:"title"`

	// restrictions on the result set, used for pagination and set limits.
	Offset int `json:"offset"`
	Limit  int `json:"limit"`
}

BlogFilter represents a filter used by FindBlogs to filter the response.

type BlogService

type BlogService interface {
	// FindBlogByID returns a blog based on the id.
	// returns ENOTFOUND if the blog doesent exist.
	FindBlogByID(ctx context.Context, id int) (*Blog, error)

	// FindBlogs returns a range of blogs and the length of the range. If filter
	// is specified FindBlogs will apply the filter to return set response.
	FindBlogs(ctx context.Context, filter BlogFilter) ([]*Blog, int, error)

	// CreateBlog creates a blog.
	// returns EUNAUTHORIZED if used by anyone other then the adim user.
	CreateBlog(ctx context.Context, blog *Blog) error

	// UpdateBlog updates a blog based on the update field.
	// returns ENOTFOUND if blog doesent exist.
	// returns EUNAUTHORIZED if used by anyone other then the adim user.
	UpdateBlog(ctx context.Context, id int, update BlogUpdate) (*Blog, error)

	// DeleteBlog permanently deletes a blog.
	// returns ENOTFOUND if blog doesent exist.
	// returns EUNAUTHORIZED if used by anyone other then the adim user.
	DeleteBlog(ctx context.Context, id int) error
}

BlogService represents a service which manages blogs in the system.

type BlogUpdate

type BlogUpdate struct {
	// fields which can be updated.
	Title       *string `json:"title"`
	Description *string `json:"description"`
}

BlogUpdate represents an update used by UpdateBlog to update a blog.

type Comment

type Comment struct {
	// the pk of the comment.
	ID int `json:"id"`

	// linking fields of the comment.
	SubBlogID int   `json:"subBlogID"`
	UserID    int   `json:"userID"`
	User      *User `json:"user"`

	// content of the comment.
	Content string `json:"content"`

	// timestamp.
	CreatedAt time.Time `json:"createdAt"`
}

Comment represents a comment in the system.

func (*Comment) Validate

func (c *Comment) Validate() error

Validate performs basic validation on the comment. returns EINVALID if any error is found.

type CommentFilter

type CommentFilter struct {
	// fields to filter on.
	ID        *int `json:"id"`
	SubBlogID *int `json:"SubBlogID"`
	UserID    *int `json:"userID"`

	// restrictions on the result set, used for pagination and set limits.
	Offset int `json:"offset"`
	Limit  int `json:"limit"`
}

CommentFilter represents a filter used by FindComments to filter the response.

type CommentPayload

type CommentPayload struct {
	Comment   *Comment `json:"comment"`
	SubBlogID int      `json:"subBlogID"`
	SubBlog   *SubBlog `json:"subBlog"`
}

CommentPayload represents the payload carried by a EventTopicNewComment -> ./event.go.

type CommentService

type CommentService interface {
	// FindCommentByID returns a comment based on the id.
	// returns ENOTFOUND if the comment doesent exist.
	FindCommentByID(ctx context.Context, id int) (*Comment, error)

	// FindComments returns a range of comments and the length of the range. If filter
	// is specified FindComments will apply the filter to return set response.
	FindComments(ctx context.Context, filter CommentFilter) ([]*Comment, int, error)

	// CreateComment creates a comment.
	CreateComment(ctx context.Context, comment *Comment) error

	// UpdateComment updates a comment based on the update field.
	// returns ENOTFOUND if the comment doesent exist.
	// returns EUNAUTHORIZED if used by anyone other then the adim user.
	UpdateComment(ctx context.Context, id int, update CommentUpdate) (*Comment, error)

	// DeleteComment permanently deletes a comment.
	// returns ENOTFOUND if comment doesent exist.
	// returns EUNAUTHORIZED if used by anyone other then the user owning the comment.
	DeleteComment(ctx context.Context, id int) error
}

CommentService represents a service which manages comments in the system.

type CommentUpdate

type CommentUpdate struct {
	// fields which can be updated.
	Content *string `json:"content"`
}

CommentUpdate represents an update used by UpdateComment to update a comment.

type Config

type Config struct {
	Github struct {
		ClientID       string `mapstructure:"client-id"`
		ClientSecret   string `mapstructure:"client-secret"`
		AdminUserEmail string `mapstructure:"admin-user-email"`
	} `mapstructure:"github"`

	HTTP struct {
		Addr        string `mapstructure:"addr"`
		Domain      string `mapstructure:"domain"`
		BlockKey    string `mapstructure:"block-key"`
		HashKey     string `mapstructure:"hash-key"`
		FrontendURL string `mapstructure:"frontend-url"`
	} `mapstructure:"http"`

	Database struct {
		SqliteDSN string `mapstructure:"sqlite-dsn"`
		RedisDSN  string `mapstructure:"redis-dsn"`
	} `mapstructure:"database"`

	Smtp struct {
		Addr     string `mapstructure:"addr"`
		Identity string `mapstructure:"identity"`
		Username string `mapstructure:"username"`
		Password string `mapstructure:"password"`
		Host     string `mapstructure:"host"`
	} `mapstructure:"smtp"`

	FileStructure struct {
		ProjectImagesDir string `mapstructure:"project-images-dir"`
		BlogImagesDir    string `mapstructure:"blog-images-dir"`
	} `mapstructure:"file-structure"`
}

Config layouts the .toml config file structure its expecting. uses mapstructure tags which is used by viper.

type EmailService

type EmailService interface {
	// SendEmail will send a emails to the adresses provided in to.
	SendEmail(to []string, body, subject string) error
}

EmailService represents a service which manages emails in the system.

type Error

type Error struct {
	// Code to check the type of the error.
	Code string

	// Human readeable message.
	Message string
}

Error is a struct containing full details about the error.

func Errorf

func Errorf(code string, format string, args ...interface{}) *Error

Errorf is a helper function to quickly init an error with code and format: message.

func (*Error) Error

func (e *Error) Error() string

Error is used to implement the error interface.

type Event

type Event struct {
	// The topic of the event, ie: EventTopicNewSubBlog -> ./event.go.
	Topic string

	// The payload of the event, ie: BlogPayload -> ./event.go.
	Payload Payload
}

Event is passed to EventHandler.

type EventHandler

type EventHandler func(ctx context.Context, handler SubscriptionService, event Event) error

EventHandler represents a fucntion which is called on each event.

type EventService

type EventService interface {
	// Push pushes event in the event queue.
	Push(ctx context.Context, event Event) error

	// RegisterHandler registers handler as the handler for topic.
	RegisterHandler(topic string, handler EventHandler)

	// RegisterSubscriptionsHandler registers the subscriptions manager.
	RegisterSubscriptionsHandler(hand SubscriptionService)
}

EventService represents a service which manages events in the system.

func NewNOPEventService

func NewNOPEventService() EventService

type FileService

type FileService interface {
	// CreateFile creates a new file.
	CreateFile(ctx context.Context, path string, content io.Reader) error

	// DeletePath deletes path.
	DeleteFile(ctx context.Context, path string) error
}

FileService represents a service which manages files in the system. Should be used to create / delete files in a served fs.

type NOPEventService

type NOPEventService struct{}

NOPEventService is EventService which does nothing. Should only be used in tests.

func (*NOPEventService) Push

func (n *NOPEventService) Push(ctx context.Context, event Event) error

func (*NOPEventService) RegisterHandler

func (n *NOPEventService) RegisterHandler(topic string, handler EventHandler)

func (*NOPEventService) RegisterSubscriptionsHandler

func (n *NOPEventService) RegisterSubscriptionsHandler(hand SubscriptionService)

type Payload

type Payload interface{}

Payload is an iterface to pe used when accepting event payloads, ie: BlogPayload -> ./event.go.

type Project

type Project struct {
	// pk in our system.
	ID int `json:"id"`

	// github api fields.
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Topics      []string `json:"topics"`
	HtmlURL     string   `json:"html_url"`
}

Project represents a github api repo response simplified. this is consumed by our frontend to avoid getting github rate-limited.

func (*Project) Validate

func (p *Project) Validate() error

Validate performs basic validation on the project. returns EINVALID if any error is found.

type ProjectFilter

type ProjectFilter struct {
	// fields to filter on.
	ID   *int    `json:"id"`
	Name *string `json:"name"`

	// restrictions on the result set, used for pagination and set limits.
	Offset int `json:"offset"`
	Limit  int `json:"limit"`
}

ProjectFilter represents a filter used by FindProjects to filter the response.

type ProjectService

type ProjectService interface {
	// FindProjectByID returns a project based on the id.
	// returns ENOTFOUND if the project doesent exist.
	FindProjectByID(ctx context.Context, id int) (*Project, error)

	// FindProjectByName returns a project based on the name.
	// returns ENOTFOUND if the project doesent exist.
	// (helper function to also return ENOTFOUND when filtering on name)
	FindProjectByName(ctx context.Context, name string) (*Project, error)

	// FindProjects returns a range of preojects and the length of the range. If filter
	// is specified FindProjects will apply the filter to return set response.
	FindProjects(ctx context.Context, filter ProjectFilter) ([]*Project, int, error)

	// CreateOrUpdateProject checks for existing id field on project or any duplicate name, if any
	// found the project field will be used to update the pointed to project.
	// returns EUNAUTHORIZED if not used by admin user or internally.
	CreateOrUpdateProject(ctx context.Context, project *Project) error

	// DeleteProject permanently deletes a project based on name.
	// returns ENOTFOUND if project doesent exist.
	// returns EUNAUTHORIZED if not used by admin user or internally.
	DeleteProject(ctx context.Context, name string) error
}

ProjectService represents a service which manages projects in the system.

type Session

type Session struct {
	UserID  int  `json:"userID"`
	IsAdmin bool `json:"isAdmin"`
	// Mainly used for auth 2.0 protocol dialogue to prevent CSRF attacks.
	// can also be used to store redirect urls and any other state type variables.
	State string `json:"state"`
}

Session represents data stored per session under a secure cookie.

type SubBlog

type SubBlog struct {
	// the pk of the sub blog.
	ID int `json:"id"`

	// the id of the blog under which the sub blog is.
	BlogID int `json:"blogID"`

	// the descriptive fields of the sub blog.
	Title    string     `json:"title"`
	Content  string     `json:"body"`
	Comments []*Comment `json:"comments"`

	// timestamps.
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

SubBlog represents an sub blog object in the system. SubBlog has no reason to store any user ID as the admin user is the only one who can interact with SubBlogService.CreateSubBlog().

func (*SubBlog) Validate

func (s *SubBlog) Validate() error

Validate performs basic validation on the sub blog. returns EINVALID if any error is found.

type SubBlogFilter

type SubBlogFilter struct {
	// fields to filter on.
	ID     *int    `json:"id"`
	Title  *string `json:"title"`
	BlogID *int    `json:"blogID"`

	// restrictions on the result set, used for pagination and set limits.
	Offset int `json:"offset"`
	Limit  int `json:"limit"`
}

SubBlogFilter represents a filter used by FindSubBlogs to filter the response.

type SubBlogPayload

type SubBlogPayload struct {
	SubBlog *SubBlog `json:"subBlog"`
	BlogID  int      `json:"blogID"`
	Blog    *Blog    `json:"blog"`
}

SubBlogPayload represents the payload carried by a EventTopicNewSubBlog -> ./event.go.

type SubBlogService

type SubBlogService interface {
	// FindSubBlogByID returns a sub blog based on the id.
	// returns ENOTFOUND if the sub blog doesent exist.
	FindSubBlogByID(ctx context.Context, id int) (*SubBlog, error)

	// FindSubBlogs returns a range of sub blogs and the length of the range. If filter
	// is specified FindSubBlogs will apply the filter to return set response.
	FindSubBlogs(ctx context.Context, filter SubBlogFilter) ([]*SubBlog, int, error)

	// CreateSubBlog creates a sub blog.
	// returns EUNAUTHORIZED if used by anyone other then the adim user.
	CreateSubBlog(ctx context.Context, subBlog *SubBlog) error

	// UpdateSubBlog updates a sub blog based on the update field.
	// returns ENOTFOUND if sub blog doesent exist.
	// returns EUNAUTHORIZED if used by anyone other then the adim user.
	UpdateSubBlog(ctx context.Context, id int, update SubBlogUpdate) (*SubBlog, error)

	// DeleteSubBlog permanently deletes a sub blog.
	// returns ENOTFOUND if sub blog doesent exist.
	// returns EUNAUTHORIZED if used by anyone other then the adim user.
	DeleteSubBlog(ctx context.Context, id int) error
}

SubBlogService represents a service which manages sub-blogs in the system.

type SubBlogUpdate

type SubBlogUpdate struct {
	// fields which can be updated.
	Title   *string `json:"title"`
	Content *string `json:"content"`
}

SubBlogUpdate represents an update used by UpdateSubBlog to update a sub blog.

type Subscription

type Subscription struct {
	// the pk of the subscription.
	ID int

	// the subscribed user.
	UserID int

	// topic to which the user is subscribed, ie: EventTopicNewBlog -> ./event.go.
	// each topic will map to a table in the database, logic handeled in service.
	Topic string

	// Payload is used to provide unique information about each Topic. The type of payload can be identified
	// by the topic, for example a Topic of type EventTopicNewBlog will come with a payload of type
	// BlogPayload -> ./event.go
	Payload Payload
}

Subscription represents a subscription handeled by the event handler on an event / topic.

type SubscriptionFilter

type SubscriptionFilter struct {
	// fields to filter on.
	ID      *int    `json:"id"`
	UserID  *int    `json:"userID"`
	Topic   *string `json:"topic"`
	Payload Payload `json:"payload"`

	// restrictions on the result set, used for pagination and set limits.
	Offset int `json:"offset"`
	Limit  int `json:"limit"`
}

SubscriptionFilter represents a filter used by FindSubscriptions to filter the response.

type SubscriptionService

type SubscriptionService interface {
	// FindSubscriptionByID returns a subscription based on the id and topic.
	// returns ENOTFOUND if the subscription doesent exist.
	FindSubscriptionByID(ctx context.Context, id int, topic string) (*Subscription, error)

	// FindSubscriptions returns a range of subscriptions and the length of the range. If filter
	// is specified FindSubscriptions will apply the filter to return set response.
	FindSubscriptions(ctx context.Context, filter SubscriptionFilter) ([]*Subscription, int, error)

	// CreateSubscription creates a subscription on topic. User is passed through context.
	CreateSubscription(ctx context.Context, subscription *Subscription) error

	// DeleteSubscription permanently deletes a subscription. Returns EUNAUTHORIZED if the user owning the subscription
	// isnt the one calling and ENOTFOUND id the subscription doesent exist. User is passed through context.
	DeleteSubscription(ctx context.Context, id int, topic string) error
}

SubscriptionService represents a service which manages subscriptions in the system.

type Topic

type Topic struct {
	ID      int
	Content string
}

Topic represents a topic from github.

type TopicLink struct {
	ProjectID int
	TopicID   int
}

TopicLink represents a link between a topic and a project.

type User

type User struct {
	// the pk of the user.
	ID int `json:"id"`

	// name / email
	Name  string `json:"name"`
	Email string `json:"email"`

	// apikey for the user to communicate to the api on behalf of the user.
	APIKey string `json:"-"`

	// timestamps.
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`

	// assosciated auths.
	Auths []*Auth `json:"auths"`

	// field to identify the user as an admin.
	IsAdmin bool `json:"isAdmin"`
}

User represents an user in the system.

func UserFromContext

func UserFromContext(ctx context.Context) *User

UserFromContext pulls the user from context ctx.

func (*User) AvatarURL

func (u *User) AvatarURL(size int) string

AvatarURL checks the first auth in .Auths and returns a URL to the users pfp on set auth source. returns an empty string if no avatar URL is found.

func (*User) Validate

func (u *User) Validate() error

Vlidate performs basic validation on User. returns EINVALID if any error is found.

type UserFilter

type UserFilter struct {
	// fields to filter on.
	ID     *int    `json:"id"`
	Email  *string `json:"email"`
	APIKey *string `json:"apiKey"`

	// restrictions on the result set, used for pagination and set limits.
	Offset int `json:"offset"`
	Limit  int `json:"limit"`
}

UserFilter represents a filter used by FindUsers to filter the response.

type UserService

type UserService interface {
	// FindUserByID returns a user based on id.
	// returns ENOTFOUND if the user doesent exist.
	FindUserByID(ctx context.Context, id int) (*User, error)

	// FindUsers returns a range of users and the length of the range. If filter
	// is specified FindUsers will apply the filter to return set response.
	FindUsers(ctx context.Context, filter UserFilter) ([]*User, int, error)

	// CreateUser creates an user. To only be used in testing, the main pipeline when creating an user
	// starts over at CreateAuth -> ./auth.go
	CreateUser(ctx context.Context, user *User) error

	// UpdateUser updates a user based on the update field.
	// returns ENOTFOUND if the user doesent exist.
	// returns EUHATHORIZED if the caller isnt trying to update himself.
	UpdateUser(ctx context.Context, id int, update UserUpdate) (*User, error)

	// DeleteUser permanently deletes a user. This also permanently deletes all of the users assosiacions
	// such as: auths, comments.
	// returns ENOTFOUND if user doesent exist.
	// returns EUHATHORIZED if the caller isnt trying to delete himself.
	DeleteUser(ctx context.Context, id int) error
}

UserService represents a service which manages users in the system.

type UserUpdate

type UserUpdate struct {
	// fields which can be updated.
	Name   *string `json:"name"`
	Email  *string `json:"email"`
	ApiKey *string `json:"apiKey"` // TODO: test api key update.
}

UserUpdate represents an update used by UpdateUser to update a user.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL