feat: add pre-commit hooks & refactor code to pass linting
This commit is contained in:
@ -9,20 +9,27 @@ import (
|
||||
|
||||
var jwtKey = []byte("taskcafe_test_key")
|
||||
|
||||
// RestrictedMode is used restrict JWT access to just the install route
|
||||
type RestrictedMode string
|
||||
|
||||
const (
|
||||
// Unrestricted is the code to allow access to all routes
|
||||
Unrestricted RestrictedMode = "unrestricted"
|
||||
InstallOnly = "install_only"
|
||||
// InstallOnly is the code to restrict access ONLY to install route
|
||||
InstallOnly = "install_only"
|
||||
)
|
||||
|
||||
// Role is the role code for the user
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleAdmin Role = "admin"
|
||||
// RoleAdmin is the code for the admin role
|
||||
RoleAdmin Role = "admin"
|
||||
// RoleMember is the code for the member role
|
||||
RoleMember Role = "member"
|
||||
)
|
||||
|
||||
// AccessTokenClaims is the claims the access JWT token contains
|
||||
type AccessTokenClaims struct {
|
||||
UserID string `json:"userId"`
|
||||
Restricted RestrictedMode `json:"restricted"`
|
||||
@ -30,23 +37,23 @@ type AccessTokenClaims struct {
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
type RefreshTokenClaims struct {
|
||||
UserID string `json:"userId"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// ErrExpiredToken is the error returned if the token has expired
|
||||
type ErrExpiredToken struct{}
|
||||
|
||||
// Error returns the error message for ErrExpiredToken
|
||||
func (r *ErrExpiredToken) Error() string {
|
||||
return "token is expired"
|
||||
}
|
||||
|
||||
// ErrMalformedToken is the error returned if the token has malformed
|
||||
type ErrMalformedToken struct{}
|
||||
|
||||
// Error returns the error message for ErrMalformedToken
|
||||
func (r *ErrMalformedToken) Error() string {
|
||||
return "token is malformed"
|
||||
}
|
||||
|
||||
// NewAccessToken generates a new JWT access token with the correct claims
|
||||
func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string) (string, error) {
|
||||
role := RoleMember
|
||||
if orgRole == "admin" {
|
||||
@ -68,6 +75,7 @@ func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string
|
||||
return accessTokenString, nil
|
||||
}
|
||||
|
||||
// NewAccessTokenCustomExpiration creates an access token with a custom duration
|
||||
func NewAccessTokenCustomExpiration(userID string, dur time.Duration) (string, error) {
|
||||
accessExpirationTime := time.Now().Add(dur)
|
||||
accessClaims := &AccessTokenClaims{
|
||||
@ -85,6 +93,7 @@ func NewAccessTokenCustomExpiration(userID string, dur time.Duration) (string, e
|
||||
return accessTokenString, nil
|
||||
}
|
||||
|
||||
// ValidateAccessToken validates a JWT access token and returns the contained claims or an error if it's invalid
|
||||
func ValidateAccessToken(accessTokenString string) (AccessTokenClaims, error) {
|
||||
accessClaims := &AccessTokenClaims{}
|
||||
accessToken, err := jwt.ParseWithClaims(accessTokenString, accessClaims, func(token *jwt.Token) (interface{}, error) {
|
||||
@ -112,18 +121,3 @@ func ValidateAccessToken(accessTokenString string) (AccessTokenClaims, error) {
|
||||
}
|
||||
return AccessTokenClaims{}, err
|
||||
}
|
||||
|
||||
func NewRefreshToken(userID string) (string, time.Time, error) {
|
||||
refreshExpirationTime := time.Now().Add(24 * time.Hour)
|
||||
refreshClaims := &RefreshTokenClaims{
|
||||
UserID: userID,
|
||||
StandardClaims: jwt.StandardClaims{ExpiresAt: refreshExpirationTime.Unix()},
|
||||
}
|
||||
|
||||
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
||||
refreshTokenString, err := refreshToken.SignedString(jwtKey)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
return refreshTokenString, refreshExpirationTime, nil
|
||||
}
|
||||
|
@ -9,10 +9,6 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const TaskcafeConfDirEnvName = "TASKCAFE_CONFIG_DIR"
|
||||
|
||||
const TaskcafeAppConf = "taskcafe"
|
||||
|
||||
const mainDescription = `Taskcafé is an open soure project management
|
||||
system written in Golang & React.`
|
||||
|
||||
@ -26,10 +22,6 @@ 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 cfgFile string
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@ -69,6 +61,7 @@ func initConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the root cobra command
|
||||
func Execute() {
|
||||
rootCmd.SetVersionTemplate(versionTemplate)
|
||||
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd())
|
||||
|
@ -3,8 +3,6 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jordanknott/taskcafe/internal/migrations"
|
||||
)
|
||||
|
||||
|
@ -8,16 +8,17 @@ import (
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/golang-migrate/migrate/v4/source/httpfs"
|
||||
"github.com/jmoiron/sqlx"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// MigrateLog is a logger for go migrate
|
||||
type MigrateLog struct {
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// Printf logs to logrus
|
||||
func (l *MigrateLog) Printf(format string, v ...interface{}) {
|
||||
log.Printf("%s", v)
|
||||
}
|
||||
|
@ -2,14 +2,14 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/golang-migrate/migrate/v4/source/httpfs"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jordanknott/taskcafe/internal/route"
|
||||
|
@ -1,58 +0,0 @@
|
||||
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
|
||||
}
|
@ -19,4 +19,3 @@ DELETE FROM task_group WHERE task_group_id = $1;
|
||||
|
||||
-- name: UpdateTaskGroupLocation :one
|
||||
UPDATE task_group SET position = $2 WHERE task_group_id = $1 RETURNING *;
|
||||
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// Repository contains methods for interacting with a database storage
|
||||
type Repository struct {
|
||||
*Queries
|
||||
db *sqlx.DB
|
||||
|
@ -2405,7 +2405,7 @@ type Query {
|
||||
teams: [Team!]!
|
||||
labelColors: [LabelColor!]!
|
||||
taskGroups: [TaskGroup!]!
|
||||
me: MePayload!
|
||||
me: MePayload!
|
||||
}
|
||||
|
||||
type Mutation
|
||||
|
@ -18,7 +18,6 @@ import (
|
||||
"github.com/jordanknott/taskcafe/internal/auth"
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
)
|
||||
|
||||
// NewHandler returns a new graphql endpoint handler.
|
||||
@ -107,26 +106,32 @@ func NewPlaygroundHandler(endpoint string) http.Handler {
|
||||
return playground.Handler("GraphQL Playground", endpoint)
|
||||
}
|
||||
|
||||
// GetUserID retrieves the UserID out of a context
|
||||
func GetUserID(ctx context.Context) (uuid.UUID, bool) {
|
||||
userID, ok := ctx.Value("userID").(uuid.UUID)
|
||||
return userID, ok
|
||||
}
|
||||
|
||||
// GetUserRole retrieves the user role out of a context
|
||||
func GetUserRole(ctx context.Context) (auth.Role, bool) {
|
||||
role, ok := ctx.Value("org_role").(auth.Role)
|
||||
return role, ok
|
||||
}
|
||||
|
||||
// GetUser retrieves both the user id & user role out of a context
|
||||
func GetUser(ctx context.Context) (uuid.UUID, auth.Role, bool) {
|
||||
userID, userOK := GetUserID(ctx)
|
||||
role, roleOK := GetUserRole(ctx)
|
||||
return userID, role, userOK && roleOK
|
||||
}
|
||||
|
||||
// GetRestrictedMode retrieves the restricted mode code out of a context
|
||||
func GetRestrictedMode(ctx context.Context) (auth.RestrictedMode, bool) {
|
||||
restricted, ok := ctx.Value("restricted_mode").(auth.RestrictedMode)
|
||||
return restricted, ok
|
||||
}
|
||||
|
||||
// GetProjectRoles retrieves the team & project role for the given project ID
|
||||
func GetProjectRoles(ctx context.Context, r db.Repository, projectID uuid.UUID) (db.GetUserRolesForProjectRow, error) {
|
||||
userID, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
@ -135,6 +140,7 @@ func GetProjectRoles(ctx context.Context, r db.Repository, projectID uuid.UUID)
|
||||
return r.GetUserRolesForProject(ctx, db.GetUserRolesForProjectParams{UserID: userID, ProjectID: projectID})
|
||||
}
|
||||
|
||||
// ConvertToRoleCode converts a role code string to a RoleCode type
|
||||
func ConvertToRoleCode(r string) RoleCode {
|
||||
if r == RoleCodeAdmin.String() {
|
||||
return RoleCodeAdmin
|
||||
@ -144,47 +150,3 @@ func ConvertToRoleCode(r string) RoleCode {
|
||||
}
|
||||
return RoleCodeObserver
|
||||
}
|
||||
|
||||
func RequireTeamAdmin(ctx context.Context, r db.Repository, teamID uuid.UUID) error {
|
||||
userID, role, ok := GetUser(ctx)
|
||||
if !ok {
|
||||
return errors.New("internal: user id is not set")
|
||||
}
|
||||
teamRole, err := r.GetTeamRoleForUserID(ctx, db.GetTeamRoleForUserIDParams{UserID: userID, TeamID: teamID})
|
||||
isAdmin := role == auth.RoleAdmin
|
||||
isTeamAdmin := err == nil && ConvertToRoleCode(teamRole.RoleCode) == RoleCodeAdmin
|
||||
if !(isAdmin || isTeamAdmin) {
|
||||
return &gqlerror.Error{
|
||||
Message: "organization or team admin role required",
|
||||
Extensions: map[string]interface{}{
|
||||
"code": "2-400",
|
||||
},
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RequireProjectOrTeamAdmin(ctx context.Context, r db.Repository, projectID uuid.UUID) error {
|
||||
role, ok := GetUserRole(ctx)
|
||||
if !ok {
|
||||
return errors.New("user ID is not set")
|
||||
}
|
||||
if role == auth.RoleAdmin {
|
||||
return nil
|
||||
}
|
||||
roles, err := GetProjectRoles(ctx, r, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !(roles.ProjectRole == "admin" || roles.TeamRole == "admin") {
|
||||
return &gqlerror.Error{
|
||||
Message: "You must be a team or project admin",
|
||||
Extensions: map[string]interface{}{
|
||||
"code": "4-400",
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -7,9 +7,12 @@ import (
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
)
|
||||
|
||||
// GetOwnedList todo: remove this
|
||||
func GetOwnedList(ctx context.Context, r db.Repository, user db.UserAccount) (*OwnedList, error) {
|
||||
return &OwnedList{}, nil
|
||||
}
|
||||
|
||||
// GetMemberList returns a list of projects the user is a member of
|
||||
func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*MemberList, error) {
|
||||
projectMemberIDs, err := r.GetMemberProjectIDsForUserID(ctx, user.UserID)
|
||||
if err != sql.ErrNoRows && err != nil {
|
||||
|
@ -1,5 +1,6 @@
|
||||
//go:generate sh ../scripts/genSchema.sh
|
||||
//go:generate go run github.com/99designs/gqlgen
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
@ -8,6 +9,7 @@ import (
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
)
|
||||
|
||||
// Resolver handles resolving GraphQL queries & mutations
|
||||
type Resolver struct {
|
||||
Repository db.Repository
|
||||
mu sync.Mutex
|
||||
|
@ -3,18 +3,21 @@ package graph
|
||||
import (
|
||||
"io"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// MarshalUUID converts a UUID to JSON string
|
||||
func MarshalUUID(uuid uuid.UUID) graphql.Marshaler {
|
||||
return graphql.WriterFunc(func(w io.Writer) {
|
||||
w.Write([]byte(strconv.Quote(uuid.String())))
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalUUID converts a String to a UUID
|
||||
func UnmarshalUUID(v interface{}) (uuid.UUID, error) {
|
||||
if uuidRaw, ok := v.(string); ok {
|
||||
return uuid.Parse(uuidRaw)
|
||||
|
@ -187,7 +187,7 @@ type Query {
|
||||
teams: [Team!]!
|
||||
labelColors: [LabelColor!]!
|
||||
taskGroups: [TaskGroup!]!
|
||||
me: MePayload!
|
||||
me: MePayload!
|
||||
}
|
||||
|
||||
type Mutation
|
||||
@ -663,4 +663,3 @@ type DeleteUserAccountPayload {
|
||||
ok: Boolean!
|
||||
userAccount: UserAccount!
|
||||
}
|
||||
|
||||
|
@ -800,11 +800,11 @@ func (r *queryResolver) Users(ctx context.Context) ([]db.UserAccount, error) {
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindUser(ctx context.Context, input FindUser) (*db.UserAccount, error) {
|
||||
userId, err := uuid.Parse(input.UserID)
|
||||
userID, err := uuid.Parse(input.UserID)
|
||||
if err != nil {
|
||||
return &db.UserAccount{}, err
|
||||
}
|
||||
account, err := r.Repository.GetUserAccountByID(ctx, userId)
|
||||
account, err := r.Repository.GetUserAccountByID(ctx, userID)
|
||||
if err == sql.ErrNoRows {
|
||||
return &db.UserAccount{}, &gqlerror.Error{
|
||||
Message: "User not found",
|
||||
@ -1097,9 +1097,9 @@ func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, e
|
||||
return &TaskBadges{}, err
|
||||
}
|
||||
for _, item := range items {
|
||||
total += 1
|
||||
total++
|
||||
if item.Complete {
|
||||
complete += 1
|
||||
complete++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ type Query {
|
||||
teams: [Team!]!
|
||||
labelColors: [LabelColor!]!
|
||||
taskGroups: [TaskGroup!]!
|
||||
me: MePayload!
|
||||
me: MePayload!
|
||||
}
|
||||
|
||||
type Mutation
|
||||
|
@ -25,4 +25,3 @@ type DeleteProjectPayload {
|
||||
ok: Boolean!
|
||||
project: Project!
|
||||
}
|
||||
|
||||
|
@ -9,19 +9,17 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// StructuredLogger is a simple, but powerful implementation of a custom structured
|
||||
// logger backed on logrus. I encourage users to copy it, adapt it and make it their
|
||||
// own. Also take a look at https://github.com/pressly/lg for a dedicated pkg based
|
||||
// on this work, designed for context-based http routers.
|
||||
|
||||
// NewStructuredLogger creates a new logger for chi router
|
||||
func NewStructuredLogger(logger *logrus.Logger) func(next http.Handler) http.Handler {
|
||||
return middleware.RequestLogger(&StructuredLogger{logger})
|
||||
}
|
||||
|
||||
// StructuredLogger is a logger for chi router
|
||||
type StructuredLogger struct {
|
||||
Logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewLogEntry creates a new log entry for the given HTTP request
|
||||
func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
entry := &StructuredLoggerEntry{Logger: logrus.NewEntry(l.Logger)}
|
||||
logFields := logrus.Fields{}
|
||||
@ -48,10 +46,12 @@ func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
return entry
|
||||
}
|
||||
|
||||
// StructuredLoggerEntry is a log entry will all relevant information about a specific http request
|
||||
type StructuredLoggerEntry struct {
|
||||
Logger logrus.FieldLogger
|
||||
}
|
||||
|
||||
// Write logs information about http request response body
|
||||
func (l *StructuredLoggerEntry) Write(status, bytes int, elapsed time.Duration) {
|
||||
l.Logger = l.Logger.WithFields(logrus.Fields{
|
||||
"resp_status": status, "resp_bytes_length": bytes,
|
||||
@ -60,6 +60,7 @@ func (l *StructuredLoggerEntry) Write(status, bytes int, elapsed time.Duration)
|
||||
l.Logger.Debugln("request complete")
|
||||
}
|
||||
|
||||
// Panic logs if the request panics
|
||||
func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
|
||||
l.Logger = l.Logger.WithFields(logrus.Fields{
|
||||
"stack": string(stack),
|
||||
@ -67,24 +68,20 @@ func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
|
||||
})
|
||||
}
|
||||
|
||||
// Helper methods used by the application to get the request-scoped
|
||||
// logger entry and set additional fields between handlers.
|
||||
//
|
||||
// This is a useful pattern to use to set state on the entry as it
|
||||
// passes through the handler chain, which at any point can be logged
|
||||
// with a call to .Print(), .Info(), etc.
|
||||
|
||||
// GetLogEntry helper function for getting log entry for request
|
||||
func GetLogEntry(r *http.Request) logrus.FieldLogger {
|
||||
entry := middleware.GetLogEntry(r).(*StructuredLoggerEntry)
|
||||
return entry.Logger
|
||||
}
|
||||
|
||||
// LogEntrySetField sets a key's value
|
||||
func LogEntrySetField(r *http.Request, key string, value interface{}) {
|
||||
if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
|
||||
entry.Logger = entry.Logger.WithField(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// LogEntrySetFields sets the log entry's fields
|
||||
func LogEntrySetFields(r *http.Request, fields map[string]interface{}) {
|
||||
if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
|
||||
entry.Logger = entry.Logger.WithFields(fields)
|
||||
|
@ -18,11 +18,13 @@ var jwtKey = []byte("taskcafe_test_key")
|
||||
|
||||
type authResource struct{}
|
||||
|
||||
// LoginRequestData is the request data when a user logs in
|
||||
type LoginRequestData struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// NewUserAccount is the request data for a new user
|
||||
type NewUserAccount struct {
|
||||
FullName string `json:"fullname"`
|
||||
Username string
|
||||
@ -31,30 +33,35 @@ type NewUserAccount struct {
|
||||
Email string
|
||||
}
|
||||
|
||||
// InstallRequestData is the request data for installing new Taskcafe app
|
||||
type InstallRequestData struct {
|
||||
User NewUserAccount
|
||||
}
|
||||
|
||||
// LoginResponseData is the response data for when a user logs in
|
||||
type LoginResponseData struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
IsInstalled bool `json:"isInstalled"`
|
||||
}
|
||||
|
||||
// LogoutResponseData is the response data for when a user logs out
|
||||
type LogoutResponseData struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// RefreshTokenResponseData is the response data for when an access token is refreshed
|
||||
type RefreshTokenResponseData struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
}
|
||||
|
||||
// AvatarUploadResponseData is the response data for a user profile is uploaded
|
||||
type AvatarUploadResponseData struct {
|
||||
UserID string `json:"userID"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// RefreshTokenHandler handles when a user attempts to refresh token
|
||||
func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
_, err := h.repo.GetSystemOptionByKey(r.Context(), "is_installed")
|
||||
if err == sql.ErrNoRows {
|
||||
user, err := h.repo.GetUserAccountByUsername(r.Context(), "system")
|
||||
@ -131,6 +138,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
||||
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, IsInstalled: true})
|
||||
}
|
||||
|
||||
// LogoutHandler removes all refresh tokens to log out user
|
||||
func (h *TaskcafeHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := r.Cookie("refreshToken")
|
||||
if err != nil {
|
||||
@ -150,6 +158,7 @@ func (h *TaskcafeHandler) LogoutHandler(w http.ResponseWriter, r *http.Request)
|
||||
json.NewEncoder(w).Encode(LogoutResponseData{Status: "success"})
|
||||
}
|
||||
|
||||
// LoginHandler creates a new refresh & access token for the user if given the correct credentials
|
||||
func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var requestData LoginRequestData
|
||||
err := json.NewDecoder(r.Body).Decode(&requestData)
|
||||
@ -171,8 +180,8 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(requestData.Password))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"username": requestData.Username,
|
||||
}).Warn("password incorrect for user")
|
||||
"username": requestData.Username,
|
||||
}).Warn("password incorrect for user")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@ -196,6 +205,7 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
|
||||
}
|
||||
|
||||
// InstallHandler creates first user on fresh install
|
||||
func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if restricted, ok := r.Context().Value("restricted_mode").(auth.RestrictedMode); ok {
|
||||
if restricted != auth.InstallOnly {
|
||||
@ -256,6 +266,7 @@ func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request)
|
||||
json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
|
||||
}
|
||||
|
||||
// Routes registers all authentication routes
|
||||
func (rs authResource) Routes(taskcafeHandler TaskcafeHandler) chi.Router {
|
||||
r := chi.NewRouter()
|
||||
r.Post("/login", taskcafeHandler.LoginHandler)
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"github.com/jordanknott/taskcafe/internal/frontend"
|
||||
)
|
||||
|
||||
// Frontend serves the index.html file
|
||||
func (h *TaskcafeHandler) Frontend(w http.ResponseWriter, r *http.Request) {
|
||||
f, err := frontend.Frontend.Open("index.h")
|
||||
if os.IsNotExist(err) {
|
||||
@ -26,6 +27,7 @@ func (h *TaskcafeHandler) Frontend(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeContent(w, r, "index.html", time.Now(), f)
|
||||
}
|
||||
|
||||
// ProfileImageUpload handles a user uploading a new avatar profile image
|
||||
func (h *TaskcafeHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info("preparing to upload file")
|
||||
userID, ok := r.Context().Value("userID").(uuid.UUID)
|
||||
|
@ -10,6 +10,19 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ContextKey represents a context key
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
// UserIDKey is the key for the user id of the authenticated user
|
||||
UserIDKey ContextKey = "userID"
|
||||
//RestrictedModeKey is the key for whether the authenticated user only has access to install route
|
||||
RestrictedModeKey ContextKey = "restricted_mode"
|
||||
// OrgRoleKey is the key for the organization role code of the authenticated user
|
||||
OrgRoleKey ContextKey = "org_role"
|
||||
)
|
||||
|
||||
// AuthenticationMiddleware is a middleware that requires a valid JWT token to be passed via the Authorization header
|
||||
func AuthenticationMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
bearerTokenRaw := r.Header.Get("Authorization")
|
||||
@ -51,9 +64,9 @@ func AuthenticationMiddleware(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), "userID", userID)
|
||||
ctx = context.WithValue(ctx, "restricted_mode", accessClaims.Restricted)
|
||||
ctx = context.WithValue(ctx, "org_role", accessClaims.OrgRole)
|
||||
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
||||
ctx = context.WithValue(ctx, RestrictedModeKey, accessClaims.Restricted)
|
||||
ctx = context.WithValue(ctx, OrgRoleKey, accessClaims.OrgRole)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
|
@ -10,23 +10,22 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
"github.com/jordanknott/taskcafe/internal/frontend"
|
||||
"github.com/jordanknott/taskcafe/internal/graph"
|
||||
"github.com/jordanknott/taskcafe/internal/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// 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.
|
||||
// FrontendHandler serves an embed React client through chi
|
||||
type FrontendHandler struct {
|
||||
staticPath string
|
||||
indexPath string
|
||||
}
|
||||
|
||||
// IsDir checks if the given file is a directory
|
||||
func IsDir(f http.File) bool {
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
@ -35,6 +34,7 @@ func IsDir(f http.File) bool {
|
||||
return fi.IsDir()
|
||||
}
|
||||
|
||||
// ServeHTTP attempts to serve a requested file for the embedded React client
|
||||
func (h FrontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path, err := filepath.Abs(r.URL.Path)
|
||||
if err != nil {
|
||||
@ -58,10 +58,12 @@ func (h FrontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeContent(w, r, path, time.Now(), f)
|
||||
}
|
||||
|
||||
// TaskcafeHandler contains all the route handlers
|
||||
type TaskcafeHandler struct {
|
||||
repo db.Repository
|
||||
}
|
||||
|
||||
// NewRouter creates a new router for chi
|
||||
func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
|
||||
formatter := new(log.TextFormatter)
|
||||
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
||||
|
Reference in New Issue
Block a user