diff --git a/docs/API.md b/docs/API.md index 524e8077..1201f737 100644 --- a/docs/API.md +++ b/docs/API.md @@ -39,21 +39,6 @@ This returns: Endpoints ========= -`/v1/account_details` ----------------- - -This endpoint fetches account details and returns them as JSON. The request is a JSON object with fields: - -* `accountName`: string, name of the account - -The response is a JSON object with fields: - -* `success`: whether the account exists or not -* `accountName`: canonical, case-unfolded version of the account name -* `email`: email address of the account provided -* `registeredAt`: string, registration date/time of the account (in ISO8601 format) -* `channels`: array of strings, list of channels the account is registered on or associated with - `/v1/check_auth` ---------------- @@ -67,16 +52,53 @@ The response is a JSON object with fields: * `success`: whether the credentials provided were valid * `accountName`: canonical, case-unfolded version of the account name -`/v1/rehash` ------------- +`/v1/ns/info` +------------- -This endpoint rehashes the server (i.e. reloads the configuration file, TLS certificates, and other associated data). The body is ignored. The response is a JSON object with fields: +This endpoint fetches account details and returns them as JSON. The request is a JSON object with fields: -* `success`: boolean, indicates whether the rehash was successful -* `error`: string, optional, human-readable description of the failure +* `accountName`: string, name of the account -`/v1/saregister` ----------------- +The response is a JSON object with fields: + +* `success`: whether the account exists or not +* `accountName`: canonical, case-unfolded version of the account name +* `email`: email address of the account provided +* `registeredAt`: string, registration date/time of the account (in ISO8601 format) +* `channels`: array of strings, list of channels the account is registered on or associated with + +Note: this endpoint was previously named `/v1/account_details`. The old name is still accepted for backwards compatibility. + +`/v1/ns/list` +------------- + +This endpoint fetches a list of all accounts. The request body is ignored and can be empty. + +The response is a JSON object with fields: + +* `success`: whether the request succeeded +* `accounts`: array of objects, each with fields: + * `success`: boolean, whether this individual account query succeeded + * `accountName`: string, canonical, case-unfolded version of the account name +* `totalCount`: integer, total number of accounts returned + +Note: this endpoint was previously named `/v1/account_list`. The old name is still accepted for backwards compatibility. + +`/v1/ns/passwd` +--------------- + +This endpoint changes the password of an existing NickServ account. The request is a JSON object with fields: + +* `accountName`: string, name of the account +* `passphrase`: string, new passphrase for the account + +The response is a JSON object with fields: + +* `success`: whether the password change succeeded +* `errorCode`: string, optional, machine-readable description of the error. Possible values include: `ACCOUNT_DOES_NOT_EXIST`, `INVALID_PASSPHRASE`, `CREDENTIALS_EXTERNALLY_MANAGED`, `UNKNOWN_ERROR`. + +`/v1/ns/saregister` +------------------- This endpoint registers an account in NickServ, with the same semantics as `NS SAREGISTER`. The request is a JSON object with fields: @@ -89,22 +111,18 @@ The response is a JSON object with fields: * `errorCode`: string, optional, machine-readable description of the error. Possible values include: `ACCOUNT_EXISTS`, `INVALID_PASSPHRASE`, `UNKNOWN_ERROR`. * `error`: string, optional, human-readable description of the failure. -`/v1/account_list` -------------------- +Note: this endpoint was previously named `/v1/saregister`. The old name is still accepted for backwards compatibility. -This endpoint fetches a list of all accounts. The request body is ignored and can be empty. +`/v1/rehash` +------------ -The response is a JSON object with fields: - -* `success`: whether the request succeeded -* `accounts`: array of objects, each with fields: - * `success`: boolean, whether this individual account query succeeded - * `accountName`: string, canonical, case-unfolded version of the account name -* `totalCount`: integer, total number of accounts returned +This endpoint rehashes the server (i.e. reloads the configuration file, TLS certificates, and other associated data). The body is ignored. The response is a JSON object with fields: +* `success`: boolean, indicates whether the rehash was successful +* `error`: string, optional, human-readable description of the failure `/v1/status` -------------- +------------ This endpoint returns status information about the running Ergo server. The request body is ignored and can be empty. diff --git a/irc/api.go b/irc/api.go index cb313cbc..ca31f99e 100644 --- a/irc/api.go +++ b/irc/api.go @@ -17,12 +17,23 @@ func newAPIHandler(server *Server) http.Handler { mux: http.NewServeMux(), } + // server-level functionality: api.mux.HandleFunc("POST /v1/rehash", api.handleRehash) + api.mux.HandleFunc("POST /v1/status", api.handleStatus) + + // use Ergo as a source of truth for authentication in other services: api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth) + + // legacy names for /v1/ns endpoints: api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister) api.mux.HandleFunc("POST /v1/account_details", api.handleAccountDetails) api.mux.HandleFunc("POST /v1/account_list", api.handleAccountList) - api.mux.HandleFunc("POST /v1/status", api.handleStatus) + + // /v1/ns: nickserv functionality + api.mux.HandleFunc("POST /v1/ns/info", api.handleAccountDetails) + api.mux.HandleFunc("POST /v1/ns/list", api.handleAccountList) + api.mux.HandleFunc("POST /v1/ns/passwd", api.handleNsPasswd) + api.mux.HandleFunc("POST /v1/ns/saregister", api.handleSaregister) return api } @@ -174,6 +185,31 @@ func (a *ergoAPI) handleSaregister(w http.ResponseWriter, r *http.Request) { a.writeJSONResponse(response, w, r) } +func (a *ergoAPI) handleNsPasswd(w http.ResponseWriter, r *http.Request) { + var request apiSaregisterRequest + if err := a.decodeJSONRequest(&request, w, r); err != nil { + return + } + + var response apiGenericResponse + err := a.server.accounts.setPassword(request.AccountName, request.Passphrase, true) + switch err { + case nil: + response.Success = true + case errAccountDoesNotExist: + response.ErrorCode = "ACCOUNT_DOES_NOT_EXIST" + case errAccountBadPassphrase, errEmptyCredentials: + response.ErrorCode = "INVALID_PASSPHRASE" + case errCredsExternallyManaged: + response.ErrorCode = "CREDENTIALS_EXTERNALLY_MANAGED" + default: + a.server.logger.Error("api", "could not change user password:", err.Error()) + response.ErrorCode = "UNKNOWN_ERROR" + } + + a.writeJSONResponse(response, w, r) +} + type apiAccountDetailsResponse struct { apiGenericResponse AccountName string `json:"accountName,omitempty"` @@ -244,26 +280,24 @@ func (a *ergoAPI) handleAccountList(w http.ResponseWriter, r *http.Request) { response.TotalCount = len(accounts) // Load account details - response.Accounts = make([]apiAccountDetailsResponse, len(accounts)) - for i, account := range accounts { + response.Accounts = make([]apiAccountDetailsResponse, 0, len(accounts)) + for _, account := range accounts { accountData, err := a.server.accounts.LoadAccount(account) if err != nil { - response.Accounts[i] = apiAccountDetailsResponse{ - apiGenericResponse: apiGenericResponse{ - Success: false, - Error: err.Error(), - }, - } + // shouldn't happen continue } - response.Accounts[i] = apiAccountDetailsResponse{ - apiGenericResponse: apiGenericResponse{ - Success: true, + response.Accounts = append( + response.Accounts, + apiAccountDetailsResponse{ + apiGenericResponse: apiGenericResponse{ + Success: true, + }, + AccountName: accountData.Name, + Email: accountData.Settings.Email, }, - AccountName: accountData.Name, - Email: accountData.Settings.Email, - } + ) } response.Success = true