feature: add web & migrate commands

This commit is contained in:
Jordan Knott
2020-07-15 18:20:08 -05:00
parent 1e9813601e
commit 90515f6aa4
31 changed files with 1300 additions and 640 deletions

View File

@ -0,0 +1,40 @@
package commands
import (
"fmt"
"github.com/spf13/cobra"
)
const CitadelConfDirEnvName = "CITADEL_CONFIG_DIR"
const CitadelAppConf = "citadel"
const mainDescription = `citadel is an open soure project management
system written in Golang & React.`
var (
version = "dev"
commit = "none"
date = "unknown"
)
var versionTemplate = fmt.Sprintf(`Version: %s
Commit: %s
Built: %s`, version, commit, date+"\n")
var commandError error
var configDir string
var verbose bool
var noColor bool
var rootCmd = &cobra.Command{
Use: "citadel",
Long: mainDescription,
Version: version,
}
func Execute() {
rootCmd.SetVersionTemplate(versionTemplate)
rootCmd.AddCommand(newWebCmd(), newMigrateCmd())
rootCmd.Execute()
}

View File

@ -0,0 +1,68 @@
package commands
import (
"fmt"
"github.com/spf13/cobra"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/jmoiron/sqlx"
"github.com/jordanknott/project-citadel/api/internal/config"
log "github.com/sirupsen/logrus"
)
type MigrateLog struct {
verbose bool
}
func (l *MigrateLog) Printf(format string, v ...interface{}) {
log.Printf("%s", v)
}
// Verbose shows if verbose print enabled
func (l *MigrateLog) Verbose() bool {
return l.verbose
}
func newMigrateCmd() *cobra.Command {
return &cobra.Command{
Use: "migrate",
Short: "Run the database schema migrations",
Long: "Run the database schema migrations",
RunE: func(cmd *cobra.Command, args []string) error {
appConfig, err := config.LoadConfig("conf/app.toml")
if err != nil {
return err
}
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
appConfig.Database.User,
appConfig.Database.Password,
appConfig.Database.Host,
appConfig.Database.Name,
)
db, err := sqlx.Connect("postgres", connection)
if err != nil {
return err
}
defer db.Close()
driver, err := postgres.WithInstance(db.DB, &postgres.Config{})
if err != nil {
return err
}
m, err := migrate.NewWithDatabaseInstance(
"file://migrations",
"postgres", driver)
if err != nil {
return err
}
logger := &MigrateLog{}
m.Log = logger
err = m.Up()
if err != nil {
return err
}
return nil
},
}
}

52
internal/commands/web.go Normal file
View File

@ -0,0 +1,52 @@
package commands
import (
"fmt"
"github.com/spf13/cobra"
"net/http"
"time"
"github.com/jmoiron/sqlx"
"github.com/jordanknott/project-citadel/api/internal/config"
"github.com/jordanknott/project-citadel/api/internal/route"
log "github.com/sirupsen/logrus"
)
func newWebCmd() *cobra.Command {
return &cobra.Command{
Use: "web",
Short: "Run the web server",
Long: "Run the web & api server",
Run: func(cmd *cobra.Command, args []string) {
appConfig, err := config.LoadConfig("conf/app.toml")
if err != nil {
log.WithError(err).Error("loading config")
}
Formatter := new(log.TextFormatter)
Formatter.TimestampFormat = "02-01-2006 15:04:05"
Formatter.FullTimestamp = true
log.SetFormatter(Formatter)
log.SetLevel(log.InfoLevel)
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
appConfig.Database.User,
appConfig.Database.Password,
appConfig.Database.Host,
appConfig.Database.Name,
)
db, err := sqlx.Connect("postgres", connection)
if err != nil {
log.Panic(err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
defer db.Close()
log.WithFields(log.Fields{"url": appConfig.General.Host}).Info("starting server")
r, _ := route.NewRouter(appConfig, db)
http.ListenAndServe(appConfig.General.Host, r)
},
}
}

58
internal/config/config.go Normal file
View File

@ -0,0 +1,58 @@
package config
import (
"github.com/BurntSushi/toml"
"io/ioutil"
)
type Database struct {
Host string
Name string
User string
Password string
}
type General struct {
Host string
}
type EmailNotifications struct {
Enabled bool
DisplayName string `toml:"display_name"`
FromAddress string `toml:"from_address"`
}
type Storage struct {
StorageSystem string `toml:"local_storage"`
UploadDirPath string `toml:"upload_dir_path"`
}
type Smtp struct {
Username string
Password string
Server string
Port int
ConnectionSecurity string `toml:"connection_security"`
}
type AppConfig struct {
General General
Database Database
EmailNotifications EmailNotifications `toml:"email_notifications"`
Storage Storage
Smtp Smtp
}
func LoadConfig(path string) (AppConfig, error) {
dat, err := ioutil.ReadFile("conf/app.toml")
if err != nil {
return AppConfig{}, err
}
var appConfig AppConfig
_, err = toml.Decode(string(dat), &appConfig)
if err != nil {
return AppConfig{}, err
}
return appConfig, nil
}

View File

@ -83,6 +83,7 @@ type Querier interface {
SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
SetTeamOwner(ctx context.Context, arg SetTeamOwnerParams) (Team, error)
SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error)
UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)

