diff --git a/.gitignore b/.gitignore index 71da8c2..1d7fb26 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ # Folders _obj _test +elm-stuff +node_modules # Architecture specific extensions/prefixes *.[568vq] @@ -22,5 +24,3 @@ _testmain.go *.exe *.test *.prof - -elm-stuff diff --git a/cmd/console/console.go b/cmd/console/console.go index 6fb51ac..c9e9528 100644 --- a/cmd/console/console.go +++ b/cmd/console/console.go @@ -366,6 +366,31 @@ func main() { ), ) + router.Methods("GET").Path("/api/apps/{appID:[0-9]+}/users/{userID:[0-9]+}").Name("userFetch").HandlerFunc( + handler.Wrap( + withConstraints, + handler.UserFetchConsole( + core.UserFetchConsole(apps, users), + ), + ), + ) + + router.Methods("GET").Path("/api/apps/{appID:[0-9]+}/users/search").Name("userSearch").HandlerFunc( + handler.Wrap( + withConstraints, + handler.UserSearchConsole( + core.UserSearchConsole(apps, users), + ), + ), + ) + + router.Methods("PUT").Path(`/api/apps/{appID:[0-9]+}/users/{userID:[0-9]+}`).Name("userUpdate").HandlerFunc( + handler.Wrap( + withConstraints, + handler.UserUpdateConsole(core.UserUpdateConsole(apps, users)), + ), + ) + router.Methods("GET").PathPrefix("/fonts").Name("fonts").Handler( http.FileServer(FS(*staticLocal)), ) diff --git a/cmd/console/src/Action.elm b/cmd/console/src/Action.elm index 2cbba0d..484c0b6 100644 --- a/cmd/console/src/Action.elm +++ b/cmd/console/src/Action.elm @@ -8,6 +8,7 @@ import App.Model exposing (App) import Member.Model exposing (Member) import Route exposing (Route) import Rule.Model exposing (Rule) +import User.Model exposing (User) type Msg @@ -33,3 +34,16 @@ type Msg | RuleDelete (Result Http.Error ()) | Tick Time | TokenPersist String + | UserFetch (WebData User) + | UserSearch (WebData (List User)) + | UserSearchFormBlur String + | UserSearchFormClear + | UserSearchFormFocus String + | UserSearchFormSubmit + | UserSearchFormUpdate String String + | UserUpdate (WebData User) + | UserUpdateFormBlur String + | UserUpdateFormClear + | UserUpdateFormFocus String + | UserUpdateFormSubmit + | UserUpdateFormUpdate String String diff --git a/cmd/console/src/Error.elm b/cmd/console/src/Error.elm new file mode 100644 index 0000000..b6656dd --- /dev/null +++ b/cmd/console/src/Error.elm @@ -0,0 +1,21 @@ +module Error exposing (decodeList) + +import Json.Decode as Decode + + +type alias Error = + { code : Int + , message : String + } + + +decode : Decode.Decoder Error +decode = + Decode.map2 Error + (Decode.field "code" Decode.int) + (Decode.field "message" Decode.string) + + +decodeList : Decode.Decoder (List Error) +decodeList = + Decode.at [ "errors" ] (Decode.list decode) diff --git a/cmd/console/src/Model.elm b/cmd/console/src/Model.elm index 497e7ff..b722843 100644 --- a/cmd/console/src/Model.elm +++ b/cmd/console/src/Model.elm @@ -13,6 +13,8 @@ import Member.Model exposing (Member) import Route exposing (Route, parse) import Rule.Api exposing (getRule, listRules) import Rule.Model exposing (Rule) +import User.Api exposing (getUser) +import User.Model exposing (User, initUserSearchForm, initUserUpdateForm) type alias Flags = @@ -35,6 +37,11 @@ type alias Model = , rules : WebData (List Rule) , startTime : Time , time : Time + , user : WebData User + , userId : String + , users : WebData (List User) + , userSearchForm : Form + , userUpdateForm : Form , zone : String } @@ -57,7 +64,7 @@ init { loginUrl, zone } location = Just code -> ( model, Cmd.map MemberLogin (loginMember code) ) - Just (Route.Login) -> + Just Route.Login -> ( model, Cmd.none ) _ -> @@ -71,7 +78,7 @@ init { loginUrl, zone } location = initModel : String -> String -> Maybe Route -> Model initModel loginUrl zone route = - Model NotAsked NotAsked initAppForm "" "" loginUrl Loading NotAsked route NotAsked NotAsked 0 0 zone + Model NotAsked NotAsked initAppForm "" "" loginUrl Loading NotAsked route NotAsked NotAsked 0 0 NotAsked "" NotAsked initUserSearchForm (initUserUpdateForm NotAsked) zone initRoute : Model -> ( Model, Cmd Msg ) @@ -80,9 +87,26 @@ initRoute model = Just (Route.App id) -> ( { model | app = Loading, appId = id }, Cmd.map FetchApp (getApp id) ) - Just (Route.Apps) -> + Just Route.Apps -> ( { model | apps = Loading }, Cmd.map FetchApps getApps ) + Just (Route.Rule appId ruleId) -> + case model.app of + Success _ -> + ( { model | appId = appId, rule = Loading } + , Cmd.batch + [ Cmd.map FetchRule (getRule appId ruleId) + ] + ) + + _ -> + ( { model | app = Loading, appId = appId, rule = Loading } + , Cmd.batch + [ Cmd.map FetchApp (getApp appId) + , Cmd.map FetchRule (getRule appId ruleId) + ] + ) + Just (Route.Rules appId) -> case model.app of Success _ -> @@ -100,23 +124,31 @@ initRoute model = ] ) - Just (Route.Rule appId ruleId) -> + Just (Route.User appId userId) -> case model.app of Success _ -> - ( { model | appId = appId, rule = Loading } + ( { model | appId = appId, user = Loading, userId = userId } , Cmd.batch - [ Cmd.map FetchRule (getRule appId ruleId) + [ Cmd.map UserFetch (getUser appId userId) ] ) _ -> - ( { model | app = Loading, appId = appId, rule = Loading } + ( { model | app = Loading, appId = appId, user = Loading, userId = userId } , Cmd.batch [ Cmd.map FetchApp (getApp appId) - , Cmd.map FetchRule (getRule appId ruleId) + , Cmd.map UserFetch (getUser appId userId) ] ) + Just (Route.Users appId) -> + case model.app of + Success _ -> + ( model, Cmd.none ) + + _ -> + ( { model | app = Loading, appId = appId }, Cmd.map FetchApp (getApp appId) ) + _ -> ( model, Cmd.none ) diff --git a/cmd/console/src/Route.elm b/cmd/console/src/Route.elm index 5568d89..c246114 100644 --- a/cmd/console/src/Route.elm +++ b/cmd/console/src/Route.elm @@ -16,6 +16,7 @@ type Route | OAuthCallback (Maybe String) (Maybe String) | Rule String String | Rules String + | User String String | Users String @@ -50,6 +51,9 @@ construct route = Rules appId -> "/apps/" ++ appId ++ "/rules" + User appId uesrId -> + "/apps/" ++ appId ++ "/users/" ++ uesrId + Users appId -> "/apps/" ++ appId ++ "/users" @@ -75,5 +79,6 @@ routes = , map OAuthCallback (s "oauth2callback" stringParam "code" stringParam "state") , map Rule (s "apps" string s "rules" string) , map Rules (s "apps" string s "rules") + , map User (s "apps" string s "users" string) , map Users (s "apps" string s "users") ] diff --git a/cmd/console/src/Update.elm b/cmd/console/src/Update.elm index 37524ae..5778b12 100644 --- a/cmd/console/src/Update.elm +++ b/cmd/console/src/Update.elm @@ -11,6 +11,8 @@ import App.Api exposing (createApp) import App.Model exposing (initAppForm) import Route import Rule.Api exposing (activateRule, deactivateRule, deleteRule) +import User.Api exposing (searchUser, updateUser) +import User.Model exposing (initUserSearchForm, initUserUpdateForm) update : Msg -> Model -> ( Model, Cmd Msg ) @@ -131,6 +133,63 @@ update msg model = in ( { model | startTime = startTime, time = time }, Cmd.none ) + UserFetch response -> + ( { model | user = response, userUpdateForm = (initUserUpdateForm response) }, Cmd.none ) + + UserSearch response -> + ( { model | users = response }, Cmd.none ) + + UserSearchFormBlur field -> + ( { model | userSearchForm = blurElement model.userSearchForm field }, Cmd.none ) + + UserSearchFormClear -> + ( { model | userSearchForm = initUserSearchForm }, Cmd.none ) + + UserSearchFormFocus field -> + ( { model | userSearchForm = focusElement model.userSearchForm field }, Cmd.none ) + + UserSearchFormSubmit -> + let + ( form, isValid ) = + validateForm model.userSearchForm + in + case isValid of + True -> + ( { model | users = Loading }, Cmd.map UserSearch (searchUser model.appId (elementValue model.userSearchForm "query")) ) + + False -> + ( { model | userSearchForm = form }, Cmd.none ) + + UserSearchFormUpdate field value -> + ( { model | userSearchForm = updateElementValue model.userSearchForm field value }, Cmd.none ) + + UserUpdate response -> + ( { model | user = response }, Cmd.none ) + + UserUpdateFormBlur field -> + ( { model | userUpdateForm = blurElement model.userUpdateForm field }, Cmd.none ) + + UserUpdateFormClear -> + ( { model | userUpdateForm = (initUserUpdateForm model.user) }, Cmd.none ) + + UserUpdateFormFocus field -> + ( { model | userUpdateForm = focusElement model.userUpdateForm field }, Cmd.none ) + + UserUpdateFormSubmit -> + let + ( form, isValid ) = + validateForm model.userUpdateForm + in + case isValid of + True -> + ( { model | user = Loading }, Cmd.map UserUpdate (updateUser model.appId model.userId (elementValue model.userUpdateForm "username")) ) + + False -> + ( { model | userUpdateForm = form }, Cmd.none ) + + UserUpdateFormUpdate field value -> + ( { model | userUpdateForm = updateElementValue model.userUpdateForm field value }, Cmd.none ) + -- HELPER diff --git a/cmd/console/src/User/Api.elm b/cmd/console/src/User/Api.elm new file mode 100644 index 0000000..717fad0 --- /dev/null +++ b/cmd/console/src/User/Api.elm @@ -0,0 +1,36 @@ +module User.Api exposing (getUser, searchUser, updateUser) + +import Http +import RemoteData exposing (WebData, sendRequest) +import User.Model exposing (User, decode, decodeList, encode) + + +getUser : String -> String -> Cmd (WebData User) +getUser appId userId = + Http.get ("/api/apps/" ++ appId ++ "/users/" ++ userId) decode + |> sendRequest + + +searchUser : String -> String -> Cmd (WebData (List User)) +searchUser appId query = + Http.get ("/api/apps/" ++ appId ++ "/users/search?q=" ++ query) decodeList + |> sendRequest + + +updateUser : String -> String -> String -> Cmd (WebData User) +updateUser appId userId username = + Http.request + { body = (encode username |> Http.jsonBody) + , expect = Http.expectJson decode + , headers = [] + , method = "PUT" + , timeout = Nothing + , url = (userUrl appId userId) + , withCredentials = False + } + |> sendRequest + + +userUrl : String -> String -> String +userUrl appId userId = + "/api/apps/" ++ appId ++ "/users/" ++ userId diff --git a/cmd/console/src/User/Model.elm b/cmd/console/src/User/Model.elm new file mode 100644 index 0000000..6e3d0e9 --- /dev/null +++ b/cmd/console/src/User/Model.elm @@ -0,0 +1,77 @@ +module User.Model exposing (User, decode, decodeList, encode, initUserSearchForm, initUserUpdateForm) + +import Json.Decode as Decode +import Json.Encode as Encode +import RemoteData exposing (RemoteData(Success), WebData) +import Formo exposing (Form, initForm, updateElementValue, validatorExist, validatorMinLength) + + +-- MODEL + + +type alias User = + { email : String + , enabled : Bool + , id : String + , username : String + } + + + +-- DECODERS + + +decode : Decode.Decoder User +decode = + Decode.map4 User + (Decode.field "email" Decode.string) + (Decode.field "enabled" Decode.bool) + (Decode.field "id_string" Decode.string) + (Decode.field "user_name" Decode.string) + + +decodeList : Decode.Decoder (List User) +decodeList = + Decode.at [ "users" ] (Decode.list decode) + + +encode : String -> Encode.Value +encode username = + Encode.object + [ ( "user_name", Encode.string username ) + ] + + + +-- FORM + + +initUserSearchForm : Form +initUserSearchForm = + initForm + [ ( "query" + , [ validatorExist + , validatorMinLength 3 + ] + ) + ] + + +initUserUpdateForm : WebData User -> Form +initUserUpdateForm user = + let + form = + initForm + [ ( "username" + , [ validatorExist + , validatorMinLength 3 + ] + ) + ] + in + case user of + Success user -> + updateElementValue form "username" user.username + + _ -> + form diff --git a/cmd/console/src/User/View.elm b/cmd/console/src/User/View.elm new file mode 100644 index 0000000..ae52d4a --- /dev/null +++ b/cmd/console/src/User/View.elm @@ -0,0 +1,51 @@ +module User.View exposing (viewUser, viewUserItem, viewUserTable) + +import Color exposing (rgb) +import Json.Decode as Decode +import Html exposing (Html, h3, li, span, strong, table, tbody, td, text, th, thead, tr, ul) +import Html.Attributes exposing (class) +import Html.Events exposing (onClick) +import User.Model exposing (User) + + +viewEnabled : Bool -> Html msg +viewEnabled enabled = + if enabled then + span [ class "nc-icon-outline ui-1_check-circle-08" ] [] + else + span [ class "nc-icon-outline ui-1_circle-remove" ] [] + + +viewUser : User -> Html msg +viewUser user = + h3 [] + [ text "This is the user" + , strong [] [ text user.username ] + , text "with the ID" + , strong [] [ text user.id ] + , text "and email" + , strong [] [ text user.email ] + ] + + +viewUserItem : msg -> User -> Html msg +viewUserItem msg user = + tr [ onClick msg ] + [ td [ class "icon" ] [ viewEnabled user.enabled ] + , td [] [ text user.username ] + , td [] [ text user.id ] + ] + + +viewUserTable : (User -> Html msg) -> List User -> Html msg +viewUserTable item users = + table [ class "navigation" ] + [ thead [] + [ tr [] + [ th [ class "icon" ] [ text "enabled" ] + , th [] [ text "username" ] + , th [] [ text "id" ] + ] + ] + , tbody [] (List.map item users) + ] diff --git a/cmd/console/src/View.elm b/cmd/console/src/View.elm index c2a5486..e1cb4b4 100644 --- a/cmd/console/src/View.elm +++ b/cmd/console/src/View.elm @@ -5,12 +5,15 @@ import Color exposing (rgb) import Html exposing (..) import Html.Attributes exposing (class, href, id, placeholder, src, title, type_, value) import Html.Events exposing (onBlur, onClick, onFocus, onInput, onSubmit) +import Http exposing (Error(BadStatus)) +import Json.Decode as Decode import RemoteData exposing (RemoteData(Failure, Loading, NotAsked, Success), WebData) import Time exposing (Time) import Action exposing (..) import App.Model exposing (App) import App.View exposing (viewAppItem, viewAppsTable) import Container +import Error import Formo exposing (Form, elementErrors, elementIsFocused, elementIsValid, elementValue, formIsValidated) import Loader import Member.Model exposing (Member) @@ -18,6 +21,8 @@ import Model exposing (Model, isLoggedIn) import Route import Rule.Model exposing (Rule) import Rule.View exposing (viewRule, viewRuleItem, viewRuleTable) +import User.Model exposing (User) +import User.View exposing (viewUser, viewUserItem, viewUserTable) view : Model -> Html Msg @@ -36,16 +41,16 @@ getPage model = Just (Route.App _) -> pageApp model - Just (Route.Apps) -> + Just Route.Apps -> pageApps model - Just (Route.Dashboard) -> + Just Route.Dashboard -> pageDashboard model - Just (Route.Login) -> + Just Route.Login -> pageLogin model - Just (Route.Members) -> + Just Route.Members -> pageNotFound Just (Route.OAuthCallback _ _) -> @@ -57,8 +62,11 @@ getPage model = Just (Route.Rules _) -> pageRules model + Just (Route.User _ _) -> + pageUser model + Just (Route.Users _) -> - pageNotFound + pageUsers model else if model.route == (Just Route.Login) then pageLogin model else @@ -113,12 +121,12 @@ pageApps { app, apps, appForm, newApp, startTime, time } = if List.length apps == 0 then div [] [ h3 [] [ text "Looks like you haven't created an App yet." ] - --, formApp newApp appForm startTime time + , formApp newApp appForm startTime time ] else div [] [ viewAppsTable viewItem apps - --, formApp newApp appForm startTime time + , formApp newApp appForm startTime time ] in main_ [] @@ -191,21 +199,6 @@ pageNotFound = [ h3 [] [ text "Looks like we couldn't find the page you were looking for." ] ] -viewAction : ( Msg, String, Maybe Int, String ) -> Html Msg -viewAction ( msg, icon, _, name ) = - li [] - [ a [ onClick msg ] - [ div [ class ("icon nc-icon-glyph " ++ icon) ] [] - , div [ class "name" ] [ text name ] - ] - ] - -viewActions : List ( Msg, String, Maybe Int, String ) -> Html Msg -viewActions actions = - Container.view (section [ class "actions" ]) - [ ul [] (List.map viewAction actions) - ] - pageRule : Model -> Html Msg pageRule { app, appId, rule, startTime, time } = @@ -236,7 +229,6 @@ pageRule { app, appId, rule, startTime, time } = -- , span [] [ text "activate" ] -- ] -- ] - --viewActions rule = -- case rule of -- Success rule -> @@ -255,15 +247,14 @@ pageRule { app, appId, rule, startTime, time } = -- ] -- ] -- ] - -- _ -> -- ul [] [] - in div [] [ viewContextApps app , viewContextRules appId rule , actions + --, Container.view (section [ class "actions" ]) [ (viewActions rule) ] , Container.view (section [ class "highlight" ]) [ (viewWebData viewRule startTime time rule) ] ] @@ -285,6 +276,58 @@ pageRules { app, appId, rule, rules, startTime, time } = ] +pageUser : Model -> Html Msg +pageUser { app, appId, userUpdateForm, startTime, time, user } = + div [] + [ viewContextApps app + , viewContextUsers appId user + , Container.view (section [ class "highlight" ]) + [ (viewWebData viewUser startTime time user) + , formUser user userUpdateForm startTime time + ] + ] + + +pageUsers : Model -> Html Msg +pageUsers { app, appId, startTime, time, user, users, userSearchForm } = + let + viewItem = + (\user -> viewUserItem (Navigate (Route.User appId user.id)) user) + in + div [] + [ viewContextApps app + , viewContextUsers appId user + , Container.view (section [ class "highlight" ]) + [ form [ onSubmit UserSearchFormSubmit ] + [ formGroup + [ formElementText UserSearchFormBlur UserSearchFormFocus UserSearchFormUpdate userSearchForm "query" + , div [ class "action-group" ] + [ formButtonSubmit UserSearchFormSubmit "Search" + ] + ] + ] + , viewWebData (viewUserTable viewItem) startTime time users + ] + ] + + +viewAction : ( Msg, String, Maybe Int, String ) -> Html Msg +viewAction ( msg, icon, _, name ) = + li [] + [ a [ onClick msg ] + [ div [ class ("icon nc-icon-glyph " ++ icon) ] [] + , div [ class "name" ] [ text name ] + ] + ] + + +viewActions : List ( Msg, String, Maybe Int, String ) -> Html Msg +viewActions actions = + Container.view (section [ class "actions" ]) + [ ul [] (List.map viewAction actions) + ] + + viewContext : String -> Msg -> Html Msg -> Bool -> String -> Html Msg viewContext entities listMsg view selected icon = let @@ -335,6 +378,20 @@ viewContextRules appId rule = viewContext "Rules" (Navigate (Route.Rules appId)) viewRule False "education_book-39" +viewContextUsers : String -> WebData User -> Html Msg +viewContextUsers appId user = + let + ( selected, viewUser ) = + case user of + Success user -> + ( True, viewSelected (Navigate (Route.User appId user.id)) user.username ) + + _ -> + ( False, span [] [] ) + in + viewContext "Users" (Navigate (Route.Users appId)) viewUser selected "users_multiple-11" + + viewDebug : Model -> Html Msg viewDebug model = div [ class "debug" ] @@ -377,13 +434,10 @@ viewHeader { member, zone } = viewFooter : Model -> Html Msg viewFooter model = Container.view (footer []) + --[ viewDebug model ] [] - ---[ viewDebug model ] - - viewProfile : WebData Member -> Html Msg viewProfile member = case member of @@ -411,13 +465,38 @@ viewWebData : (a -> Html Msg) -> Time -> Time -> WebData a -> Html Msg viewWebData view startTime time data = case data of NotAsked -> - h3 [] [ text "Initialising" ] + div [] [] Loading -> Loader.view 64 (rgb 63 91 96) (Loader.nextStep startTime time) Failure err -> - h3 [] [ text ("Error: " ++ toString err) ] + case err of + BadStatus response -> + let + errors = + Decode.decodeString Error.decodeList response.body + in + case errors of + Ok errors -> + let + viewError err = + li [] + [ text err.message + , span [] + [ text " (" + , text (toString err.code) + , text ")" + ] + ] + in + ul [ class "errors api" ] (List.map viewError errors) + + Err err -> + span [ class "errors parse" ] [ text err ] + + _ -> + span [ class "errors network" ] [ text ("Error: " ++ toString err) ] Success data -> view data @@ -427,6 +506,19 @@ viewWebData view startTime time data = -- FORM +formUser : WebData User -> Form -> Time -> Time -> Html Msg +formUser user userUpdateForm startTime time = + form [ onSubmit UserUpdateFormSubmit ] + [ formGroup + [ formElementText UserUpdateFormBlur UserUpdateFormFocus UserUpdateFormUpdate userUpdateForm "username" + ] + , div [ class "action-group" ] + [ formButtonReset UserUpdateFormClear "Clear" + , formButtonSubmit UserUpdateFormSubmit "Update" + ] + ] + + formApp : WebData App -> Form -> Time -> Time -> Html Msg formApp new appForm startTime time = let diff --git a/cmd/console/styles/console.css b/cmd/console/styles/console.css index 1bb6ead..c6fec06 100644 --- a/cmd/console/styles/console.css +++ b/cmd/console/styles/console.css @@ -408,6 +408,12 @@ table.navigation tbody tr:hover { background: rgb(236, 240, 241); } +ul.errors li { + font-size: 2rem; + list-style: none; + text-transform: capitalize; +} + /*ul.actions { display: flex; justify-content: flex-end; diff --git a/core/user.go b/core/user.go index 557dca4..43e70e6 100644 --- a/core/user.go +++ b/core/user.go @@ -139,6 +139,32 @@ func UserFetch(users user.Service) UserFetchFunc { } } +type UserFetchConsoleFunc func(appID, userID uint64) (*user.User, error) + +func UserFetchConsole(apps app.Service, users user.Service) UserFetchConsoleFunc { + return func(appID, userID uint64) (*user.User, error) { + currentApp, err := AppFetch(apps)(appID) + if err != nil { + return nil, err + } + + us, err := users.Query(currentApp.Namespace(), user.QueryOptions{ + IDs: []uint64{ + userID, + }, + }) + if err != nil { + return nil, err + } + + if len(us) == 0 { + return nil, ErrNotFound + } + + return us[0], nil + } +} + // UserListByEmailsFunc returns all users for the given emails. type UserListByEmailsFunc func( currentApp *app.App, @@ -373,6 +399,36 @@ func UserRetrieve( } } +type UserSearchConsoleFunc func( + appID uint64, + query string, + limit int, + offset uint, +) (user.List, error) + +func UserSearchConsole( + apps app.Service, + users user.Service, +) UserSearchConsoleFunc { + return func(appID uint64, query string, limit int, offset uint) (user.List, error) { + currentApp, err := AppFetch(apps)(appID) + if err != nil { + return nil, err + } + + us, err := users.Search(currentApp.Namespace(), user.QueryOptions{ + Limit: limit, + Offset: offset, + Query: query, + }) + if err != nil { + return nil, err + } + + return us, nil + } +} + // UserSearchFunc returns all users for the given query. type UserSearchFunc func( currentApp *app.App, @@ -425,6 +481,29 @@ func UserSearch( } } +type UserUpdateConsoleFunc func(appID, userID uint64, username string) (*user.User, error) + +func UserUpdateConsole( + apps app.Service, + users user.Service, +) UserUpdateConsoleFunc { + return func(appID, userID uint64, username string) (*user.User, error) { + currentApp, err := AppFetch(apps)(appID) + if err != nil { + return nil, err + } + + u, err := UserFetchConsole(apps, users)(appID, userID) + if err != nil { + return nil, err + } + + u.Username = username + + return users.Put(currentApp.Namespace(), u) + } +} + // UserUpdateFunc stores the new attributes for the user. type UserUpdateFunc func( currentApp *app.App, diff --git a/handler/http/user.go b/handler/http/user.go index 729b705..54c24ab 100644 --- a/handler/http/user.go +++ b/handler/http/user.go @@ -182,6 +182,79 @@ func UserRetrieveMe(fn core.UserRetrieveFunc) Handler { } } +func UserFetchConsole(fn core.UserFetchConsoleFunc) Handler { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + appID, err := extractAppID(r) + if err != nil { + respondError(w, 0, wrapError(ErrBadRequest, err.Error())) + return + } + + userID, err := extractUserID(r) + if err != nil { + respondError(w, 0, wrapError(ErrBadRequest, err.Error())) + return + } + + u, err := fn(appID, userID) + if err != nil { + respondError(w, 0, err) + return + } + + respondJSON(w, http.StatusOK, &payloadUser{user: u}) + } +} + +// UserSearchConsole returns users matching the query. +func UserSearchConsole(fn core.UserSearchConsoleFunc) Handler { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + appID, err := extractAppID(r) + if err != nil { + respondError(w, 0, wrapError(ErrBadRequest, err.Error())) + return + } + + var ( + query = r.URL.Query().Get(keyUserQuery) + ) + + limit, err := extractLimit(r) + if err != nil { + respondError(w, 0, wrapError(ErrBadRequest, err.Error())) + return + } + + offset, err := extractOffsetCursorBefore(r) + if err != nil { + respondError(w, 0, wrapError(ErrBadRequest, err.Error())) + return + } + + us, err := fn(appID, query, limit, offset) + if err != nil { + respondError(w, 0, err) + return + } + + if len(us) == 0 { + respondJSON(w, http.StatusNoContent, nil) + return + } + + respondJSON(w, http.StatusOK, &payloadUsers{ + pagination: pagination( + r, + limit, + userSearchCursorAfter(us, limit, offset), + userSearchCursorBefore(us, limit, offset), + keyUserQuery, query, + ), + users: us, + }) + } +} + // UserSearch returns all users for the given search query. func UserSearch(fn core.UserSearchFunc) Handler { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { @@ -367,6 +440,38 @@ func UserSearchPlatform(fn core.UserListByPlatformIDsFunc) Handler { } } +func UserUpdateConsole(fn core.UserUpdateConsoleFunc) Handler { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + appID, err := extractAppID(r) + if err != nil { + respondError(w, 0, wrapError(ErrBadRequest, err.Error())) + return + } + + userID, err := extractUserID(r) + if err != nil { + respondError(w, 0, wrapError(ErrBadRequest, err.Error())) + return + } + + p := payloadUser{} + + err = json.NewDecoder(r.Body).Decode(&p) + if err != nil { + respondError(w, 0, wrapError(ErrBadRequest, err.Error())) + return + } + + u, err := fn(appID, userID, p.user.Username) + if err != nil { + respondError(w, 0, err) + return + } + + respondJSON(w, http.StatusOK, &payloadUser{user: u}) + } +} + // UserUpdate stores the new attributes given. func UserUpdate(fn core.UserUpdateFunc) Handler { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { @@ -455,6 +560,7 @@ func (p *payloadUser) MarshalJSON() ([]byte, error) { About string `json:"about"` CustomID string `json:"custom_id,omitempty"` Email string `json:"email"` + Enabled bool `json:"enabled"` Firstname string `json:"first_name"` FollowerCount int `json:"follower_count"` FollowingCount int `json:"followed_count"` @@ -478,6 +584,7 @@ func (p *payloadUser) MarshalJSON() ([]byte, error) { About: p.user.About, CustomID: p.user.CustomID, Email: p.user.Email, + Enabled: p.user.Enabled, Firstname: p.user.Firstname, FollowerCount: p.user.FollowerCount, FollowingCount: p.user.FollowingCount,