diff --git a/appapi.yaml b/appapi.yaml index 71556bb4558a2276a11fac2b98aec6a423cbb79e..b1061e8232697ccc7afb9dcd7b496cb0400bbab8 100755 --- a/appapi.yaml +++ b/appapi.yaml @@ -87,9 +87,9 @@ paths: get: summary: Search for users by their username. description: >- - Returns an array of objects representing the app users + Returns an array of objects representing the app users, whose usernames start with the search term. - Casing is ignored. + Caseing is ignored. The user objects are stripped down to what is necessary to know as an app user. parameters: diff --git a/endpoint/user/endpoint.go b/endpoint/user/endpoint.go index 5cbeb85b07e6efbe0fdeeff16426cf576f6a2422..5b025834e2f8634d5c8e4a50dee7b59c4077aae3 100755 --- a/endpoint/user/endpoint.go +++ b/endpoint/user/endpoint.go @@ -39,6 +39,13 @@ func SetupUserRoutes(group *gin.RouterGroup, repository UserQuery, titleReposito }) //endpoint used to fetch data of all users group.GET("/all", getAllUsers(repository)) + + group.OPTIONS("/search", func(ctx *gin.Context) { + ctx.Header("Allow", "GET") + ctx.Status(200) + }) + // endpoint used to search for users by username + group.GET("/search", searchForUsers(repository)) } /* @@ -328,6 +335,29 @@ func getAllUsers(repository UserQuery) gin.HandlerFunc { } } +func searchForUsers(repository UserQuery) gin.HandlerFunc { + return func(ctx *gin.Context) { + searchParam := ctx.Query("s") + + // Only accept searches with minimum 2 characters + if len(searchParam) < 2 { + ctx.String(http.StatusBadRequest, "Search failed. Specify at least two characters.") + return + } + + // fetch matching users from database + users, err := repository.SelectByUsernameLike(searchParam) + + if err != nil { + eventID := raven.CaptureError(err, nil) + ctx.String(http.StatusInternalServerError, "Failed fetching data from DB. "+eventID) + return + } + + ctx.JSON(http.StatusOK, users) + } +} + func ForgotPassword(repository UserQuery, tokenFactory otp.TokenFactory, templatemail *sendmail.TemplateMail) gin.HandlerFunc { return func(ctx *gin.Context) { var req ForgotPasswordRequest diff --git a/endpoint/user/query.go b/endpoint/user/query.go index fb15e4da1c37d948da43c3e2cba67d2d1deb71b2..94bb19f7b58fcd3e0bd257c529864ed485c0b554 100755 --- a/endpoint/user/query.go +++ b/endpoint/user/query.go @@ -31,6 +31,7 @@ type UserQuery interface { Insert(user *schemas.FullUserSchema, password string) (id uint32, err error) Select(id uint32) ([]schemas.FullUserSchema, error) SelectByUsername(username string) ([]schemas.InfoUserSchema, error) + SelectByUsernameLike(username string) ([]schemas.InfoUserSchema, error) SelectCredentialsByField(field UserField, value interface{}) (*schemas.CredentialsSchema, error) Authenticate(name string, password string) (uint32, error) Update(userID uint32, params PatchUserRequest, titleRepository title.TitleQuery) error @@ -332,6 +333,109 @@ func (MySQLUserQuery) SelectByUsername(username string) ([]schemas.InfoUserSchem return user, nil } +// SelectByUsernameLike fetches and returns a list of userdata of all users with matching usernames +func (MySQLUserQuery) SelectByUsernameLike(username string) ([]schemas.InfoUserSchema, error) { + db, connectionError := dbhandler.GetDBConnection() + if connectionError != nil { + return nil, errors.New("Unable to connect to database. " + connectionError.Error()) + } + + stmt, prepareError := db.Prepare(` + SELECT iduser, username, idavatar, avatar.image, avatar.level, title.idtitle, + title.name, subject.idsubject, subject.name, subject.shortform, + subject.department, subject.description, title.unlock_score, title.unlock_win + FROM user + LEFT JOIN title ON (title.idtitle=selected_title) + LEFT JOIN subject ON (idsubject=title.subject) + LEFT JOIN avatar ON (idavatar=selected_avatar) + WHERE username LIKE ?`) + + if prepareError != nil { + return nil, errors.New( + "An error occurred while preparing database access. " + prepareError.Error()) + } + defer stmt.Close() + + // execute sql query + rows, queryError := stmt.Query(username + "%") + if queryError != nil { + return nil, errors.New("Cannot execute database query. " + queryError.Error()) + } + defer rows.Close() + + users := []schemas.InfoUserSchema{} + + for rows.Next() { + var tmpUser schemas.InfoUserSchema + + scanError := scanUser(rows.Scan, &tmpUser) + if scanError != nil { + return nil, errors.New("Cannot scan db values into users list. " + scanError.Error()) + } + + users = append(users, tmpUser) + } + + return users, nil +} + +func scanUser(scanFn func(dest ...interface{}) error, tmpUser *schemas.InfoUserSchema) error { + // Temporary subject + var tmpSubjectID sql.NullInt32 + var tmpSubjectName sql.NullString + var tmpSubjectShortForm sql.NullString + var tmpSubjectDepartment sql.NullString + var tmpSubjectDescription sql.NullString + + var tmpUnlockScore sql.NullInt32 + var tmpUnlockWin sql.NullInt32 + + err := scanFn( + &tmpUser.ID, + &tmpUser.Username, + &tmpUser.SelectedAvatar.ID, + &tmpUser.SelectedAvatar.Image, + &tmpUser.SelectedAvatar.Level, + &tmpUser.SelectedTitle.ID, + &tmpUser.SelectedTitle.Name, + &(tmpSubjectID), + &(tmpSubjectName), + &(tmpSubjectShortForm), + &(tmpSubjectDepartment), + &(tmpSubjectDescription), + &tmpUnlockScore, + &tmpUnlockWin, + ) + + if err != nil { + return err + } + + if tmpSubjectName.Valid { + tmpUser.SelectedTitle.Subject = &schemas.Subject{ + ID: uint32(tmpSubjectID.Int32), + Name: tmpSubjectName.String, + ShortForm: tmpSubjectShortForm.String, + Department: tmpSubjectDepartment.String, + Description: tmpSubjectDescription.String, + } + } + + if tmpUnlockScore.Valid { + tmpUser.SelectedTitle.UnlockScore = tmpUnlockScore.Int32 + } else { + tmpUser.SelectedTitle.UnlockScore = -1 + } + + if tmpUnlockWin.Valid { + tmpUser.SelectedTitle.UnlockWin = tmpUnlockWin.Int32 + } else { + tmpUser.SelectedTitle.UnlockWin = -1 + } + + return nil +} + func (MySQLUserQuery) Authenticate(name string, password string) (uint32, error) { db, dberr := dbhandler.GetDBConnection()