[server] UserID+email instead of username

This commit is contained in:
Abhishek Shroff
2025-05-14 11:04:36 +05:30
parent adcfd303f5
commit 038adab527
33 changed files with 147 additions and 135 deletions

View File

@@ -57,8 +57,8 @@ func extractUserDetails(c *gin.Context) (user.User, error) {
}
return user.User{}, errAuthRequired
} else if auth, ok := checkAuthHeader(header, "basic"); ok {
if username, password, ok := decodeBasicAuth(auth); ok {
if u, err := userManager.VerifyUserPassword(username, password); err == nil {
if email, password, ok := decodeBasicAuth(auth); ok {
if u, err := userManager.VerifyUserPassword(email, password); err == nil {
return u, nil
} else {
return user.User{}, err
@@ -82,15 +82,15 @@ func checkAuthHeader(header, prefix string) (string, bool) {
return header[len(prefix):], true
}
func decodeBasicAuth(auth string) (username, password string, ok bool) {
func decodeBasicAuth(auth string) (email, password string, ok bool) {
c, err := base64.StdEncoding.DecodeString(auth)
if err != nil {
return "", "", false
}
cs := string(c)
username, password, ok = strings.Cut(cs, ":")
email, password, ok = strings.Cut(cs, ":")
if !ok {
return "", "", false
}
return username, password, true
return email, password, true
}

View File

@@ -11,7 +11,7 @@ import (
)
type passwordParams struct {
Username string `json:"username" form:"username" binding:"required"`
Email string `json:"email" form:"email" binding:"required"`
Password string `json:"password" form:"password" binding:"required"`
}
@@ -20,9 +20,9 @@ type loginResponse struct {
User loggedInUserResponse `json:"user"`
}
type loggedInUserResponse struct {
Username string `json:"username"`
Display string `json:"display"`
Home pgtype.UUID `json:"home"`
Email string `json:"email"`
Display string `json:"display"`
Home pgtype.UUID `json:"home"`
}
func SetupRoutes(r *gin.RouterGroup) {
@@ -35,22 +35,22 @@ func handlePasswordAuth(c *gin.Context) {
var params passwordParams
err := c.ShouldBind(&params)
if err != nil {
panic(errors.NewError(http.StatusBadRequest, "missing_params", "Username or password not specified"))
panic(errors.NewError(http.StatusBadRequest, "missing_params", "Email or password not specified"))
}
userManager := user.ManagerFromContext(c.Request.Context())
if user, err := userManager.VerifyUserPassword(params.Username, params.Password); err != nil {
if user, err := userManager.VerifyUserPassword(params.Email, params.Password); err != nil {
panic(err)
} else {
if token, err := userManager.CreateAccessToken(user.Username); err != nil {
if token, err := userManager.CreateAccessToken(user.Email); err != nil {
panic(err)
} else {
c.JSON(200, loginResponse{
AccessToken: token,
User: loggedInUserResponse{
Username: user.Username,
Display: user.DisplayName,
Home: user.Home,
Email: user.Email,
Display: user.DisplayName,
Home: user.Home,
},
})
}

View File

@@ -5,11 +5,12 @@ import (
"github.com/shroff/phylum/server/internal/api/authenticator"
"github.com/shroff/phylum/server/internal/api/v1/responses"
"github.com/shroff/phylum/server/internal/core/fs"
"github.com/shroff/phylum/server/internal/core/user"
)
type shareParams struct {
Path string `json:"path" form:"path" binding:"required"`
Username string `json:"username" form:"username" binding:"required"`
Email string `json:"email" form:"email" binding:"required"`
Permissions fs.Permission `json:"permission" form:"permissions" binding:"required"`
}
@@ -29,7 +30,12 @@ func handleShareRequest(c *gin.Context) {
panic(err)
}
r, err = r.UpdatePermissions(params.Username, params.Permissions)
user, err := user.ManagerFromContext(c).UserByEmail(params.Email)
if err != nil {
panic(err)
}
r, err = r.UpdatePermissions(user.ID, params.Permissions)
if err != nil {
panic(err)
}

View File

@@ -37,7 +37,7 @@ func handleHomeRoute(c *gin.Context) {
c.JSON(200, homeResponse{
User: responses.User{
Username: user.Username,
Email: user.Email,
DisplayName: user.DisplayName,
},
Home: responses.FullResourceFromFS(r),

View File

@@ -37,7 +37,7 @@ type ResourceFull struct {
}
type User struct {
Username string `json:"username"`
Email string `json:"email"`
DisplayName string `json:"display"`
}

View File

@@ -32,7 +32,7 @@ func handleUsersListRoute(c *gin.Context) {
result := make([]responses.User, len(users))
for i, u := range users {
result[i] = responses.User{
Username: u.Username,
Email: u.Email,
DisplayName: u.DisplayName,
}
}

View File

@@ -42,10 +42,10 @@ func SetupHandler(r *gin.RouterGroup) {
func (h *handler) HandleRequest(c *gin.Context) {
authSuccess := false
var f fs.FileSystem
if username, pass, ok := c.Request.BasicAuth(); ok {
if email, pass, ok := c.Request.BasicAuth(); ok {
ctx := c.Request.Context()
userManager := user.ManagerFromContext(ctx)
if u, err := userManager.VerifyUserPassword(username, pass); err == nil {
if u, err := userManager.VerifyUserPassword(email, pass); err == nil {
authSuccess = true
root := c.Param("root")
f = u.OpenFileSystem(ctx)

View File

@@ -25,7 +25,7 @@ func User(cmd *cobra.Command) *user.User {
} else if value == "" {
u = nil
} else {
if user, err := user.ManagerFromContext(context.Background()).UserByUsername(value); err != nil {
if user, err := user.ManagerFromContext(context.Background()).UserByEmail(value); err != nil {
fmt.Println("could not find user '" + value + "': " + err.Error())
os.Exit(1)
} else {

View File

@@ -14,7 +14,7 @@ import (
func setupSetfaclCommand() *cobra.Command {
cmd := cobra.Command{
Use: "setfacl <path> <user> (none|read|write|share)",
Use: "setfacl <path> <user-email> (none|read|write|share)",
Short: "Access Control",
Args: cobra.ExactArgs(3),
Run: func(cmd *cobra.Command, args []string) {
@@ -25,23 +25,23 @@ func setupSetfaclCommand() *cobra.Command {
fmt.Println("cannot update permissions for '" + path + "': " + err.Error())
os.Exit(1)
}
username := args[1]
if _, err := user.ManagerFromContext(context.Background()).UserByUsername(username); err != nil {
fmt.Println("cannot update permissions for user '" + username + "': " + err.Error())
os.Exit(1)
}
permission, err := parsePermissionString(args[2])
if err != nil {
fmt.Println("cannot update permissions for '" + path + "': " + err.Error())
os.Exit(1)
}
if _, err := r.UpdatePermissions(username, permission); err != nil {
fmt.Println("cannot update permissions for '" + path + "': " + err.Error())
email := args[1]
if user, err := user.ManagerFromContext(context.Background()).UserByEmail(email); err != nil {
fmt.Println("cannot update permissions for user '" + email + "': " + err.Error())
os.Exit(1)
} else {
if _, err := r.UpdatePermissions(user.ID, permission); err != nil {
fmt.Println("cannot update permissions for '" + path + "': " + err.Error())
os.Exit(1)
}
}
},
}
return &cmd

View File

@@ -19,18 +19,18 @@ import (
func setupUserAddCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "add username",
Use: "add email",
Short: "Add User",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
username := args[0]
email := args[0]
displayName := username
displayName := email
if n, err := cmd.Flags().GetString("name"); err != nil {
fmt.Println("invalid value for flag 'name': " + err.Error())
os.Exit(1)
} else if n != "" {
displayName = username
displayName = email
}
password, err := cmd.Flags().GetString("password")
@@ -67,7 +67,7 @@ func setupUserAddCommand() *cobra.Command {
homePath := ""
if !noCreateHome {
basePath, _ := cmd.Flags().GetString("base-dir")
homePath = strings.TrimRight(basePath, "/") + "/" + username
homePath = strings.TrimRight(basePath, "/") + "/" + email
}
err = db.Get(context.Background()).RunInTx(func(db db.Handler) error {
@@ -90,14 +90,14 @@ func setupUserAddCommand() *cobra.Command {
Valid: true,
}
}
if user, err := userManager.CreateUser(username, displayName, password, homeID); err != nil {
if user, err := userManager.CreateUser(email, displayName, password, homeID); err != nil {
return err
} else {
u = user
}
if homeID.Valid {
if _, err := home.UpdatePermissions(u.Username, fs.PermissionRead|fs.PermissionWrite|fs.PermissionShare); err != nil {
if _, err := home.UpdatePermissions(u.ID, fs.PermissionRead|fs.PermissionWrite|fs.PermissionShare); err != nil {
return err
}
}

View File

@@ -40,7 +40,7 @@ func setupUserListCommand() *cobra.Command {
logrus.Fatal(err)
}
for _, user := range users {
logrus.Infof("%24s : %s", user.Username, user.DisplayName)
logrus.Infof("%24s : %s", user.Email, user.DisplayName)
}
},
}

View File

@@ -14,14 +14,14 @@ import (
func setupUserModCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "mod username",
Use: "mod email",
Short: "Add User",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
username := args[0]
u, err := user.ManagerFromContext(context.Background()).UserByUsername(username)
email := args[0]
u, err := user.ManagerFromContext(context.Background()).UserByEmail(email)
if err != nil {
fmt.Println("could not update user '" + username + "': " + err.Error())
fmt.Println("could not update user '" + email + "': " + err.Error())
os.Exit(1)
}
@@ -57,7 +57,7 @@ func setupUserModCommand() *cobra.Command {
return nil
})
if err != nil {
fmt.Println("could not update user '" + username + "': " + err.Error())
fmt.Println("could not update user '" + email + "': " + err.Error())
os.Exit(3)
}
},

View File

@@ -13,12 +13,12 @@ import (
func setupUserPasswdCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "passwd username",
Use: "passwd email",
Short: "Change User Password",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
username := args[0]
u, err := user.ManagerFromContext(context.Background()).UserByUsername(username)
email := args[0]
u, err := user.ManagerFromContext(context.Background()).UserByEmail(email)
if err != nil {
fmt.Println("unable to change password: " + err.Error())
os.Exit(1)

View File

@@ -1,8 +1,9 @@
CREATE TABLE users(
username TEXT PRIMARY KEY,
id SERIAL PRIMARY KEY,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted TIMESTAMP,
email TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
password_hash TEXT NOT NULL,
home uuid REFERENCES resources(id),

View File

@@ -2,7 +2,7 @@ CREATE TABLE access_tokens(
id TEXT NOT NULL PRIMARY KEY,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires TIMESTAMP NOT NULL,
username TEXT NOT NULL REFERENCES users(username) ON UPDATE CASCADE ON DELETE CASCADE
user_id TEXT NOT NULL REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE
);
---- create above / drop below ----

View File

@@ -1,15 +1,15 @@
CREATE TABLE bookmarks(
username TEXT NOT NULL REFERENCES users(username),
user_id TEXT NOT NULL REFERENCES users(id),
resource_id UUID NOT NULL REFERENCES resources(id),
name TEXT NOT NULL,
dir BOOLEAN NOT NULL,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted TIMESTAMP,
PRIMARY KEY(username, resource_id)
PRIMARY KEY(user_id, resource_id)
);
CREATE INDEX modified_bookmarks ON bookmarks(username, modified);
CREATE INDEX modified_bookmarks ON bookmarks(user_id, modified);
---- create above / drop below ----

View File

@@ -24,11 +24,11 @@ func (f filesystem) scanResourceAncestor(row pgx.CollectableRow) (ResourceAncest
}
const ancestorsQuery = `WITH RECURSIVE nodes(id, name, parent, userPermission) AS (
SELECT r.id, r.name, r.parent, COALESCE(r.permissions[$2::TEXT]::INTEGER, 0) as userPermission
SELECT r.id, r.name, r.parent, COALESCE(r.permissions[$2::INT]::INT, 0) as userPermission
FROM resources r
WHERE r.id = $1::UUID
UNION ALL
SELECT r.id, r.name, r.parent, COALESCE(r.permissions[$2::TEXT]::INTEGER, 0) as userPermission
SELECT r.id, r.name, r.parent, COALESCE(r.permissions[$2::INT]::INT, 0) as userPermission
FROM resources r
JOIN nodes n ON r.id = n.parent
WHERE userPermission <> 0
@@ -37,7 +37,7 @@ const ancestorsQuery = `WITH RECURSIVE nodes(id, name, parent, userPermission) A
SELECT id, name, userPermission FROM nodes`
func (r Resource) GetAncestors() ([]ResourceAncestor, error) {
if rows, err := r.f.db.Query(ancestorsQuery, r.id, r.f.username, r.f.fullAccess); err != nil {
if rows, err := r.f.db.Query(ancestorsQuery, r.id, r.f.userID, r.f.fullAccess); err != nil {
return nil, err
} else if a, err := pgx.CollectRows(rows, r.f.scanResourceAncestor); err != nil {
return nil, err

View File

@@ -10,7 +10,7 @@ type filesystem struct {
db db.Handler
cs storage.Storage
pathRoot pgtype.UUID
username string
userID int32
fullAccess bool
}
@@ -23,7 +23,7 @@ func (f filesystem) withDb(db db.Handler) filesystem {
db: db,
cs: f.cs,
pathRoot: f.pathRoot,
username: f.username,
userID: f.userID,
fullAccess: f.fullAccess,
}
}
@@ -37,7 +37,7 @@ func (f filesystem) withPathRoot(pathRoot pgtype.UUID) filesystem {
db: f.db,
cs: f.cs,
pathRoot: pathRoot,
username: f.username,
userID: f.userID,
fullAccess: f.fullAccess,
}
}

View File

@@ -12,8 +12,8 @@ import (
func (f filesystem) ResourceByID(id uuid.UUID) (Resource, error) {
const query = fullResourceQuery + "WHERE r.id = @id::UUID"
args := pgx.NamedArgs{
"username": f.username,
"id": id,
"user_id": f.userID,
"id": id,
}
if rows, err := f.db.Query(query, args); err != nil {
return Resource{}, err
@@ -64,7 +64,7 @@ func (f filesystem) ResourceByPath(path string) (Resource, error) {
q := pg.Select(r.All(),
pg.Select(goqu.L(publinkFieldsQuery)).From(l).Where(l.Col("root").Eq(r.Col("id"))),
pg.Select(goqu.L("CASE WHEN COALESCE(p.permissions[?]::INTEGER, 0) <> 0 THEN p.id ELSE NULL END AS visible_parent", f.username)),
pg.Select(goqu.L("CASE WHEN COALESCE(p.permissions[?]::INTEGER, 0) <> 0 THEN p.id ELSE NULL END AS visible_parent", f.userID)),
pg.Select(goqu.L("COALESCE(p.permissions, '{}'::JSONB)")),
).
From(r).
@@ -125,9 +125,9 @@ func (f filesystem) targetNameParentByPathWithRoot(path string, src Resource) (s
func (f filesystem) childResourceByName(parentID uuid.UUID, name string) (Resource, error) {
const query = fullResourceQuery + "WHERE r.parent = @parent::UUID AND r.name = @name::TEXT AND r.deleted IS NULL"
args := pgx.NamedArgs{
"username": f.username,
"parent": parentID,
"name": name,
"user_id": f.userID,
"parent": parentID,
"name": name,
}
if rows, err := f.db.Query(query, args); err != nil {
return Resource{}, err

View File

@@ -62,11 +62,11 @@ type FileSystem interface {
TrashEmpty() (int, error)
}
func Open(ctx context.Context, username string, root pgtype.UUID, fullAccess bool) FileSystem {
func Open(ctx context.Context, userID int32, root pgtype.UUID, fullAccess bool) FileSystem {
return filesystem{
db: db.Get(ctx),
cs: storage.Get(),
username: username,
userID: userID,
pathRoot: root,
fullAccess: fullAccess,
}
@@ -74,7 +74,7 @@ func Open(ctx context.Context, username string, root pgtype.UUID, fullAccess boo
func OpenOmniscient(ctx context.Context) FileSystem {
id := rootID()
return Open(ctx, "", pgtype.UUID{Bytes: id, Valid: true}, true)
return Open(ctx, -1, pgtype.UUID{Bytes: id, Valid: true}, true)
}
func rootID() uuid.UUID {

View File

@@ -15,7 +15,7 @@ const (
PermissionSU = Permission(-1)
)
func (r Resource) UpdatePermissions(username string, permission Permission) (Resource, error) {
func (r Resource) UpdatePermissions(userID int32, permission Permission) (Resource, error) {
if r.deleted.Valid {
return r, ErrResourceDeleted
}
@@ -26,14 +26,14 @@ func (r Resource) UpdatePermissions(username string, permission Permission) (Res
permission = permission & r.userPermission
const qUpdate = `
UPDATE resources SET
grants[@username::TEXT] = jsonb_build_object('p', @permission::INT, 't', EXTRACT(EPOCH FROM NOW())::INTEGER),
grants[@user_id::INT] = jsonb_build_object('p', @permission::INT, 't', EXTRACT(EPOCH FROM NOW())::INTEGER),
modified = NOW()
WHERE id = @resource_id::UUID
RETURNING grants`
const qRemove = `
UPDATE resources SET
grants = grants - @username::TEXT,
grants = grants - @user_id::INT,
modified = NOW()
WHERE id = @resource_id::UUID
RETURNING grants`
@@ -47,7 +47,7 @@ RETURNING grants`
err := r.f.runInTx(func(f filesystem) error {
row := f.db.QueryRow(q, pgx.NamedArgs{
"resource_id": r.id,
"username": username,
"user_id": userID,
"permission": permission,
})

View File

@@ -84,7 +84,7 @@ func (f filesystem) scanFullResource(row pgx.CollectableRow) (Resource, error) {
permission := Permission(0)
if f.fullAccess {
permission = -1
} else if p, err := readPermissionFromJson(r.permissions, f.username); err != nil {
} else if p, err := readPermissionFromJson(r.permissions, f.userID); err != nil {
return Resource{}, err
} else {
permission = p
@@ -119,7 +119,7 @@ func (f filesystem) scanResourceWithoutParent(row pgx.CollectableRow) (Resource,
permission := Permission(0)
if f.fullAccess {
permission = -1
} else if p, err := readPermissionFromJson(r.permissions, f.username); err != nil {
} else if p, err := readPermissionFromJson(r.permissions, f.userID); err != nil {
return Resource{}, err
} else {
permission = p
@@ -131,16 +131,16 @@ func (f filesystem) scanResourceWithoutParent(row pgx.CollectableRow) (Resource,
return r, err
}
func readPermissionFromJson(j []byte, username string) (Permission, error) {
func readPermissionFromJson(j []byte, userID int32) (Permission, error) {
if j == nil {
return PermissionNone, nil
}
p := make(map[string]Permission)
p := make(map[int32]Permission)
err := json.Unmarshal(j, &p)
if err != nil {
return PermissionNone, err
}
if p, ok := p[username]; ok {
if p, ok := p[userID]; ok {
return p, nil
}
return PermissionNone, nil

View File

@@ -14,13 +14,13 @@ func (f filesystem) Search(query string, includeDeleted bool) ([]Resource, error
qb.WriteString("\nAND r.deleted IS NULL")
}
if !f.fullAccess {
qb.WriteString("\nAND r.permissions[@username::TEXT]::INTEGER <> 0")
qb.WriteString("\nAND r.permissions[@user_id::INT]::INTEGER <> 0")
}
qb.WriteString("\nORDER BY word_similarity(f_prepare_search(r.name), @query::TEXT) DESC")
args := pgx.NamedArgs{
"query": strings.ToLower(query),
"username": f.username,
"query": strings.ToLower(query),
"user_id": f.userID,
}
if rows, err := f.db.Query(qb.String(), args); err != nil {

View File

@@ -6,13 +6,13 @@ import (
)
func (f filesystem) SharedResources(excluded pgtype.UUID) ([]Resource, error) {
const q = fullResourceQuery + `WHERE r.grants ? @username::TEXT
const q = fullResourceQuery + `WHERE r.grants ? @user_id::INT
AND r.id != @excluded::UUID
AND r.deleted IS NULL
AND (r.grants -> @username::TEXT -> 'p')::INTEGER <> 0
ORDER BY r.grants -> @username::TEXT -> 't' DESC`
AND (r.grants -> @user_id::INT -> 'p')::INTEGER <> 0
ORDER BY r.grants -> @user_id::INT -> 't' DESC`
args := pgx.NamedArgs{
"username": f.username,
"user_id": f.userID,
"excluded": excluded,
}
if rows, err := f.db.Query(q, args); err != nil {

View File

@@ -20,7 +20,7 @@ const publinkFieldsQuery = "COALESCE(JSONB_AGG(JSONB_BUILD_OBJECT(" +
const fullResourceQuery = `SELECT r.*,
(SELECT ` + publinkFieldsQuery + ` FROM publinks l WHERE l.root = r.id) AS links,
CASE WHEN COALESCE(p.permissions[@username::TEXT]::INTEGER, 0) <> 0 THEN p.id ELSE NULL END AS visible_parent,
CASE WHEN COALESCE(p.permissions[@user_id::INT]::INTEGER, 0) <> 0 THEN p.id ELSE NULL END AS visible_parent,
COALESCE(p.permissions, '{}'::JSONB) AS inherited_permissions
FROM resources r LEFT JOIN resources p ON p.id = r.parent
`

View File

@@ -22,11 +22,12 @@ func (f filesystem) TrashList(cursor string, n uint) ([]Resource, string, error)
Select(
r.All(),
pg.Select(goqu.L(publinkFieldsQuery)).From(l).Where(l.Col("root").Eq(r.Col("id"))),
pg.Select(goqu.L("CASE WHEN COALESCE(p.permissions[?]::INTEGER, 0) <> 0 THEN p.id ELSE NULL END AS visible_parent", f.username)),
// TODO: Always select p.id when fullAccess
pg.Select(goqu.L("CASE WHEN COALESCE(p.permissions[?]::INTEGER, 0) <> 0 THEN p.id ELSE NULL END AS visible_parent", f.userID)),
pg.Select(goqu.L("COALESCE(p.permissions, '{}'::JSONB)")),
)
if !f.fullAccess {
q = q.Where(goqu.L("r.permissions[?]::INTEGER <> 0", f.username))
q = q.Where(goqu.L("r.permissions[?]::INTEGER <> 0", f.userID))
}
if cursor != "" {
if d, err := base64.StdEncoding.DecodeString(cursor); err != nil {

View File

@@ -10,7 +10,7 @@ func (f filesystem) selectDeletedResources() *goqu.SelectDataset {
From("resources").
Where(goqu.C("deleted").IsNotNull())
if !f.fullAccess {
q = q.Where(goqu.L("permissions[?]::INTEGER <> 0", f.username))
q = q.Where(goqu.L("permissions[?]::INTEGER <> 0", f.userID))
}
return q
}

View File

@@ -21,8 +21,8 @@ var accessTokenValidity = pgtype.Interval{
var ErrCredentialsInvalid = errors.NewError(http.StatusUnauthorized, "credentials_invalid", "invalid Credentials")
func (m manager) VerifyUserPassword(username, password string) (User, error) {
if user, err := m.UserByUsername(username); err != nil {
func (m manager) VerifyUserPassword(email, password string) (User, error) {
if user, err := m.UserByEmail(email); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return User{}, ErrCredentialsInvalid
}
@@ -39,10 +39,10 @@ func (m manager) VerifyUserPassword(username, password string) (User, error) {
}
}
func (m manager) CreateAccessToken(username string) (string, error) {
const q = `INSERT INTO access_tokens(id, expires, username) VALUES ($1::text, NOW() + $2::interval, $3::text)`
func (m manager) CreateAccessToken(userID string) (string, error) {
const q = `INSERT INTO access_tokens(id, expires, user_id) VALUES ($1::TEXT, NOW() + $2::INTERVAL, $3::INT)`
id := GenerateRandomString(accessTokenLength)
if _, err := m.db.Exec(q, id, accessTokenValidity, username); err != nil {
if _, err := m.db.Exec(q, id, accessTokenValidity, userID); err != nil {
return "", err
} else {
return id, nil
@@ -50,15 +50,16 @@ func (m manager) CreateAccessToken(username string) (string, error) {
}
func (m manager) ReadAccessToken(accessToken string) (User, error) {
const q = `SELECT t.expires, u.username, u.display_name, u.permissions, u.home FROM access_tokens t JOIN users u ON t.username = u.username WHERE t.id = $1; `
const q = `SELECT t.expires, u.id, u.email, u.display_name, u.permissions, u.home FROM access_tokens t JOIN users u ON t.user_id = u.id WHERE t.id = $1; `
row := m.db.QueryRow(q, accessToken)
var expires pgtype.Timestamp
var username string
var userID int32
var email string
var displayName string
var permissions int32
var home pgtype.UUID
if err := row.Scan(&expires, &username, &displayName, &permissions, &home); err != nil {
if err := row.Scan(&expires, &userID, &email, &displayName, &permissions, &home); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
err = ErrCredentialsInvalid
}
@@ -68,9 +69,10 @@ func (m manager) ReadAccessToken(accessToken string) (User, error) {
} else {
return User{
BasicUser: BasicUser{
Username: username,
Email: email,
DisplayName: displayName,
},
ID: userID,
Permissions: permissions,
Home: home,
}, nil

View File

@@ -36,9 +36,9 @@ func (m manager) AddBookmark(u User, resource fs.Resource, name string) (Bookmar
return Bookmark{}, fs.ErrResourceNameInvalid
}
const q = `INSERT INTO bookmarks(username, resource_id, name, dir)
VALUES ($1::TEXT, $2::UUID, $3::TEXT, $4::BOOLEAN)
ON CONFLICT(username, resource_id) DO UPDATE
const q = `INSERT INTO bookmarks(user_id, resource_id, name, dir)
VALUES ($1::INT, $2::UUID, $3::TEXT, $4::BOOLEAN)
ON CONFLICT(user_id, resource_id) DO UPDATE
SET
created = CASE WHEN bookmarks.deleted IS NULL THEN CURRENT_TIMESTAMP ELSE bookmarks.created END,
modified = CURRENT_TIMESTAMP,
@@ -46,7 +46,7 @@ SET
name = $3::TEXT
RETURNING resource_id, name, dir, created`
if rows, err := m.db.Query(q, u.Username, resource.ID(), name, resource.Dir()); err != nil {
if rows, err := m.db.Query(q, u.ID, resource.ID(), name, resource.Dir()); err != nil {
return Bookmark{}, err
} else if bookmark, err := pgx.CollectExactlyOneRow(rows, scanBookmark); err != nil {
return Bookmark{}, err
@@ -56,19 +56,19 @@ RETURNING resource_id, name, dir, created`
}
func (m manager) RemoveBookmark(u User, id uuid.UUID) error {
const q = "DELETE FROM bookmarks WHERE username = $1::TEXT AND resource_id = $2::UUID"
_, err := m.db.Exec(q, u.Username, id)
const q = "DELETE FROM bookmarks WHERE user_id = $1::INT AND resource_id = $2::UUID"
_, err := m.db.Exec(q, u.ID, id)
return err
}
func (m manager) ListBookmarks(u User) ([]Bookmark, error) {
const q = `SELECT resource_id, name, dir, created
FROM bookmarks b
WHERE username = $1::TEXT
WHERE user_id = $1::INT
AND deleted IS NULL
ORDER BY modified DESC`
if rows, err := m.db.Query(q, u.Username); err != nil {
if rows, err := m.db.Query(q, u.ID); err != nil {
return nil, err
} else if bookmarks, err := pgx.CollectRows(rows, scanBookmark); err != nil {
return nil, err

View File

@@ -12,14 +12,14 @@ import (
var ErrUserExists = errors.NewError(http.StatusNotFound, "user_already_exists", "user already exists")
func (m manager) CreateUser(username, displayName, password string, home pgtype.UUID) (User, error) {
const q = ` INSERT INTO users(username, display_name, password_hash, home)
func (m manager) CreateUser(email, displayName, password string, home pgtype.UUID) (User, error) {
const q = ` INSERT INTO users(email, display_name, password_hash, home)
VALUES ($1, $2, $3, $4)
RETURNING username, display_name, password_hash, home, permissions`
RETURNING id, email, display_name, password_hash, home, permissions`
if hash, err := crypt.GenerateArgon2EncodedHash(password); err != nil {
return User{}, err
} else if rows, err := m.db.Query(q, username, displayName, hash, home); err != nil {
} else if rows, err := m.db.Query(q, email, displayName, hash, home); err != nil {
if strings.Contains(err.Error(), "users_pkey") {
err = ErrUserExists
}

View File

@@ -18,7 +18,7 @@ func (m manager) ListUsers(since *time.Time) ([]BasicUser, error) {
if since != nil {
s.Time = *since
}
const q = "SELECT username, display_name FROM users WHERE modified > $1::TIMESTAMP"
const q = "SELECT email, display_name FROM users WHERE modified > $1::TIMESTAMP"
if rows, err := m.db.Query(q, s); err != nil {
return nil, err
} else {
@@ -26,9 +26,9 @@ func (m manager) ListUsers(since *time.Time) ([]BasicUser, error) {
}
}
func (m manager) UserByUsername(username string) (User, error) {
const q = "SELECT username, display_name, password_hash, home, permissions FROM users WHERE username = $1"
if rows, err := m.db.Query(q, username); err != nil {
func (m manager) UserByEmail(email string) (User, error) {
const q = "SELECT id, email, display_name, password_hash, home, permissions FROM users WHERE email = $1"
if rows, err := m.db.Query(q, email); err != nil {
return User{}, err
} else if u, err := pgx.CollectExactlyOneRow(rows, scanUser); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
@@ -40,9 +40,9 @@ func (m manager) UserByUsername(username string) (User, error) {
}
}
func (m manager) UserHome(username string) (pgtype.UUID, error) {
const q = "SELECT home FROM users WHERE username = $1"
row := m.db.QueryRow(q, username)
func (m manager) UserHome(email string) (pgtype.UUID, error) {
const q = "SELECT home FROM users WHERE email = $1"
row := m.db.QueryRow(q, email)
var id pgtype.UUID
if err := row.Scan(&id); err == nil {
if !id.Valid {

View File

@@ -6,27 +6,27 @@ import (
)
func (m manager) UpdateUserHome(user User, home pgtype.UUID) error {
const q = "UPDATE users SET home = $2::UUID, modified = NOW() WHERE username = $1::TEXT"
if _, err := m.db.Exec(q, user.Username, home); err != nil {
const q = "UPDATE users SET home = $2::UUID, modified = NOW() WHERE id = $1::INT"
if _, err := m.db.Exec(q, user.ID, home); err != nil {
return err
}
return nil
}
func (m manager) UpdateUserDisplayName(user User, displayName string) error {
const q = "UPDATE users SET display_name = $2::UUID, modified = NOW() WHERE username = $1::TEXT"
if _, err := m.db.Exec(q, user.Username, displayName); err != nil {
const q = "UPDATE users SET display_name = $2::UUID, modified = NOW() WHERE id = $1::INT"
if _, err := m.db.Exec(q, user.ID, displayName); err != nil {
return err
}
return nil
}
func (m manager) UpdateUserPassword(user User, password string) error {
const q = "UPDATE users SET password_hash = $2::TEXT, modified = NOW() WHERE username = $1::TEXT"
const q = "UPDATE users SET password_hash = $2::TEXT, modified = NOW() WHERE id = $1::INT"
if hash, err := crypt.GenerateArgon2EncodedHash(password); err != nil {
return err
} else {
if _, err := m.db.Exec(q, user.Username, hash); err != nil {
if _, err := m.db.Exec(q, user.ID, hash); err != nil {
return err
}
}
@@ -34,16 +34,16 @@ func (m manager) UpdateUserPassword(user User, password string) error {
}
func (m manager) GrantUserPermissions(user User, permissions int) error {
const q = "UPDATE users SET permissions = permissions | $2::INTEGER, modified = NOW() WHERE username = $1::UUID"
if _, err := m.db.Exec(q, user.Username, permissions); err != nil {
const q = "UPDATE users SET permissions = permissions | $2::INTEGER, modified = NOW() WHERE id = $1::INT"
if _, err := m.db.Exec(q, user.ID, permissions); err != nil {
return err
}
return nil
}
func (m manager) RevokeUserPermissions(user User, permissions int) error {
const q = "UPDATE users SET permissions = permissions & ~ $2::INTEGER, modified = NOW() WHERE username = $1::UUID"
if _, err := m.db.Exec(q, user.Username, permissions); err != nil {
const q = "UPDATE users SET permissions = permissions & ~ $2::INTEGER, modified = NOW() WHERE id = $1::INT"
if _, err := m.db.Exec(q, user.ID, permissions); err != nil {
return err
}
return nil

View File

@@ -11,12 +11,13 @@ import (
)
type BasicUser struct {
Username string
Email string
DisplayName string
}
type User struct {
BasicUser
ID int32
PasswordHash string
Home pgtype.UUID
Permissions int32
@@ -25,7 +26,7 @@ type User struct {
func scanBasicUser(row pgx.CollectableRow) (BasicUser, error) {
var u BasicUser
err := row.Scan(
&u.Username,
&u.Email,
&u.DisplayName,
)
return u, err
@@ -34,7 +35,8 @@ func scanBasicUser(row pgx.CollectableRow) (BasicUser, error) {
func scanUser(row pgx.CollectableRow) (User, error) {
var u User
err := row.Scan(
&u.Username,
&u.ID,
&u.Email,
&u.DisplayName,
&u.PasswordHash,
&u.Home,
@@ -44,17 +46,17 @@ func scanUser(row pgx.CollectableRow) (User, error) {
}
func (u User) OpenFileSystem(ctx context.Context) fs.FileSystem {
return fs.Open(ctx, u.Username, u.Home, u.Permissions&PermissionAllFiles != 0)
return fs.Open(ctx, u.ID, u.Home, u.Permissions&PermissionAllFiles != 0)
}
type Manager interface {
// create.go
CreateUser(username, displayName, password string, home pgtype.UUID) (User, error)
CreateUser(email, displayName, password string, home pgtype.UUID) (User, error)
// select.go
ListUsers(since *time.Time) ([]BasicUser, error)
UserByUsername(email string) (User, error)
UserHome(username string) (pgtype.UUID, error)
UserByEmail(email string) (User, error)
UserHome(email string) (pgtype.UUID, error)
// update.go
UpdateUserHome(user User, home pgtype.UUID) error
@@ -63,7 +65,7 @@ type Manager interface {
// auth.go
VerifyUserPassword(email, password string) (User, error)
CreateAccessToken(username string) (string, error)
CreateAccessToken(email string) (string, error)
ReadAccessToken(accessToken string) (User, error)
// bookmarks.go