Profile API (#151)

* Profile retrieval

* Saving avatar (without propagating it)

* Saving display name (without propagating it)

* Getters for display name and avatar URL

* Doc'd

* Remove unused import

* Applied requested changes

* Added auth on PUT /profile/{userID}/...

* Improved error handling/reporting

* Using utils log reporting

* Removed useless checks
This commit is contained in:
Brendan Abolivier 2017-07-10 14:52:41 +01:00 committed by Mark Haines
parent 69c29172c3
commit 1efbad8119
6 changed files with 341 additions and 16 deletions

View file

@ -23,6 +23,7 @@ type Account struct {
UserID string
Localpart string
ServerName gomatrixserverlib.ServerName
Profile *Profile
// TODO: Other flags like IsAdmin, IsGuest
// TODO: Devices
// TODO: Associations (e.g. with application services)

View file

@ -0,0 +1,22 @@
// Copyright 2017 Vector Creations Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package authtypes
// Profile represents the profile for a Matrix account on this home server.
type Profile struct {
Localpart string
DisplayName string
AvatarURL string
}

View file

@ -0,0 +1,93 @@
// Copyright 2017 Vector Creations Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accounts
import (
"database/sql"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
)
const profilesSchema = `
-- Stores data about accounts profiles.
CREATE TABLE IF NOT EXISTS profiles (
-- The Matrix user ID localpart for this account
localpart TEXT NOT NULL PRIMARY KEY,
-- The display name for this account
display_name TEXT,
-- The URL of the avatar for this account
avatar_url TEXT
);
`
const insertProfileSQL = "" +
"INSERT INTO profiles(localpart, display_name, avatar_url) VALUES ($1, $2, $3)"
const selectProfileByLocalpartSQL = "" +
"SELECT localpart, display_name, avatar_url FROM profiles WHERE localpart = $1"
const setAvatarURLSQL = "" +
"UPDATE profiles SET avatar_url = $1 WHERE localpart = $2"
const setDisplayNameSQL = "" +
"UPDATE profiles SET display_name = $1 WHERE localpart = $2"
type profilesStatements struct {
insertProfileStmt *sql.Stmt
selectProfileByLocalpartStmt *sql.Stmt
setAvatarURLStmt *sql.Stmt
setDisplayNameStmt *sql.Stmt
}
func (s *profilesStatements) prepare(db *sql.DB) (err error) {
_, err = db.Exec(profilesSchema)
if err != nil {
return
}
if s.insertProfileStmt, err = db.Prepare(insertProfileSQL); err != nil {
return
}
if s.selectProfileByLocalpartStmt, err = db.Prepare(selectProfileByLocalpartSQL); err != nil {
return
}
if s.setAvatarURLStmt, err = db.Prepare(setAvatarURLSQL); err != nil {
return
}
if s.setDisplayNameStmt, err = db.Prepare(setDisplayNameSQL); err != nil {
return
}
return
}
func (s *profilesStatements) insertProfile(localpart string) (err error) {
_, err = s.insertProfileStmt.Exec(localpart, "", "")
return
}
func (s *profilesStatements) selectProfileByLocalpart(localpart string) (*authtypes.Profile, error) {
var profile authtypes.Profile
err := s.selectProfileByLocalpartStmt.QueryRow(localpart).Scan(&profile.Localpart, &profile.DisplayName, &profile.AvatarURL)
return &profile, err
}
func (s *profilesStatements) setAvatarURL(localpart string, avatarURL string) (err error) {
_, err = s.setAvatarURLStmt.Exec(avatarURL, localpart)
return
}
func (s *profilesStatements) setDisplayName(localpart string, displayName string) (err error) {
_, err = s.setDisplayNameStmt.Exec(displayName, localpart)
return
}

View file

@ -28,9 +28,10 @@ import (
type Database struct {
db *sql.DB
accounts accountsStatements
profiles profilesStatements
}
// NewDatabase creates a new accounts database
// NewDatabase creates a new accounts and profiles database
func NewDatabase(dataSourceName string, serverName gomatrixserverlib.ServerName) (*Database, error) {
var db *sql.DB
var err error
@ -41,7 +42,11 @@ func NewDatabase(dataSourceName string, serverName gomatrixserverlib.ServerName)
if err = a.prepare(db, serverName); err != nil {
return nil, err
}
return &Database{db, a}, nil
p := profilesStatements{}
if err = p.prepare(db); err != nil {
return nil, err
}
return &Database{db, a, p}, nil
}
// GetAccountByPassword returns the account associated with the given localpart and password.
@ -57,13 +62,34 @@ func (d *Database) GetAccountByPassword(localpart, plaintextPassword string) (*a
return d.accounts.selectAccountByLocalpart(localpart)
}
// CreateAccount makes a new account with the given login name and password. If no password is supplied,
// the account will be a passwordless account.
// GetProfileByLocalpart returns the profile associated with the given localpart.
// Returns sql.ErrNoRows if no profile exists which matches the given localpart.
func (d *Database) GetProfileByLocalpart(localpart string) (*authtypes.Profile, error) {
return d.profiles.selectProfileByLocalpart(localpart)
}
// SetAvatarURL updates the avatar URL of the profile associated with the given
// localpart. Returns an error if something went wrong with the SQL query
func (d *Database) SetAvatarURL(localpart string, avatarURL string) error {
return d.profiles.setAvatarURL(localpart, avatarURL)
}
// SetDisplayName updates the display name of the profile associated with the given
// localpart. Returns an error if something went wrong with the SQL query
func (d *Database) SetDisplayName(localpart string, displayName string) error {
return d.profiles.setDisplayName(localpart, displayName)
}
// CreateAccount makes a new account with the given login name and password, and creates an empty profile
// for this account. If no password is supplied, the account will be a passwordless account.
func (d *Database) CreateAccount(localpart, plaintextPassword string) (*authtypes.Account, error) {
hash, err := hashPassword(plaintextPassword)
if err != nil {
return nil, err
}
if err := d.profiles.insertProfile(localpart); err != nil {
return nil, err
}
return d.accounts.insertAccount(localpart, hash)
}

View file

@ -0,0 +1,161 @@
// Copyright 2017 Vector Creations Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readers
import (
"fmt"
"net/http"
"strings"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/util"
)
type profileResponse struct {
AvatarURL string `json:"avatar_url"`
DisplayName string `json:"displayname"`
}
type avatarURL struct {
AvatarURL string `json:"avatar_url"`
}
type displayName struct {
DisplayName string `json:"displayname"`
}
// GetProfile implements GET /profile/{userID}
func GetProfile(
req *http.Request, accountDB *accounts.Database, userID string,
) util.JSONResponse {
if req.Method != "GET" {
return util.JSONResponse{
Code: 405,
JSON: jsonerror.NotFound("Bad method"),
}
}
localpart := getLocalPart(userID)
profile, err := accountDB.GetProfileByLocalpart(localpart)
if err != nil {
return httputil.LogThenError(req, err)
}
res := profileResponse{
AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName,
}
return util.JSONResponse{
Code: 200,
JSON: res,
}
}
// GetAvatarURL implements GET /profile/{userID}/avatar_url
func GetAvatarURL(
req *http.Request, accountDB *accounts.Database, userID string,
) util.JSONResponse {
localpart := getLocalPart(userID)
profile, err := accountDB.GetProfileByLocalpart(localpart)
if err != nil {
return httputil.LogThenError(req, err)
}
res := avatarURL{
AvatarURL: profile.AvatarURL,
}
return util.JSONResponse{
Code: 200,
JSON: res,
}
}
// SetAvatarURL implements PUT /profile/{userID}/avatar_url
func SetAvatarURL(
req *http.Request, accountDB *accounts.Database, userID string,
) util.JSONResponse {
var r avatarURL
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
return *resErr
}
if r.AvatarURL == "" {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.BadJSON("'avatar_url' must be supplied."),
}
}
localpart := getLocalPart(userID)
if err := accountDB.SetAvatarURL(localpart, r.AvatarURL); err != nil {
return httputil.LogThenError(req, err)
}
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}
// GetDisplayName implements GET /profile/{userID}/displayname
func GetDisplayName(
req *http.Request, accountDB *accounts.Database, userID string,
) util.JSONResponse {
localpart := getLocalPart(userID)
profile, err := accountDB.GetProfileByLocalpart(localpart)
if err != nil {
return httputil.LogThenError(req, err)
}
res := displayName{
DisplayName: profile.DisplayName,
}
return util.JSONResponse{
Code: 200,
JSON: res,
}
}
// SetDisplayName implements PUT /profile/{userID}/displayname
func SetDisplayName(
req *http.Request, accountDB *accounts.Database, userID string,
) util.JSONResponse {
var r displayName
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
return *resErr
}
if r.DisplayName == "" {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.BadJSON("'displayname' must be supplied."),
}
}
localpart := getLocalPart(userID)
if err := accountDB.SetDisplayName(localpart, r.DisplayName); err != nil {
return httputil.LogThenError(req, err)
}
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}
func getLocalPart(userID string) string {
if !strings.HasPrefix(userID, "@") {
panic(fmt.Errorf("Invalid user ID"))
}
// Get the part before ":"
username := strings.Split(userID, ":")[0]
// Return the part after the "@"
return strings.Split(username, "@")[1]
}

