mirror of
https://github.com/1f349/dendrite.git
synced 2024-11-22 11:41:38 +00:00
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:
parent
69c29172c3
commit
1efbad8119
@ -23,6 +23,7 @@ type Account struct {
|
|||||||
UserID string
|
UserID string
|
||||||
Localpart string
|
Localpart string
|
||||||
ServerName gomatrixserverlib.ServerName
|
ServerName gomatrixserverlib.ServerName
|
||||||
|
Profile *Profile
|
||||||
// TODO: Other flags like IsAdmin, IsGuest
|
// TODO: Other flags like IsAdmin, IsGuest
|
||||||
// TODO: Devices
|
// TODO: Devices
|
||||||
// TODO: Associations (e.g. with application services)
|
// TODO: Associations (e.g. with application services)
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -28,9 +28,10 @@ import (
|
|||||||
type Database struct {
|
type Database struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
accounts accountsStatements
|
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) {
|
func NewDatabase(dataSourceName string, serverName gomatrixserverlib.ServerName) (*Database, error) {
|
||||||
var db *sql.DB
|
var db *sql.DB
|
||||||
var err error
|
var err error
|
||||||
@ -41,7 +42,11 @@ func NewDatabase(dataSourceName string, serverName gomatrixserverlib.ServerName)
|
|||||||
if err = a.prepare(db, serverName); err != nil {
|
if err = a.prepare(db, serverName); err != nil {
|
||||||
return nil, err
|
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.
|
// 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)
|
return d.accounts.selectAccountByLocalpart(localpart)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAccount makes a new account with the given login name and password. If no password is supplied,
|
// GetProfileByLocalpart returns the profile associated with the given localpart.
|
||||||
// the account will be a passwordless account.
|
// 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) {
|
func (d *Database) CreateAccount(localpart, plaintextPassword string) (*authtypes.Account, error) {
|
||||||
hash, err := hashPassword(plaintextPassword)
|
hash, err := hashPassword(plaintextPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := d.profiles.insertProfile(localpart); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return d.accounts.insertAccount(localpart, hash)
|
return d.accounts.insertAccount(localpart, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
161
src/github.com/matrix-org/dendrite/clientapi/readers/profile.go
Normal file
161
src/github.com/matrix-org/dendrite/clientapi/readers/profile.go
Normal 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]
|
||||||
|
}
|
@ -163,14 +163,43 @@ func Setup(
|
|||||||
|
|
||||||
r0mux.Handle("/profile/{userID}",
|
r0mux.Handle("/profile/{userID}",
|
||||||
common.MakeAPI("profile", func(req *http.Request) util.JSONResponse {
|
common.MakeAPI("profile", func(req *http.Request) util.JSONResponse {
|
||||||
// TODO: Get profile data for user ID
|
vars := mux.Vars(req)
|
||||||
return util.JSONResponse{
|
return readers.GetProfile(req, accountDB, vars["userID"])
|
||||||
Code: 200,
|
|
||||||
JSON: struct{}{},
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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",
|
r0mux.Handle("/account/3pid",
|
||||||
common.MakeAPI("account_3pid", func(req *http.Request) util.JSONResponse {
|
common.MakeAPI("account_3pid", func(req *http.Request) util.JSONResponse {
|
||||||
// TODO: Get 3pid data for user ID
|
// 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}",
|
r0mux.Handle("/user/{userID}/account_data/{type}",
|
||||||
common.MakeAPI("user_account_data", func(req *http.Request) util.JSONResponse {
|
common.MakeAPI("user_account_data", func(req *http.Request) util.JSONResponse {
|
||||||
// TODO: Set and get the account_data
|
// TODO: Set and get the account_data
|
||||||
|
Loading…
Reference in New Issue
Block a user