ποΈ PUT-1705: TeamService β provision an org account - #3713
Conversation
c121eae to
9e12175
Compare
9e12175 to
3916dd5
Compare
3916dd5 to
b138c3d
Compare
7ade8d3 to
bbf43f2
Compare
bbf43f2 to
28920a1
Compare
28920a1 to
d6fdc5e
Compare
d6fdc5e to
99fbef5
Compare
99fbef5 to
9be96b6
Compare
9be96b6 to
1e8bef8
Compare
1e8bef8 to
b73b048
Compare
b73b048 to
d05627d
Compare
d05627d to
6192c79
Compare
6192c79 to
917531d
Compare
917531d to
01df2b6
Compare
01df2b6 to
eaac8cd
Compare
eaac8cd to
e885ed2
Compare
e885ed2 to
f3547cb
Compare
Local validationEngine: mysql 8 Β· Provisioning three seats, then hitting the cap The rows that result β seats are Provisioning mail actually delivered (mailhog, real SMTP β not a spy), carrying the Body confirms no credential is included, as intended β the password goes out of band. Credential reissue returns a fresh one-time password Provisioning, the seat cap, β One follow-up, tracked in PUT-1745 rather than here: |
f3547cb to
f026da6
Compare
Covers PUT-1705. The master account supplies { username, email }; the account
is created with no password, gets the default filesystem tree, joins with
org_owned = 1, and receives a one-shot activation link.
Activation reuses password recovery rather than new token machinery: the same
pass_recovery_token, the same one-hour purpose-scoped JWT, the same
/action/set-new-password link. No team_activation table, no new token type,
and no unauthenticated endpoint on the team surface. Activation state needs no
column either -- an unactivated account is one with no password.
Applies the same username and email rules as signup rather than its own:
USERNAME_REGEX, USERNAME_MAX_LENGTH, RESERVED_USERNAMES and validator.isEmail,
now exported from AuthController. Without them a workspace could mint accounts
signup would refuse -- the username becomes the /username home-directory
segment -- claim unregistered reserved names, and send activation mail to
arbitrary unvalidated addresses at the route's daily limit.
Usernames come from Puter's global pool, so a taken one is refused with free
alternatives rather than silently modified: a suffixed name would appear in
every share dialog that person ever sees, and they never agreed to it. The
check runs before any write, so a rejected provision leaves no orphaned user
row -- asserted by a test on the workspace's member count.
The new account carries requires_email_confirmation, since the address came
from the administrator rather than its holder.
Adds a team_account_activation email template stating what the workspace can
and cannot do -- including that it can reset the password, which the design
requires be said rather than only claiming files are private.
free_storage stamping and the billing event are phase 3.
f026da6 to
673f7fe
Compare
PUT-1705 on its own. This is the operation the feature exists for, it creates real user accounts and sends mail, so it doesn't share a review with anything.
Activation reuses password recovery
No new token machinery, no
team_activationtable, no unauthenticated endpoint on the team surface. Provisioning sets the samepass_recovery_tokenthat/send-pass-recovery-emailuses, signs the same one-hourotpJWT withpurpose: 'pass-recovery', and hands back the same/action/set-new-passwordlink.That inherits, for free: a one-shot token cleared on use, expiry carried in a signed JWT rather than a DB column, atomic single-use consumption, purpose scoping so the link can't be replayed for another operation, and refusal for suspended accounts.
Activation state needs no column either. An unactivated account is one with no password β the predicate migration
0066already uses.resendActivationrefuses an account that has one, because re-sending then would be a password reset wearing the wrong name.Usernames come from the global pool
Usernames are globally unique and case-insensitive (
idx_user_username_nocase,0055), so a workspace provisions from the same namespace as every other Puter user. Two companies cannot both have ajuan.What provisioning adds is a check and a suggestion β never a silent modification. A suffixed
juan-castro-2would appear in every share dialog that person ever sees, and they never agreed to it. So a taken name is refused withusername_already_in_useplus alternatives that have been verified free, and the administrator chooses.β The check runs before any write. A taken name failing part-way through account setup would leave an orphaned
userrow and a half-provisioned account. There's a test asserting the workspace's member count is unchanged after a rejected provision.Two smaller decisions
requires_email_confirmationis set on the new account. The address came from the administrator, not its holder, so it is unverified by definition β and Β§5.5 later depends on that address being the member's, which makes confirming it load-bearing rather than tidy.The
team_account_activationtemplate states what the workspace can and cannot do:That last line is required, not padding. Stating only that admins cannot see the contents would describe a guarantee the system does not make.
The equivalent disclosure on the set-password screen is phase 6, since that's GUI.
Verification
Eight tests added: the account is created password-less and owned by the workspace, it gets its filesystem tree (
trash_uuidset, which is alsogenerateDefaultFsentries' own idempotency guard), the activation link is returned and a recovery token stored, a taken username writes nothing, suggestions are verified free and never equal the taken name, a non-master is refused, re-issuing rotates the token, and an activated account cannot be re-sent.Not here
user.free_storagestamped from a team policyconfig.storage_capacitylike any other. SeeTEAMS-BILLING-SPLIT.mdprovisionCloses PUT-1705.