Closed ganigeorgiev closed 2 years ago
Honestly it sounds pretty good, as I don't need to create a a profile after user creation, but small question. Does this mean the user has to edit via the user's endpoint and user list had to be from that as well?
This sounds reasonable to me @ganigeorgiev, I like the plan as you have proposed it. It would be nice to have API rules for the user itself and full "manager-subordinate" permissions would be important for an app I'm working on. I also think working off a single user.id
and not having to worry about user.profile.id
could help simplify things from a dev perspective.
While I'll try to automate as much as possible the required DB changes, this unfortunately will not be fully backward compatible.
For my specific app this shouldn't be a problem at this point. Happy to delete my users and manually recreate them, but don't want to speak for others.
This issue is with high priority because it will not be backward compatible and the quicker we introduce the changes the "less damage" it will cause.
I definitely appreciate this. Would love to start building on the new user architecture sooner rather than later.
@theMackabu: Does this mean the user has to edit via the user's endpoint and user list had to be from that as well?
Good question. I haven't decided on that yet. Since the users
will be a system collection, I think we can allow access from both services in order to minimize the breaking changes and for consistency with the admins
service.
Or in other words, the following SDK calls would be equivalent:
client.users.getList(); <=> client.records.getList("users");
client.users.getOne("USER_ID"); <=> client.records.getOne("users", "USER_ID");
client.users.create({ ... }); <=> client.records.create("users", { ... });
client.users.update("USER_ID", { ... }); <=> client.records.update("users", "USER_ID", { ... });
client.users.delete("USER_ID"); <=> client.records.delete("users", "USER_ID");
Internally the users api action could "redirect" to the records api so that the actual handling will happen in a single place.
The create and update user handling will be almost identical as for a regular record, with the exception for the special system fields like email
and password
.
Personally I'd rather have one way of doing things over backwards compatibility (assuming it doesn't limit functionality). PocketBase is new enough that hopefully it wouldn't be unduly burdensome for folks to rewrite these sections of their apps. Allowing both is something you'll carry forward and new users will continually question which way they should do it and the difference between them. Maybe this is an unpopular opinion so thumbs down this if you disagree.
What would the update look like for changing passwords?
await client.records.update('users', 'RECORD_ID', {
password: '123456',
passwordConfirm: '123456',
});
@jimafisk Maybe you are right. I will think a little more on this when I start working on the proposed changes, because supporting both apis may cause also confusion which request event hook will be triggered (user or record), so maybe it would be better to throw an error if someone tries to update the users
collection records via the regular records
api (or the other way arround).
About the password change - in my local notes I have outlined 2 options to consider:
Don't allow users to directly update their email and password, requiring them to explicitly call /api/users/request-password-reset
and /api/users/request-email-change
(at it is now).
Allow users to update their email and password, but require to send also the old password for verification (when changing the password also require 2 new fields to read the new password from). Here are couple of examples how this could look like:
// change only the email
await client.users.update('USER_ID', {
email: 'new@example.com',
password: '123456',
});
// change only the password
await client.users.update('USER_ID', {
password: '123456',
newPassword: '654321',
newPasswordConfirm: '654321',
});
// change the email and password at the same time
await client.users.update('USER_ID', {
email: 'new@example.com',
password: '123456',
newPassword: '654321',
newPasswordConfirm: '654321',
});
// change the email and password at the same time + other fields
await client.users.update('USER_ID', {
email: 'new@example.com',
password: '123456',
newPassword: '654321',
newPasswordConfirm: '654321',
// other regular fields:
name: "test",
});
For admins we will have a separate endpoint which will allow changing all user system fields (email
, password
, verified
) from the Admin UI, without requiring to enter the current user password beforehand.
Honestly the second option seems more in-line with how a lot of other services do it, require old password to set new one.
These changes sounds great to me!
I also agree that keeping both methods for the sake of backwards compatibility is not a good idea - just remove the old method. Users should expect breaking changes before 1.0
I was able to experiment a little with the proposal and the suggestions from above and here are a little more details about the planned changes.
I think the user model and it related services could be completely removed.
Instead we will introduce the following collection types:
(the names of the types are subject to change; suggestions are welcomed)
Or in other words, the "users" will be replaced with auth collection(s).
All existing user auth routes will be replaced with a corresponding collection route, for example:
/api/users/auth-via-email => /api/collections/users/auth-via-email
The SDKs will no longer have a client.users
and client.records
services and instead a new collection service will be created that will be accessible through client.collection(ID_OR_NAME).*
. For example:
// base crud:
client.records.getFullList("test", ) => client.collection("test").getFullList()
client.records.getList("test", ) => client.collection("test").getList()
client.records.getOne("test", "RECORD_ID") => client.collection("test").getOne("RECORD_ID")
client.records.create("test", {}) => client.collection("test").create({})
client.records.update("test", "RECORD_ID", {}) => client.collection("test").update("RECORD_ID", {})
client.records.delete("test", "RECORD_ID") => client.collection("test").delete("RECORD_ID")
// auth collection only:
client.users.authViaEmail(...) => client.collection("test").authViaEmail(...)
client.users.authViaOAuth2(...) => client.collection("test").authViaOAuth2(...)
client.users.refresh() => client.collection("test").authRefresh()
client.users.requestPasswordReset(...) => client.collection("test").requestPasswordReset(...)
client.users.confirmPasswordReset(...) => client.collection("test").confirmPasswordReset()
client.users.getAuthMethodsList() => client.collection("test").getAuthMethodsList()
client.users.getExternalAuthsList(...) => client.collection("test").getExternalAuthsList(...)
client.users.unlinkExternalAuth(...) => client.collection("test").unlinkExternalAuth(...)
// deleted:
[X] client.users.requestEmailChange(...)
[X] client.users.confirmEmailChange(...)
The special @request.user.*
filter rule will be replaced with @request.auth.*
.
You will be able to query the auth collection name (@request.auth.collectionName
) allowing you to even use different auth collections for roles/groups separation.
A new manage
action and API rule will be added for the auth collections to allow manager/supervisor
auth models to be able to change the special subordinate
fields like email
, password
, verified
, etc. without providing the original user password.
The owner of the auth model will be able to change their email or password via the update
action if authorizationPassword
is provided, for example:
// change only the email (the verified state will be auto set to `false` after this call)
await client.collection('users').update('RECORD_ID', {
email: 'new_email@example.com',
authorizationPassword: 'old_pass',
});
// change only the password
await client.collection('users').update('RECORD_ID', {
password: 'new_pass',
passwordConfirm: 'new_pass',
authorizationPassword: 'old_pass',
});
// change the email and password at the same time + some other regular field(s)
await client.collection('users').update('RECORD_ID', {
email: 'new_email@example.com',
password: 'new_pass',
passwordConfirm: 'new_pass',
authorizationPassword: 'old_pass',
// regular fields:
name: 'test',
});
The authorization token schema will be included in the token, meaning that you no longer will have to prefix the token with Admin
or User
:
Authorization: Admin TOKEN => Auhtorization: TOKEN
Authorization: User TOKEN => Auhtorization: TOKEN
There will be also a lot of other changes to the internals, for example to simplify the Record
model accessors we will rename:
SetDataValue -> Set
GetDataValue -> Get
GetBoolDataValue -> GetBool
GetStringDataValue -> GetString
GetIntDataValue -> GetInt
GetFloatDataValue -> GetFloat
GetTimeDataValue -> GetTime
GetDateTimeDataValue -> GetDateTime
GetStringSliceDataValue -> GetStringSlice
For example, you'll be able to access the user email via record.GetString("email")
(this is to unify the record data access API when PocketBase is used as framework).
Overall this will be a major refactoring and a lot of components will change. As mentioned in https://github.com/pocketbase/pocketbase/discussions/123#discussioncomment-3539859, new features will be pushed back for a while, so please be patient. In short, the next couple of weeks I'll try to work on:
This probably will be the last major breaking change before v1.0.0 and if you have any other ideas or suggestions, please add them as comment, so that I could consider them during the refactoring.
@ganigeorgiev some feedback:
in the example above you used "test"
as the collection name for both the catalogue
type collection and the auth
type collection right:
// base crud:
client.collection("test").getFullList()
client.collection("test").getList()
...
// auth collection only:
client.collection("test").authViaEmail(...)
client.collection("test").authViaOAuth2(...)
but this would not be the case for a real app right? you could not have the same name for two different collections - so maybe you would use "test"
for the catalogue
collection and maybe "users"
for the auth
collection?
just want to make sure I'm following along.
as for the collection type names, I like both auth
and single
but not sure about catalogue
. I don't have a better suggestion though :) just does not feel very natural to me
when it comes to the naming changes, I'm also not sure.
previously, we had:
client.records.getList("test", ) // get list of records
client.records.getOne("test", "RECORD_ID") // get one record
client.records.create("test", {}) // create a record
client.records.update("test", "RECORD_ID", {}) // update a record
client.records.delete("test", "RECORD_ID") // delete a record
with the new naming changes:
client.collection("test").getList() // does not get list of collections?
client.collection("test").getOne("RECORD_ID") // does not get a collection?
client.collection("test").create({}) // does not create a collection?
client.collection("test").update("RECORD_ID", {}) // does not update a collection?
client.collection("test").delete("RECORD_ID") // does not delete a collection?
to me this makes less sense! but I guess it's personal preference
maybe:
client.collections("test").records.getList() // get list of records
client.collections("test").records.getOne("RECORD_ID") // get one record
client.collections("test").records.create({}) // create a record
client.collections("test").records.update("RECORD_ID", {}) // update a record
client.collections("test").records.delete("RECORD_ID") // delete a record
could work but I'm guessing you have thought of this already and probably had a good reason not to choose this (maybe too verbose) 😅
regarding the changes to the auth collection:
client.users.authViaEmail(...) => client.collection("test").authViaEmail(...)
client.users.authViaOAuth2(...) => client.collection("test").authViaOAuth2(...)
client.users.refresh() => client.collection("test").authRefresh()
client.users.requestPasswordReset(...) => client.collection("test").requestPasswordReset(...)
client.users.confirmPasswordReset(...) => client.collection("test").confirmPasswordReset()
client.users.getAuthMethodsList() => client.collection("test").getAuthMethodsList()
client.users.getExternalAuthsList(...) => client.collection("test").getExternalAuthsList(...)
client.users.unlinkExternalAuth(...) => client.collection("test").unlinkExternalAuth(...)
is there a specific reason why you want to specify the name of the collection here (in this example "test"
) versus it being a internal constant value? do you want to support people having multiple auth
collections?
if not, then maybe it could still make sense to use something like client.users
even though it now uses a collection
under the hood rather than another model.
why I personally would prefer client.users
over something like client.collection("users")
comes down to three reasons:
1) it is shorter.
const { token, user } = await locals.pocketbase.users.authViaEmail(email, password);
is already pretty long and
const { token, user } = await locals.pocketbase.collection("users").authViaEmail(email, password);
would be even longer
2) more convention over configuration makes for more readable code whenever you look at other peoples pocketbase auth code
3) the difference between client.users
and client.collections("test")
makes it obvious in the code that they have different uses/APIs (the former has .authViaEmail(...)
)
should be noted that 1), 2) and 3) are not major issues but minor nitpicks.
finally, regarding
if you have any other ideas or suggestions, please add them as comment, so that I could consider them during the refactoring.
not sure if this is what you meant, but I would like to make another push for https://github.com/pocketbase/pocketbase/issues/312 if you are anyways refactoring collections. I've used pocketbase in two web apps so far and indirect expand would have been very useful in both of them!
sorry for the long post, hopefully some of it might have been useful.
again, these changes look great and I'm sure they will be great for the pocketbase project regardless of what function names are used.
thanks!
@ollema Thanks for the feedback.
in the example above you used "test" as the collection name for both the catalogue type collection and the auth type collection right:
The idea is that the auth
collection will have everything what the default catalog
colleciton has + additional auth endpoints and some special system fields (email, verified, passwordHash, etc.). Or in other words, you could treat the above example as if the "test" colleciton is auth
.
as for the collection type names, I like both auth and single but not sure about catalogue. I don't have a better suggestion though :) just does not feel very natural to me
I also don't like catalog (or catalogue) but I can't think of anything better. Initially I considered "list", but then in the code it kinda read strange when collection.IsList()
is called (eg. someone may think that this is slice or similar).
why I personally would prefer client.users over something like client.collection("users") comes down to three reasons:
The idea of introducing client.collection(ID_OR_NAME).*
is to unify the crud and auth actions in a single service and to allow multiple auth collections (eg. you could want the manager
auth collection to have different fields than the subordinate
auth collection).
Keeping both client.users
and client.records
could lead to some strange inconsistencies and duplication of the crud services. I think it is a lot easier to reason when everyhing is a collection and can be accessed through the same api (eg. you don't have to know whether a collection is auth
, catalog
or single
in order to call create
or update).
Yes, it is a little longer to type, but users coming from Firebase may find this pattern familiar.
Initially I considered "list"
Was coming here to make the same suggestion. I thought list
made more sense than catalogue
but I see your point @ganigeorgiev. Just spitballing, is the "collection" naming open to change? It's a little strange to have a single
collection in the first place. Maybe the overall structure could just be "data" instead of "collection" of which you have auth
, collection
, and single
types. Not sure if that conflicts with other PB concepts so sorry to add noise, just wanted to throw other ideas out there.
@jimafisk We can change "collections", but "data" is too broad/general as a term and I don't think it is better in this case. In practice, users rarely will have to work directly with the types, since in the Admin UI we will have human readable labels and probably a short description what each collection type do and expect to store.
An alternative to "single" I considered previously also "oneoff" but I dropped the idea because the spelling could be ambigious (one_off, oneOff, etc.).
About the SDK service naming - Instead ofclient.collection(name)
we can use client.use(name)
(similar to how in SQL you target specific database) as a more shorter version. For example:
client.collection('users').getList() vs client.use('users').getList()
client.collection('users').authViaEmail() vs client.use('users').authViaEmail()
Personally, I prefer collection()
because it is clear what actually the method is referring/targetting.
// auth collection only: client.users.authViaEmail(...) => client.collection("test").authViaEmail(...) client.users.authViaOAuth2(...) => client.collection("test").authViaOAuth2(...) client.users.refresh() => client.collection("test").authRefresh() client.users.requestPasswordReset(...) => client.collection("test").requestPasswordReset(...) client.users.confirmPasswordReset(...) => client.collection("test").confirmPasswordReset() client.users.getAuthMethodsList() => client.collection("test").getAuthMethodsList() client.users.getExternalAuthsList(...) => client.collection("test").getExternalAuthsList(...) client.users.unlinkExternalAuth(...) => client.collection("test").unlinkExternalAuth(...)
I'm confused with this API. As in, if a collection wasn't an auth collection are these methods going to exist on it? For example, if I did
client.collection("products").authViaEmail(...) // <- where products is obviously not an auth collection
Is that valid?
@blackmann Yes, you will be able to call that, but you'll get an API error telling you that the referred collection is not an auth.
TLDR; Mixing auth methods with the base collection methods is an overextension of its responsibilities
IMO, I think this design/API will become convoluted. Because as auth collections get extended with new methods, they'll be applied to collections
in general. I (personally) find that weird.
How about, say a more explicit, name like client.auth("managers")
which will just return a result with these methods as an extension of [the base] collections.
I also want to use this opportunity to say, this is really great work and I'm using this in my next SaaS project starting this weekend. It's providence that I found this.
Thank you!
Stumbled on it from some Youtube shorts!
Mixing auth methods with the base collection methods is an overextension of its responsibilities
But this is currently how the client.users.*
service is organized.
client.users.getFullList()
client.users.getList()
client.users.getOne()
client.users.create()
client.users.update()
client.users.delete()
client.users.refresh()
client.users.authViaEmail()
client.users.authViaOAuth2()
...
How about, say a more explicit, name API like client.auth("managers") which will just return a result with these methods as an extension of [the base] collections.
I thought of this, but I don't like that it collides with client.authStore.*
and it is not consistent with client.admins.*
(the admins will not be a collection since they have different business logic binded to them, like skipping API rules, special super-access middlewares, etc.).
An alternative to "single" I considered previously also "oneoff"
I agree that single
is better, I don't have a problem with it, just wanted to think it through.
Personally, I prefer collection() because it is clear what actually the method is referring/targetting.
I'd tend to agree that collection('whatever')
is more clear than use('whatever')
.
I think this design/API will become convoluted. Because as auth collections get extended with new methods, they'll be applied to collections in general.
I would assume the extension would apply to auth
collections only?
@ganigeorgiev - the API decisions you've made so far have be great, so I'm definitely comfortable with you making executive decisions on what you think is best.
@jimafisk I would assume the extension would apply to auth collections only?
Yes, auth related methods will be available only for auth
collections. Obviously, since the collection is accessed via its name or id, nothing will stop you to us an auth method with a non-auth collection, as @blackmann mentioned, but you'll get an API error.
The same will be true if we move the auth related methods in a separate auth(name)
service. We will have a type checker on the server-side no matter the SDK API. The whole complication around client.collection(name).*
is because I want to support multiple auth collections (at work I have a use for that for a small internal company knowedge base, where there is expected to have 2 different auth models - staff
and client
and both have different set of fields).
@ganigeorgiev what I mean is...
How the current client.users.*
is great. Because you could only access those methods .authVia*
, .confirmPassword
, etc on just the client.users
but not other collections.
In other words, intentions should be clear while reading code instead of cross-checking with admin panel or during runtime.
But as @jimafisk said, you'll have to make the executive decision. I'm just a troublemaker, haha.
@blackmann : In other words, intentions should be clear while reading code instead of cross-checking with admin panel or during runtime.
I get what you mean but I think even if we ended up choosing the client.collection().*
API, the name of the collection should be enough to make it clear while reading the code what is the intention of the collection (eg. "users", "clients", etc.). Tomorrow after work I'll research other libraries for ideas/inspirations and will post here if I stumble on something.
In the meantime, the TLDR version of the above discussions, seems to be around:
The names of the 3 new collection types - auth
, catalog
, single
.
The changes in the SDKs API. Based on the feedback, I think we can narrow the options to these 2:
client.records.getList("users"); vs client.collection("users").getList();
client.records.getOne("users", "RECORD_ID"); vs client.collection("users").getOne("RECORD_ID");
client.records.create("users", {}); vs client.collection("users").create({});
client.records.update("users", "RECORD_ID", {}); vs client.collection("users").update("RECORD_ID", {});
client.records.delete("users", "RECORD_ID", {}); vs client.collection("users").delete("RECORD_ID", {});
client.auth.viaEmail("users", "test@example.com", "123456"); vs client.collection("users").authViaEmail("test@example.com", "123456");
client.auth.viaOAuth2("users", ...); vs client.collection("users").authViaOAuth2(...);
// other auth related methods...
(for now, I'm inclined more towards the latter because it is consistent with the existing admins
service and it also may feel familiar to Firebase users).
I'm still a newbie but I like client.collection("users").authViaEmail("test@example.com", "123456")
more than client.auth.viaEmail("users", "test@example.com", "123456")
(it is also similar to how it is now with client.users
).
Could you also add the auth methods to the api preview in the dashboard for the auth collections? I often use the preview to copy paste the code and I think it will be useful for the auth methods too.
Could you also add the auth methods to the api preview in the dashboard for the auth collections? I often use the preview to copy paste the code and I think it will be useful for the auth methods too.
@denisgurev Yes, I will consider it when redesigning the Collections UI.
The names of the 3 new collection types - auth, catalog, single.
Other suggestions instead of collections
with collections
types auth
, catalog
, single
:
collections
with the following collections
types:
auth
, ~list
~ (confusing), single
auth
, multiple
, single
auth
, set
, single
auth
, series
, single
auth
, array
, single
(this kinda sucks)data
with the following data
types:
auth
, collection
, single
Did I miss anything?
I honestly like almost every example listed above here more than catalog, but of course that is just personal preference.
I especially like replacing collection
with data
, that feels very at home to me!
The changes in the SDKs API. Based on the feedback, I think we can narrow the options to these 2:
If you are looking for user input, I would like to cast my vote for option 1 for reasons stated earlier in the thread
@ollema I honestly like almost every example listed above here more than catalog, but of course that is just personal preference.
data
is too common and miscellaneous as a term, because everything could be data and will require special naming for local variables when working with the "data" models. collection
is a little more specific and doesn't require too much additional context to understand what the model/variable store.
I thought of multiple
initially, but I wasn't sure whether it was clear enough. Now that I think on it again, I'm starting to like it - it is a direct antonym of single
and it also reads well in code (eg. collection.IsMultiple()
). I'll still do a further research, but I think it is a good alternative to catalog
.
While the collection type is not critical even if we change them in the future, because most users will use the Admin UI to manage their collections, the SDK public API on the other hand is problematic, because I don't want to introduce another major breaking change in the future. Once I finalize the pure backend changes, I'll spend a little more time on this to explore other options.
thanks @ganigeorgiev - very valid points, sounds great!
Hi, just became aware of this discussion due to the linking in another issue. It's a long thread with a lot of things discussed. I would like to summarize the most important things at the beginning for me (the following discussions are mostl about naming and conventions for CRUD services) and would like to add some notes:
collection types
(auth, catalogue, single); the collection type
auth
has special CURD service like `authViaEmail, etc.@request.auth.*
to query different collections with the tpye auth
; using different collections for users would allow to have roles like "admin"-users are in the collection "admin", "member"-users are in the collection "member"Hope that matches the most important points; especially the new collection type auth
.
I'm not a big fan of mixing the special process of authentication with the underlying resources user / profile / account (however you are calling it) with the normale resources stored in the collection. I do understand that the basic CRUD services like "list all", "get one", "create", and so an are technically the same and can be internally shared in the core app. But the resources "user" / "profile" will ever have some special methods or implementation for example to sanitize sensitive information like password or a method which will update the current password. That's why I would say that the underlying resources for the authentication should be handled separately. PocketBase is not an IAM or a fully identity solution; that's why I would keep it simple. Products like Firebase are separating the resource user
and the corresponding process of authentication too (see for example https://firebase.google.com/docs/reference/js/v8/firebase.User ). Plus, in future there will be additional feature requests to include special processes or special attributes like mobile number verification and authentication, OTP code, and very important to have a federation of users. I think it's easier to keep it separately.
Personally, I'm a fan of separating the resources user
and profile
. The resource user
is to manage the authentication process and profile
is used to save additional information of the user which may change over the time. I like the idea of having a simple key/value store managed by the user-land code. Looking, for example at Keycloak, the have an additional relation table user_attribute
. A technical implementation not have an additional relation may to use the existing table user
and have an attribute profile
as a JSON document for a key/value store. That would mean I can store more or less anything from user-land code as long as I'm responsible for un/marshalling it.
To be honest, I do not the like the idea of having a special collection type auth
. I do not understand the benefits for this. I'm managing all my DB updates with the migrations to have a streamlined CI / CD that results in the same database in the different stages. By using a collection type auth
you must ensure that the database migration files will have all necessary attributes / fields like username
, password
, email
, and so on. I think this results in a heavy mess in the future by having new feature requests like for example OTP, federated users, and so on. I do unterstand the technically background to share the underlying methods for the collections (which are basically SQL tables) and to provide the same CRUD services to the SDK API. But again, for me the authentication process and underlying resource are too important and special to mix; plus new feature requests make it more difficult to implement.
You have mentioned two additional benefits for using the special collection type auth
as follows
@request.auth.*
to query different collections with the type auth
I think that these points are not necessarily linked to a new collection type auth
.
Using different collections for different kind of users (like admin, member, staff, etc.) may result in multiple account for the same human person (human person "max" may be a "member" and a "staff", and so on). Linking OAuth provider would be quite tricky; federating user identities as well, SSO would not be possible. I would rather see a new attribute "roles" which may be in the core app or is a user-land managed attribute.
Haven't yet used API rules in details but I'm using Open Policy Agent (OPA) a lot; it's the same idea. As a vision I would LOVE to have OPA integrated in PocketBase API rules. In the meantime it would be great to have full access to the request-input (or the authenticated user; JWT and user-model) to access all user-land attributes like "roles". With that you could implement an RBAC, ABAC, and a full "manager-subordinate".
So, long post, short:
auth
Sorry, to have different oppinion. Hope this helps in the discussion.
@mbecker Thanks for the feedback.
Personally, I'm a fan of separating the resources user and profile.
You still will be able to create your own profile
collection and create a relation to it if you want to keep your users data in a separate collection.
By using a collection type auth you must ensure that the database migration files will have all necessary attributes / fields like username, password, email, and so on.
The existing user model fields (email, verified, passwordHash, etc.) are system fields and will be automatically created for each auth
collection (and underlying table) and they cannot be deleted or renamed. From the Admin UI you will be able to manage only the configuration of your custom fields (the same as it is now with the profiles
collection).
To be honest, I do not the like the idea of having a special collection type auth. I do not understand the benefits for this.
The main benefits of treating the users models as a regular collection are:
auth
collection can be configured to be allowed to fully manage the users in another auth
collection (including changing their password, email, verified state without prior confirmation to be required).Using different collections for different kind of users (like admin, member, staff, etc.) may result in multiple account for the same human person (human person "max" may be a "member" and a "staff", and so on). Linking OAuth provider would be quite tricky; federating user identities as well, SSO would not be possible.
If your auth models have the same set of fields, then you could just use a single auth collection (by default "users" will be created). OAuth2 linking and everything related to the authentication will be only within the collection namespace.
Extend API rules to have full access to the JWT and authenticated user model (for example to get the attributes"profile.roles")
This is already supported. You should be able to query the user profile fields by doing @request.user.profile.*
.
Implement OPA for API rules and JWT / user model is the input data
I don't plan implementing an extra access layer. It will complicate both the management in the UI and when used as framework. The event hooks are the recommended way to extend and tailor the default PocketBase behavior if users need more fine grained auth controls than what the API rules provide.
While the field type “user” may be identical to a relation field pointing at the user table, I do think it’s a comforting and nice feature to have this dedicated “user” field type.
Also, I do think it’s important to not force PII like email into a public facing record. That appears to be one benefit of the separate “profile” table.
@lukepighetti Based on the several raised issues and discussions in the past, most users seems to find it confusing to have both a user
model and profiles
collection (for example https://github.com/pocketbase/pocketbase/discussions/242).
Having only one entity ("collection") should be a lot easier and intuitive for the developers.
Also, I do think it’s important to not force PII like email into a public facing record. That appears to be one benefit of the separate “profile” table.
I agree, but since the users
will be a collection, you will be able to limit the access to the user records with the collection API rules.
Users will always have the option to create their own profiles
collection and reference it with a relation
field. The only difference from the existing implementation is that it will not be created by default.
@ganigeorgiev with the new architecture would there be a way to allow users from different auth type collections to use the same login form? I'd like to have a manager-subordinate relationship between "staff" and "members" where we'd need to collect additional profile information for members, but don't need this for staff. This seems like the correct use-case for two different auth types, but wasn't sure if I'd need to handle this logins separately if I did that as opposed to a single auth type with a custom "role" field. Thanks!
with the new architecture would there be a way to allow users from different auth type collections to use the same login form?
On the frontend you can show the same login form, but when submitting the form you'll need to specify the auth collection you are trying to sign-in, eg. client.collection("staff").authViaEmail(...)
. The idea is that each auth collections will have their own options to configure the auth behavior. Currently this are the options that I'm planning to use based on my needs:
type CollectionAuthOptions struct {
FullAccessRule *string `form:"fullAccessRule" json:"fullAccessRule"`
AllowOAuth2Auth bool `form:"allowOAuth2Auth" json:"allowOAuth2Auth"`
AllowOAuth2Registration bool `form:"AllowOAuth2Registration" json:"AllowOAuth2Registration"`
AllowUsernameAuth bool `form:"allowUsernameAuth" json:"allowUsernameAuth"`
AllowEmailAuth bool `form:"allowEmailAuth" json:"allowEmailAuth"`
RequireEmail bool `form:"requireEmail" json:"requireEmail"`
ExceptEmailDomains []string `form:"exceptEmailDomains" json:"exceptEmailDomains"`
OnlyEmailDomains []string `form:"onlyEmailDomains" json:"onlyEmailDomains"`
MinPasswordLength int `form:"minPasswordLength" json:"minPasswordLength"`
}
The configuration of the individual OAuth2 providers will be global and shared across all auth collections; each auth collection will have only the option to allow/disallow OAuth2 sign-in/up.
Additionally, each auth record will have an optional emailVisibility
field, so that users can control on their own whether their email should be visible in listings or expansions (this will be ignored for admins or when the FullAccessRule
is satisfied).
Each auth colleciton will have its own table, meaning that you'll be able to have a user registered both in staff
and members
. We'll ensure only that the auth record id
is unique across all auth collections in order to simplify the API rules check when doing something like userId = @request.auth.id
.
@jimafisk If you don't have a way to identify upfront to which collection you want to submit the login form, you can apply a "failover" flow - first send the request to the staffs
collection and if it fail send another request to the members
collection. If both fail, show an error:
const email = "...";
const password = "...";
try {
await client.collection("staff").authViaEmail(email, password);
} catch (_) {
// failover to "members" authentication
try {
await client.collection("members").authViaEmail(email, password);
} catch(err) {
alert("Invalid login credentials");
}
}
Just came accross this issue after starting to write an application with pocketbasae. I agree with this change wholeheartedly, users and profiles seperated made no sense to me. Do you have an ETA on when this change (0.8) wil release? I will just postpone my implementation so I do not have to rewrite
@JessedeJonge It's hard to tell. I've been asked a lot lately for ETAs (especially on the support email) and I understand that it is important, but as I mentioned in https://github.com/pocketbase/pocketbase/discussions/123#discussioncomment-3539859, I work on PocketBase only on my free time and I don't want to constraint my self with hard deadlines.
The "pure" server-side changes (including a data migration command) are already done, but a lot of the existing unit and integration tests need to be rewritten from scratch, which along with the Admin UI redesign, probably will be the most time consuming parts of the refactoring.
Today I've taken a day off from my day job and I've started working on the server-side tests, but still there is a lot of work that remains and I don't want to rush it (especially when users authentication is involved).
If you need some "approximate time estimate", probably I'd say somewhere around mid October.
Threre would be a migration guide that will help with the upgrade, but if you are using the SDKs I expect the changes, excluding the merged users and profiles, to be mostly "cosmetics", eg. renamig client.records.getList("test")
to client.collection("test").getList()
or client.users.authViaEmail(...)
to client.collection("users").authViaEmail(...)
, etc.
Make sure you take your time so you don't burn out. PocketBase looks like it has a bright future.
Just went thru this excellent thread, and I probably won't get too many people agreeing with my point of view, but heck I'll add my 2c, as I am a user of many Baas and similar type of software. I am amazed at certain aspects of Pocketbase, which make it a dream platform for certain business uses cases. However, I would say that user authorization is not one of the features on Pocketbase that I think is revolutionary or that will give it mass adoption. There are just too many authorization platforms that are way ahead and it will take an enormous amount of work to get it up to par and then keep up. Do you really want to spend your time developing a new authorization platform? Personally, I see the strengths of Pocketbase in entirely different areas and I think a focus on user authorization tables is a distraction, and that adding other features instead would be a more wise use of time, and would add alot more value to the platform. It's just a guess, but I suspect most people trying out Pocketbase are just using the admin interface, and admin APIs, because they want an easy to use backend for some application, that actually feels like database (personally I dislike how all these Baas try to be like a spreadsheet) and isn't painful to set up and use. I doubt storing user authorization is what most people are looking for, again because there are so many other solutions already out there that work well. Simply having user tokens, which Pocketbase already does well, is probably sufficient.
In terms of 2 other features that would be more beneficial than user authorization:
The above 2 features are honestly the only 2 critical features, I think are lacking right now in Pocketbase. Everything else added on top is obviously amazing, but without those 2, I think the use cases get a bit restricted here. It's doubtful anyone using Pocketbase really wants to spend time developing a front end for their team to add/edit data, and they probably already have serverless functions they can hook into for business logic.
One final feature, given my thoughts on user authorization:
@osseonews For the SSO and 3rd party auth platforms - there is no built in solution for that, but you can always implement it on your won if you use PocketBase as framework by passing additional context from the 3rd party auth system. For example you could check https://github.com/pocketbase/pocketbase/issues/136#issuecomment-1186191919 (this will change slightly after the refactoring, but more-or-less the idea will be the same - an auth model will be considered authenticated if exist in the request contex).
@ganigeorgiev Thanks. Issue 136 answer the question, but my difficulty is that I don't know Go and more importantly don't know how to set up Pocketbase as a framework on Digital Ocean. I've already got it up and running without extending it, and continue to be incredibly impressed. Maybe there is some guide for setting up framework on DO? Or maybe this is something pockethost.io can handle?
In any case, I do agree with the person in Issue 136 where they said as follows: "no more effort into auth. pocketbase already has more than enough for standard use." If you can easily autheticate with a header as in 136 code, then I think auth is solved, and most people, especially enterprises, will almost always use their internal auth provider.
It's a cool idea to allow 3rd party auth, but personally I would be sad if the built-in auth was sacrificed for this. The big sell for me is that PocketBase is an all-in-one solution that doesn't require complicated integrations. There are endless BaaS solutions targeting the enterprise market, imo PB carved a niche with practical self-hosting that's resonating with folks.
@jimafisk I agree 100%, just saying the the auth that is available now is suitable for nearly all cases, and enterprise users, who will ultimately pay for this platform, will never migrate from their current auth platform to a pocketbase auth, so I don't see much benefit in further building out auth. "There are endless BaaS solutions targeting the enterprise market". Yes, that's true, but none are truly portable and all are silly expensive in the long-run with huge lock-in. Plus, most neglect some basic features that are available out of the box with Pocketbase. That's why I think Pocketbase resonates. Upload an executable and you have your database backend with an amazing admin interface + API in minutes! This is a dream! I think most people will use Pocketbase as a Baas to replace many use cases, but will still retain existing auth. Will be interesting to see how this develops.
Small proposal to move authViaEmail
and all required data for them into provider table with provider type 'emailAndPassword'.
New auth method will get provider type and required values for them. It's unify logic and solve problem with update user data because in user model will be stored only user data (not auth data which required verification in email case and maybe phone number in future).
New users
model require only id
, created
, updated
fields. All other fields optional.
New authProviders
model with id
, created
, updated
userId
, providerType
(forignkey to authProviderOptions
. Some examples: 'email', 'phoneNumber', 'oauth2.google', 'oauth2.facebook'), lastUsed
, and some json fileds provider_data
(contains inforamthion from oauth token or for emial provider verified
, verification_code
if email have been sent and some other fields), meta
(additional information like comment
or any other)
New authProviderOptions
model with id
, type
, options
(type json).
Exmaple row for email
provider 1, 'emailAndPassword', { 'AllowedDomains': ['gmail'], 'EmailVerificationTokenDuration': 604800, 'templates': {'Verification': {...}, ...}}}
and for google
provider 2, 'oauth2.google', {'appName': ..., 'appSecret': ..., 'AllowAuth': true, 'AllowRegistration': true, 'AutolinkBy': ['email'], ...}
Api methods examples:
// sign in using email
await client.collection("users").signIn('emailAndPassword', {'email': 'example.com', 'password': 'qwerty1234'});
// sign up using email
await client.collection("users").signUp('emailAndPassword', {'email': 'example.com', 'password': 'qwerty1234'});
// sign in using google
await client.collection("users").signIn('oauth2.google', {'code': '...'}); // insted of code we can send accessToken.
// sign out
await client.signOut()
This model is highly expandable. In it, for example, you can generally remove the auth via email and leave only the auth via google. You can also easily add any options for any user auth method and for each auth provider globaly. What do you think aboud this changes?
@heckad
Small proposal to move authViaEmail and all required data for them into provider table with provider type 'emailAndPassword'. New auth method will get provider type and required values for them. It's unify logic and solve problem with update user data because in user model will be stored only user data
But this is basically how currently the user data is managed using the profiles
collection. You wouldn't really benefit too much by moving the email and other common user fields to a separate table - it will just complicate the internals requiring always to have a join with the related "auth provider". In addition, there will be always some auth specific field (eg. password) that will have to be shared across multiple auth providers (email, username, phone, etc.).
The idea of this issue is to simplify the users management and to add support for:
@ganigeorgiev
You wouldn't really benefit too much by moving the email and other common user fields to a separate table
What if it is necessary that only users from our google workspace can sign in? If email and password would be a simple provider, we just can off them. I just don't know whether to make it as a separate feature request or it's better to make it into the current changes since backwards compatibility breaks.
it will just complicate the internals requiring always to have a join with the related "auth provider".
Can you explain why does it required always to have a join with the related "auth provider"? Maybe we have an external jwt token generator which generates correct tokens. In such a case, we didn't require any linked auth method at all.
@heckad
What if it is necessary that only users from our google workspace can sign in? If email and password would be a simple provider, we just can off them.
But you'll be able to do that (see the auth collection options in https://github.com/pocketbase/pocketbase/issues/376#issuecomment-1243574727). Why moving the email field in a separate table will help with that?
Can you explain why does it required always to have a join with the related "auth provider"?
Because you'll need to perform a search/sort based on the user email, username, etc. Additionally this will increase the complexity for developers using PocketBase as framework, because they will have to manually query or use some special helper to fetch the email every time when they need to read it.
Maybe we have an external jwt token generator which generates correct tokens. In such a case, we didn't require any linked auth method at all.
I don't see issue with that. If all possible auth methods are disabled (email, username, oauth2) you'll still be able to create and manage your users based on your custom auth logic. As mentioned in this https://github.com/pocketbase/pocketbase/issues/376#issuecomment-1250267806, an auth model is considered authenticated as long as it exists in the request context.
But you'll be able to do that (see the auth collection options in #376 (comment)). Why moving the email field in a separate table will help with that?
It's just that these fields only apply to one authorization method. It is very strange to see them in the main model when they may not be useful. Intuitively, I want to take them out to where they will be the very place.
Because you'll need to perform a search/sort based on the user email, username, etc. Additionally this will increase the complexity for developers using PocketBase as framework, because they will have to manually query or use some special helper to fetch the email every time when they need to read it.
Filter example:
client.collection('users').filter({"provider.emailAndPassword.email": "example@email.com"}}
Also, we can filter by another provider if a provider has different email
client.collection('users').filter({"provider.google.email": "example@gmail.com"}} ( email and other data field extracted from token)
I don't see issue with that. If all possible auth methods are disabled (email, username, oauth2) you'll still be able to create and manage your users based on your custom auth logic.
It would be great to have the built-in ability to use third-party tokens as if they were actually issued. For example, configure where to get JWKS
and the user id field name. Probably this is also the feature request.)
It's just that these fields only apply to one authorization method. It is very strange to see them in the main model when they may not be useful. Intuitively, I want to take them out to where they will be the very place.
That's not always the case. As mentioned previously, there are use cases where you need to share the fields with other auth providers. The password field is one example. Another is for example, linking an oauth2 account to an existing user by its email, or even verifying the user email with the oauth2 sign-in (this is actually already implemented in https://github.com/pocketbase/pocketbase/blob/master/forms/user_oauth2_login.go#L127-L188).
Filter example: ...
The shown example uses the SDK. If we move the individual email field to separate table, internally you will have to join it to the user model and this will not be very convenient when PocketBase is used as framework.
It would be great to have the built-in ability to use third-party tokens as if they were actually issued.
This is too user-land specific and I don't think it can be integrated by default. If you need custom auth logic, you have to use PocketBase as framework and atatch a router middleware similar to the linked example above. For JWKS you may want to check https://github.com/pocketbase/pocketbase/issues/144.
I think discussion is good, but be careful bike shedding on such a small high leverage personal project unless there is a huge benefit to the proposals
I think a big feature that should come out of (and enable) this is controlling which record fields should be public vs private.
For example, when merging users and profiles, the password, passwordConfirm, userId
, and provider fields should be marked as private/hidden. This way, when attaching the profiles
relation to a collection (eg articles
), none of the sensitive information will be included when ?expand=author
is present on the articles list response.
This would be useful if the toggles were available globally as I don't want all fields to be publicly visible in the API response.
@lukeed Private/hidden API fields for now are left out of the scope of v0.8.
The implementation in the upcoming v0.8 pre-release hides by default all sensitive auth model fields (eg. password/passwordHash, tokenKey, etc.). The user will have an option only to toggle the visibility of their email
using the system emailVisibility
bool field. Or in other words, when expanding a user relation, you'll have access to the following props:
{
// common meta fields available for all records
"id": "...",
"created": "...",
"updated": "...",
"collectionId": "...",
"collectionName": "...",
"expand": {...}
// fields available only for auth collections
"username": "...", // all users will have a required username (it is autogenerated by default)
"verified": true,
"emailVisibility": true,
"email": "test@example.com", // visible only if emailVisibility is true
// other collection fields...
"name": "..."
}
The email
address will be always visible (aka. ignoring emailVisibility
) when 1 of the following conditions is met:
@request.auth.id = id
)(the same applies when you perform search/sort over the email
field)
If you need to control the visibility of the other fields, for now they'll have to be moved in a separate collection (aka. similar to the existing profiles
workflow).
This was partially discussed in https://github.com/pocketbase/pocketbase/discussions/225#discussioncomment-3511148 and other similar older discussions.
The current approach of having 2 separate entities (users and profiles) to manage the application users is not very convenient and has several drawbacks:
It is confusing (and unnecessary) to have both
user
andrelation
type fields.Setting a profile field value during user create is too verbose and nonintuitive. Currently the process requires sending multiple non-transactional requests:
The
profiles
collection hascreate
anddelete
API rules, but they are not really useful. Furthermore, because an empty profile is created automatically on user create, theprofiles
create API rule is not actually triggered (unless the profile record was deleted and then created manually).Because the users api doesn't have API rules, currently everyone is allowed to create a new user.
In addition to the above, there also no easy support for full "manager-subordinate" users management. While one user could be allowed to edit the profile data of another user, currently it is not possible, without using PocketBase as framework, to allow for example one user (eg. manager) to completely manage or delete another user (eg. subordinate).
Considering the above shortcomings, I propose to merge the
user
models and theprofiles
collection into a new single system collection calledusers
.This basically means that:
We will remove the
user
type field and migrate it existing usage to arelation
type field (pointing to the newusers
collection).We will be able to use the new collection's API rules to better control who can access and perform each crud user operation.
We will be able to create/update a user with just one request, something like:
Because of the specifics and the edge cases related to the system fields (email, verified, password) we will have to modify the default behavior of the
record_upsert
form.While I'll try to automate as much as possible the required DB changes, this unfortunately will not be fully backward compatible. Of course, there will be a detailed migration guide, but still users will have to update manually their existing custom app code.
This issue is with high prirority because it will not be backward compatible and the quicker we introduce the changes the "less damage" it will cause.
Please comment with your feedback if you have any different use case or idea how to handle better the current implementation shortcomings (pseudo code and examples are also welcomed).