View File

@ -25,3 +25,6 @@ WHERE user_id = $1;
-- name: UpdateUserRole :one
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING *;
-- name: SetUserPassword :one
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING *;

View File

@ -162,6 +162,33 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
return i, err
}
const setUserPassword = `-- name: SetUserPassword :one
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
`
type SetUserPasswordParams struct {
UserID uuid.UUID `json:"user_id"`
PasswordHash string `json:"password_hash"`
}
func (q *Queries) SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error) {
row := q.db.QueryRowContext(ctx, setUserPassword, arg.UserID, arg.PasswordHash)
var i UserAccount
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
&i.FullName,
&i.Initials,
&i.ProfileAvatarUrl,
&i.RoleCode,
)
return i, err
}
const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code

View File

@ -185,6 +185,7 @@ type ComplexityRoot struct {
UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int
UpdateTaskName func(childComplexity int, input UpdateTaskName) int
UpdateTeamMemberRole func(childComplexity int, input UpdateTeamMemberRole) int
UpdateUserPassword func(childComplexity int, input UpdateUserPassword) int
UpdateUserRole func(childComplexity int, input UpdateUserRole) int
}
@ -347,6 +348,11 @@ type ComplexityRoot struct {
Ok func(childComplexity int) int
}
UpdateUserPasswordPayload struct {
Ok func(childComplexity int) int
User func(childComplexity int) int
}
UpdateUserRolePayload struct {
User func(childComplexity int) int
}
@ -415,6 +421,7 @@ type MutationResolver interface {
DeleteUserAccount(ctx context.Context, input DeleteUserAccount) (*DeleteUserAccountPayload, error)
LogoutUser(ctx context.Context, input LogoutUser) (bool, error)
ClearProfileAvatar(ctx context.Context) (*db.UserAccount, error)
UpdateUserPassword(ctx context.Context, input UpdateUserPassword) (*UpdateUserPasswordPayload, error)
UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error)
}
type OrganizationResolver interface {
@ -1341,6 +1348,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.UpdateTeamMemberRole(childComplexity, args["input"].(UpdateTeamMemberRole)), true
case "Mutation.updateUserPassword":
if e.complexity.Mutation.UpdateUserPassword == nil {
break
}
args, err := ec.field_Mutation_updateUserPassword_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UpdateUserPassword(childComplexity, args["input"].(UpdateUserPassword)), true
case "Mutation.updateUserRole":
if e.complexity.Mutation.UpdateUserRole == nil {
break
@ -2008,6 +2027,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.UpdateTeamMemberRolePayload.Ok(childComplexity), true
case "UpdateUserPasswordPayload.ok":
if e.complexity.UpdateUserPasswordPayload.Ok == nil {
break
}
return e.complexity.UpdateUserPasswordPayload.Ok(childComplexity), true
case "UpdateUserPasswordPayload.user":
if e.complexity.UpdateUserPasswordPayload.User == nil {
break
}
return e.complexity.UpdateUserPasswordPayload.User(childComplexity), true
case "UpdateUserRolePayload.user":
if e.complexity.UpdateUserRolePayload.User == nil {
break
@ -2707,9 +2740,20 @@ extend type Mutation {
logoutUser(input: LogoutUser!): Boolean!
clearProfileAvatar: UserAccount!
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
}
input UpdateUserPassword {
userID: UUID!
password: String!
}
type UpdateUserPasswordPayload {
ok: Boolean!
user: UserAccount!
}
input UpdateUserRole {
userID: UUID!
roleCode: RoleCode!
@ -3411,6 +3455,20 @@ func (ec *executionContext) field_Mutation_updateTeamMemberRole_args(ctx context
return args, nil
}
func (ec *executionContext) field_Mutation_updateUserPassword_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 UpdateUserPassword
if tmp, ok := rawArgs["input"]; ok {
arg0, err = ec.unmarshalNUpdateUserPassword2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserPassword(ctx, tmp)
if err != nil {
return nil, err
}
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_updateUserRole_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -6761,6 +6819,47 @@ func (ec *executionContext) _Mutation_clearProfileAvatar(ctx context.Context, fi
return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_updateUserPassword(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_updateUserPassword_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().UpdateUserPassword(rctx, args["input"].(UpdateUserPassword))
})
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.(*UpdateUserPasswordPayload)
fc.Result = res
return ec.marshalNUpdateUserPasswordPayload2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserPasswordPayload(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_updateUserRole(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -9945,6 +10044,74 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload_member(ctx context.Cont
return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐMember(ctx, field.Selections, res)
}
func (ec *executionContext) _UpdateUserPasswordPayload_ok(ctx context.Context, field graphql.CollectedField, obj *UpdateUserPasswordPayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "UpdateUserPasswordPayload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Ok, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _UpdateUserPasswordPayload_user(ctx context.Context, field graphql.CollectedField, obj *UpdateUserPasswordPayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "UpdateUserPasswordPayload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.User, 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.(*db.UserAccount)
fc.Result = res
return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res)
}
func (ec *executionContext) _UpdateUserRolePayload_user(ctx context.Context, field graphql.CollectedField, obj *UpdateUserRolePayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -12554,6 +12721,30 @@ func (ec *executionContext) unmarshalInputUpdateTeamMemberRole(ctx context.Conte
return it, nil
}
func (ec *executionContext) unmarshalInputUpdateUserPassword(ctx context.Context, obj interface{}) (UpdateUserPassword, error) {
var it UpdateUserPassword
var asMap = obj.(map[string]interface{})
for k, v := range asMap {
switch k {
case "userID":
var err error
it.UserID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
if err != nil {
return it, err
}
case "password":
var err error
it.Password, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputUpdateUserRole(ctx context.Context, obj interface{}) (UpdateUserRole, error) {
var it UpdateUserRole
var asMap = obj.(map[string]interface{})
@ -13340,6 +13531,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
case "updateUserPassword":
out.Values[i] = ec._Mutation_updateUserPassword(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "updateUserRole":
out.Values[i] = ec._Mutation_updateUserRole(ctx, field)
if out.Values[i] == graphql.Null {
@ -14668,6 +14864,38 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload(ctx context.Context, se
return out
}
var updateUserPasswordPayloadImplementors = []string{"UpdateUserPasswordPayload"}
func (ec *executionContext) _UpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserPasswordPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, updateUserPasswordPayloadImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("UpdateUserPasswordPayload")
case "ok":
out.Values[i] = ec._UpdateUserPasswordPayload_ok(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "user":
out.Values[i] = ec._UpdateUserPasswordPayload_user(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var updateUserRolePayloadImplementors = []string{"UpdateUserRolePayload"}
func (ec *executionContext) _UpdateUserRolePayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserRolePayload) graphql.Marshaler {
@ -16230,6 +16458,24 @@ func (ec *executionContext) marshalNUpdateTeamMemberRolePayload2ᚖgithubᚗcom
return ec._UpdateTeamMemberRolePayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNUpdateUserPassword2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserPassword(ctx context.Context, v interface{}) (UpdateUserPassword, error) {
return ec.unmarshalInputUpdateUserPassword(ctx, v)
}
func (ec *executionContext) marshalNUpdateUserPasswordPayload2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, v UpdateUserPasswordPayload) graphql.Marshaler {
return ec._UpdateUserPasswordPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNUpdateUserPasswordPayload2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, v *UpdateUserPasswordPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._UpdateUserPasswordPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNUpdateUserRole2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserRole(ctx context.Context, v interface{}) (UpdateUserRole, error) {
return ec.unmarshalInputUpdateUserRole(ctx, v)
}

View File

@ -12,13 +12,15 @@ import (
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/internal/config"
"github.com/jordanknott/project-citadel/api/internal/db"
)
// NewHandler returns a new graphql endpoint handler.
func NewHandler(repo db.Repository) http.Handler {
func NewHandler(config config.AppConfig, repo db.Repository) http.Handler {
srv := handler.New(NewExecutableSchema(Config{
Resolvers: &Resolver{
Config: config,
Repository: repo,
},
}))

View File

@ -401,6 +401,16 @@ type UpdateTeamMemberRolePayload struct {
Member *Member `json:"member"`
}
type UpdateUserPassword struct {
UserID uuid.UUID `json:"userID"`
Password string `json:"password"`
}
type UpdateUserPasswordPayload struct {
Ok bool `json:"ok"`
User *db.UserAccount `json:"user"`
}
type UpdateUserRole struct {
UserID uuid.UUID `json:"userID"`
RoleCode RoleCode `json:"roleCode"`

View File

@ -5,10 +5,12 @@ package graph
import (
"sync"
"github.com/jordanknott/project-citadel/api/internal/config"
"github.com/jordanknott/project-citadel/api/internal/db"
)
type Resolver struct {
Config config.AppConfig
Repository db.Repository
mu sync.Mutex
}

View File

@ -570,9 +570,20 @@ extend type Mutation {
logoutUser(input: LogoutUser!): Boolean!
clearProfileAvatar: UserAccount!
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
}
input UpdateUserPassword {
userID: UUID!
password: String!
}
type UpdateUserPasswordPayload {
ok: Boolean!
user: UserAccount!
}
input UpdateUserRole {
userID: UUID!
roleCode: RoleCode!

View File

@ -783,13 +783,24 @@ func (r *mutationResolver) ClearProfileAvatar(ctx context.Context) (*db.UserAcco
return &user, nil
}
func (r *mutationResolver) UpdateUserPassword(ctx context.Context, input UpdateUserPassword) (*UpdateUserPasswordPayload, error) {
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(input.Password), 14)
if err != nil {
return &UpdateUserPasswordPayload{}, err
}
user, err := r.Repository.SetUserPassword(ctx, db.SetUserPasswordParams{UserID: input.UserID, PasswordHash: string(hashedPwd)})
if err != nil {
return &UpdateUserPasswordPayload{}, err
}
return &UpdateUserPasswordPayload{Ok: true, User: &user}, err
}
func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
user, err := r.Repository.UpdateUserRole(ctx, db.UpdateUserRoleParams{RoleCode: input.RoleCode.String(), UserID: input.UserID})
if err != nil {
return &UpdateUserRolePayload{}, err
}
return &UpdateUserRolePayload{User: &user}, nil
}
func (r *organizationResolver) ID(ctx context.Context, obj *db.Organization) (uuid.UUID, error) {

View File

@ -5,9 +5,20 @@ extend type Mutation {
logoutUser(input: LogoutUser!): Boolean!
clearProfileAvatar: UserAccount!
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
}
input UpdateUserPassword {
userID: UUID!
password: String!
}
type UpdateUserPasswordPayload {
ok: Boolean!
user: UserAccount!
}
input UpdateUserRole {
userID: UUID!
roleCode: RoleCode!

View File

@ -5,13 +5,27 @@ import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"time"
"github.com/jordanknott/project-citadel/api/internal/db"
"github.com/jordanknott/project-citadel/api/internal/frontend"
)
func (h *CitadelHandler) Frontend(w http.ResponseWriter, r *http.Request) {
f, err := frontend.Frontend.Open("index.h")
if os.IsNotExist(err) {
log.Warning("does not exist")
} else if err != nil {
log.WithError(err).Error("frontend")
}
http.ServeContent(w, r, "index.html", time.Now(), f)
}
func (h *CitadelHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
log.Info("preparing to upload file")
userID, ok := r.Context().Value("userID").(uuid.UUID)

View File

@ -10,16 +10,61 @@ import (
"github.com/jmoiron/sqlx"
log "github.com/sirupsen/logrus"
"github.com/jordanknott/project-citadel/api/internal/config"
"github.com/jordanknott/project-citadel/api/internal/db"
"github.com/jordanknott/project-citadel/api/internal/frontend"
"github.com/jordanknott/project-citadel/api/internal/graph"
"github.com/jordanknott/project-citadel/api/internal/logger"
"os"
"path/filepath"
)
type CitadelHandler struct {
repo db.Repository
// spaHandler implements the http.Handler interface, so we can use it
// to respond to HTTP requests. The path to the static directory and
// path to the index file within that static directory are used to
// serve the SPA in the given static directory.
type FrontendHandler struct {
staticPath string
indexPath string
}
func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
func IsDir(f http.File) bool {
fi, err := f.Stat()
if err != nil {
return false
}
return fi.IsDir()
}
func (h FrontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path, err := filepath.Abs(r.URL.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
f, err := frontend.Frontend.Open(path)
if os.IsNotExist(err) || IsDir(f) {
index, err := frontend.Frontend.Open("index.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeContent(w, r, "index.html", time.Now(), index)
return
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeContent(w, r, path, time.Now(), f)
}
type CitadelHandler struct {
config config.AppConfig
repo db.Repository
}
func NewRouter(config config.AppConfig, dbConnection *sqlx.DB) (chi.Router, error) {
formatter := new(log.TextFormatter)
formatter.TimestampFormat = "02-01-2006 15:04:05"
formatter.FullTimestamp = true
@ -47,7 +92,7 @@ func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
r.Use(middleware.Timeout(60 * time.Second))
repository := db.NewRepository(dbConnection)
citadelHandler := CitadelHandler{*repository}
citadelHandler := CitadelHandler{config, *repository}
var imgServer = http.FileServer(http.Dir("./uploads/"))
r.Group(func(mux chi.Router) {
@ -59,8 +104,11 @@ func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
r.Group(func(mux chi.Router) {
mux.Use(AuthenticationMiddleware)
mux.Post("/users/me/avatar", citadelHandler.ProfileImageUpload)
mux.Handle("/graphql", graph.NewHandler(*repository))
mux.Handle("/graphql", graph.NewHandler(config, *repository))
})
frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}
r.Handle("/*", frontend)
return r, nil
}