View file

@ -163,14 +163,43 @@ func Setup(
r0mux.Handle("/profile/{userID}",
common.MakeAPI("profile", func(req *http.Request) util.JSONResponse {
// TODO: Get profile data for user ID
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
vars := mux.Vars(req)
return readers.GetProfile(req, accountDB, vars["userID"])
}),
)
r0mux.Handle("/profile/{userID}/avatar_url",
common.MakeAPI("profile_avatar_url", func(req *http.Request) util.JSONResponse {
vars := mux.Vars(req)
return readers.GetAvatarURL(req, accountDB, vars["userID"])
}),
).Methods("GET")
r0mux.Handle("/profile/{userID}/avatar_url",
common.MakeAuthAPI("profile_avatar_url", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars := mux.Vars(req)
return readers.SetAvatarURL(req, accountDB, vars["userID"])
}),
).Methods("PUT", "OPTIONS")
// Browsers use the OPTIONS HTTP method to check if the CORS policy allows
// PUT requests, so we need to allow this method
r0mux.Handle("/profile/{userID}/displayname",
common.MakeAPI("profile_displayname", func(req *http.Request) util.JSONResponse {
vars := mux.Vars(req)
return readers.GetDisplayName(req, accountDB, vars["userID"])
}),
).Methods("GET")
r0mux.Handle("/profile/{userID}/displayname",
common.MakeAuthAPI("profile_displayname", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars := mux.Vars(req)
return readers.SetDisplayName(req, accountDB, vars["userID"])
}),
).Methods("PUT", "OPTIONS")
// Browsers use the OPTIONS HTTP method to check if the CORS policy allows
// PUT requests, so we need to allow this method
r0mux.Handle("/account/3pid",
common.MakeAPI("account_3pid", func(req *http.Request) util.JSONResponse {
// TODO: Get 3pid data for user ID
@ -237,13 +266,6 @@ func Setup(
}),
)
r0mux.Handle("/profile/{userID}/displayname",
common.MakeAPI("profile_displayname", func(req *http.Request) util.JSONResponse {
// TODO: Set and get the displayname
return util.JSONResponse{Code: 200, JSON: struct{}{}}
}),
)
r0mux.Handle("/user/{userID}/account_data/{type}",
common.MakeAPI("user_account_data", func(req *http.Request) util.JSONResponse {
// TODO: Set and get the account_data