def set_auth_info_from_token(token): """Set the authentication by providing a TimeTagger webtoken.""" payload_base64 = token.split(".")[1].replace("_", "/") auth_info = JSON.parse( window.decodeURIComponent(window.escape(window.atob(payload_base64)))) auth_info.token = token localStorage.setItem("timetagger_auth_info", JSON.stringify(auth_info))
async def _pull(self, authtoken): # Fetch and wait for response url = tools.build_api_url("updates?since=" + self._server_time) init = dict(method="GET", headers={"authtoken": authtoken}) try: res = await window.fetch(url, init) except Exception as err: res = dict(status=0, statusText=str(err), text=lambda: "") self._pull_statuses.append(res.status) self._pull_statuses = self._pull_statuses[-5:] # Process response if res.status != 200: text = await res.text() console.warn(res.status + " (" + res.statusText + ") " + text) self._set_state("error") # E.g. Wifi or server down, or 500 if res.status == 401: # Our token is probably expired. There may be local # changes that have not yet been pushed, which would # be lost if we logout. On the other hand, this may be # a lost/stolen device and the user revoked the token. # We can distinguish between these cases by determining # that the 401 is due to a token seed mismatch (revoked). self._auth_cantuse = text if "revoked" in text: window.location.href = "../logout" else: ob = JSON.parse(await res.text()) if ob.server_time: self._log_load("server", ob) # Reset? if ob.reset: await self._clear_cache() self.reset() self._server_time = ob.server_time # The odds of something going wrong here are tiny ... # but if they happen, we're out of sync with the server :( try: self.settings._put_received(*ob.settings) except Exception as err: self._set_state("warning") console.error(err) window.alert( "Sync error (settings), see dev console for details.") try: self.records._put_received(*ob.records) except Exception as err: self._set_state("warning") console.error(err) window.alert( "Sync error (records), see dev console for details.") # Set state to ok if we got new items, and if there were no errors if ob.settings or ob.records: if self.state != "warning": self._set_state("ok")
async def _pull(self, authtoken): # Build url url = location.protocol + "//" + location.hostname + ":" + location.port url = url.rstrip(":") + "/api/v1/updates?since=" + self._server_time # Fetch and wait for response init = dict(method="GET", headers={"authtoken": authtoken}) try: res = await window.fetch(url, init) except Exception as err: res = dict(status=0, statusText=str(err), text=lambda: "") self._pull_statuses.append(res.status) self._pull_statuses = self._pull_statuses[-5:] # Process response if res.status != 200: console.warn(res.status + " (" + res.statusText + ") " + await res.text()) self._set_state("error") # E.g. Wifi or server down, or 500 if res.status == 403: if self._pull_statuses[-2] != 403 and self._pull_statuses[ -3] != 403: # We do have internet, but token is invalid: renew and sync sooner await window.auth.renew_maybe() self.sync_soon(4) else: ob = JSON.parse(await res.text()) if ob.server_time: self._log_load("server", ob) # Reset? if ob.reset: await self._clear_cache() self.reset() self._server_time = ob.server_time # The odds of something going wrong here are tiny ... # but if they happen, we're out of sync with the server :( try: self.settings._put_received(*ob.settings) except Exception as err: self._set_state("warning") console.error(err) window.alert( "Sync error (settings), see dev console for details.") try: self.records._put_received(*ob.records) except Exception as err: self._set_state("warning") console.error(err) window.alert( "Sync error (records), see dev console for details.") # Set state to ok if we got new items, and if there were no errors if ob.settings or ob.records: if self.state != "warning": self._set_state("ok")
def get_auth_info(): """Get the authentication info or None.""" x = localStorage.getItem("timetagger_auth_info") if x: try: return JSON.parse(x) except Exception as err: console.warn("Cannot parse JSON auth info: " + str(err)) return None else: return None
def _load_settings(self): x = localStorage.getItem("timetagger_local_settings") if x: try: return JSON.parse(x) except Exception as err: window.console.warn("Cannot parse local settings JSON: " + str(err)) return {} else: return {}
async def _push(self, kind, authtoken): # Build url url = location.protocol + "//" + location.hostname + ":" + location.port url = url.rstrip(":") + "/api/v1/" + kind # Take items, only proceed if nonempty items = self._to_push[kind] if len(items.keys()) == 0: return self._to_push[kind] = {} # Fetch and wait for response init = dict( method="PUT", body=JSON.stringify(items.values()), headers={"authtoken": authtoken}, ) try: res = await window.fetch(url, init) except Exception as err: res = dict(status=0, statusText=str(err), text=lambda: "") # Process response if res.status != 200: # The server was not able to process the request, maybe the # wifi is down or the server is restarting. # Put items back, but don't overwrite if the item was updated again. for key, item in items.items(): self._to_push[kind].setdefault(key, item) self._set_state( "error") # is usually less bad than the fail's below text = await res.text() console.warn(res.status + " (" + res.statusText + ") " + text) # Also notify the user for 402 errors if res.status == 402 and window.canvas: window.canvas.notify_once(text) else: # Success, but it can still mean that some records failed. In this # case these records are likely corrupt, so we delete them, and # will get the server's version back when we pull. d = JSON.parse(await res.text()) # d.accepted -> list of ok keys for key in d.fail: self[kind]._drop(key) for err in d.errors: self._set_state("warning") console.warn(f"Server dropped a {kind}: {err}")
async def renew_webtoken(verbose=True, reset=False): """Renew the webtoken. Each webtoken expires after 14 days. But while valid, it can be exhcanged for a new one. By doing this while the app is active, users won't be logged out unless this device does not use the app for 14 days. If reset is True, the token seed is reset, causing all issued web tokens to become invalid. In other words: all sessions on other devices will be logged out. """ # Get current auth info auth = get_auth_info() if not auth: if verbose: console.warn("Could not renew token - not logged in") return # Make request and wait for response url = build_api_url("webtoken") if reset: url += "?reset=1" init = dict(method="GET", headers={"authtoken": auth.token}) res = await fetch(url, init) # Handle if res.status != 200: text = await res.text() console.warn("Could not renew token: " + text) if res.status == 401 and "revoked" in text: # When revoked, we logout to drop local changes. # See notes in stores.py where we do the same. if "/app/" in location.pathname: location.href = "../logout" else: location.href = "./logout" return # Are we still logged in. User may have logged out in the mean time. auth = get_auth_info() if not auth: return # Apply d = JSON.parse(await res.text()) set_auth_info_from_token(d.token) if verbose: console.warn("webtoken renewed")
def __init__(self, app_name, id, ws_url=None): self.app = None # the root component (can be a PyComponent) self.app_name = app_name self.id = id self.status = 1 self.ws_url = ws_url self._component_counter = 0 self._disposed_ob = {'_disposed': True} # Maybe this is JLab if not self.id: jconfig = window.document.getElementById('jupyter-config-data') if jconfig: try: config = JSON.parse(jconfig.innerText) self.id = config.flexx_session_id self.app_name = config.flexx_app_name except Exception as err: print(err) # Init internal variables self._init_time = time() self._pending_commands = [] # to pend raw commands during init self._asset_count = 0 self._ws = None self.last_msg = None # self.classes = {} self.instances = {} self.instances_to_check_size = {} if not window.flexx.is_exported: self.init_socket() # Initiate service to track resize # Note that only toplevel widgets are tracked, and only once per sec window.addEventListener('resize', self._check_size_of_objects, False) window.setInterval(self._check_size_of_objects, 1000)
def __init__(self, app_name, id, ws_url=None): self.app = None # the root component (can be a PyComponent) self.app_name = app_name self.id = id self.status = 1 self.ws_url = ws_url self._component_counter = 0 self._disposed_ob = {'_disposed': True} # Maybe this is JLab if not self.id: jconfig = window.document.getElementById('jupyter-config-data') if jconfig: try: config = JSON.parse(jconfig.innerText) self.id = config.flexx_session_id self.app_name = config.flexx_app_name except Exception as err: print(err) # Init internal variables self._init_time = time() self._pending_commands = [] # to pend raw commands during init self._asset_count = 0 self._ws = None self.last_msg = None # self.classes = {} self.instances = {} self.instances_to_check_size = {} if not window.flexx.is_exported: self.init_socket() # Initiate service to track resize # Note that only toplevel widgets are tracked, and only once per sec window.addEventListener('resize', self._check_size_of_objects, False) window.setInterval(self._check_size_of_objects, 1000)