Skip to content

Commit 588b831

Browse files
committed
Add an API to patch user fields
1 parent 742c241 commit 588b831

File tree

2 files changed

+76
-2
lines changed

2 files changed

+76
-2
lines changed

jupyter_server/auth/identity.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from http.cookies import Morsel
2121

2222
from tornado import escape, httputil, web
23-
from traitlets import Bool, Dict, Type, Unicode, default
23+
from traitlets import Bool, Dict, List, TraitError, Type, Unicode, default, validate
2424
from traitlets.config import LoggingConfigurable
2525

2626
from jupyter_server.transutils import _i18n
@@ -31,6 +31,10 @@
3131
_non_alphanum = re.compile(r"[^A-Za-z0-9]")
3232

3333

34+
# Define the User properties that can be updated
35+
UpdatableField = t.Literal["name", "display_name", "initials", "avatar_url", "color"]
36+
37+
3438
@dataclass
3539
class User:
3640
"""Object representing a User
@@ -188,6 +192,14 @@ class IdentityProvider(LoggingConfigurable):
188192
help=_i18n("The logout handler class to use."),
189193
)
190194

195+
# Define the fields that can be updated
196+
updatable_fields = List(
197+
trait=Unicode(),
198+
default_value=["color"], # Default updatable field
199+
config=True,
200+
help=_i18n("List of fields in the User model that can be updated."),
201+
)
202+
191203
token_generated = False
192204

193205
@default("token")
@@ -207,6 +219,17 @@ def _token_default(self):
207219
self.token_generated = True
208220
return binascii.hexlify(os.urandom(24)).decode("ascii")
209221

222+
@validate("updatable_fields")
223+
def _validate_updatable_fields(self, proposal):
224+
"""Validate that all fields in updatable_fields are valid."""
225+
valid_updatable_fields = list(t.get_args(UpdatableField))
226+
invalid_fields = [
227+
field for field in proposal["value"] if field not in valid_updatable_fields
228+
]
229+
if invalid_fields:
230+
raise TraitError(f"Invalid fields in updatable_fields: {invalid_fields}")
231+
return proposal["value"]
232+
210233
need_token: bool | Bool[bool, t.Union[bool, int]] = Bool(True)
211234

212235
def get_user(self, handler: web.RequestHandler) -> User | None | t.Awaitable[User | None]:
@@ -269,6 +292,25 @@ async def _get_user(self, handler: web.RequestHandler) -> User | None:
269292

270293
return user
271294

295+
def update_user(
296+
self, handler: web.RequestHandler, user_data: dict[UpdatableField, str]
297+
) -> User:
298+
"""Update user information."""
299+
current_user = handler.current_user # type:ignore[attr-defined]
300+
301+
for field in user_data:
302+
if field not in self.updatable_fields:
303+
raise ValueError(f"Field {field} is not updatable")
304+
305+
# Update fields
306+
for field in self.updatable_fields:
307+
if field in user_data:
308+
setattr(current_user, field, user_data[field])
309+
310+
# Persist changes (if applicable)
311+
self.set_login_cookie(handler, current_user) # Save updated user to cookie/session
312+
return current_user
313+
272314
def identity_model(self, user: User) -> dict[str, t.Any]:
273315
"""Return a User as an Identity model"""
274316
# TODO: validate?
@@ -617,6 +659,16 @@ class PasswordIdentityProvider(IdentityProvider):
617659
def _need_token_default(self):
618660
return not bool(self.hashed_password)
619661

662+
@default("updatable_fields")
663+
def _default_updatable_fields(self):
664+
return [
665+
"name",
666+
"display_name",
667+
"initials",
668+
"avatar_url",
669+
"color",
670+
]
671+
620672
@property
621673
def login_available(self) -> bool:
622674
"""Whether a LoginHandler is needed - and therefore whether the login page should be displayed."""

jupyter_server/services/api/handlers.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from jupyter_server._tz import isoformat, utcfromtimestamp
1313
from jupyter_server.auth.decorator import authorized
14+
from jupyter_server.auth.identity import IdentityProvider
1415

1516
from ...base.handlers import APIHandler, JupyterHandler
1617

@@ -70,7 +71,7 @@ async def get(self):
7071

7172

7273
class IdentityHandler(APIHandler):
73-
"""Get the current user's identity model"""
74+
"""Get or patch the current user's identity model"""
7475

7576
@web.authenticated
7677
async def get(self):
@@ -110,9 +111,30 @@ async def get(self):
110111
model = {
111112
"identity": identity,
112113
"permissions": permissions,
114+
"updatable_fields": self.identity_provider.updatable_fields,
113115
}
114116
self.write(json.dumps(model))
115117

118+
@web.authenticated
119+
async def patch(self):
120+
"""Update user information."""
121+
user_data = self.get_json_body()
122+
if not user_data:
123+
raise web.HTTPError(400, "Invalid or missing JSON body")
124+
125+
# Update user information
126+
identity_provider = self.settings["identity_provider"]
127+
if not isinstance(identity_provider, IdentityProvider):
128+
raise web.HTTPError(500, "Identity provider not configured properly")
129+
130+
try:
131+
updated_user = identity_provider.update_user(self, user_data)
132+
self.write(
133+
{"status": "success", "user": identity_provider.identity_model(updated_user)}
134+
)
135+
except ValueError as e:
136+
raise web.HTTPError(400, str(e)) from e
137+
116138

117139
default_handlers = [
118140
(r"/api/spec.yaml", APISpecHandler),

0 commit comments

Comments
 (0)