diff --git a/etc/run.sh b/etc/run.sh index bf2bb67d48..bfb1e8940a 100644 --- a/etc/run.sh +++ b/etc/run.sh @@ -26,7 +26,6 @@ _env_mail_smtp() { _env_moderate() { declare sim_type=$1 export SIREPO_FEATURE_CONFIG_MODERATED_SIM_TYPES=$sim_type - export SIREPO_AUTH_ROLE_MODERATION_MODERATOR_EMAIL=$USER+moderator@localhost.localdomain _msg "Moderated sim_type=$sim_type" _setup_smtp _env_common diff --git a/sirepo/api_auth.py b/sirepo/api_auth.py index ab484d77d9..d8f83f8dbf 100644 --- a/sirepo/api_auth.py +++ b/sirepo/api_auth.py @@ -32,12 +32,15 @@ def check_api_call(qcall, func): a.ALLOW_SIM_TYPELESS_REQUIRE_EMAIL_USER, a.REQUIRE_COOKIE_SENTINEL, a.REQUIRE_USER, + a.REQUIRE_PLAN, a.REQUIRE_ADM, a.REQUIRE_PREMIUM, ): if not qcall.cookie.has_sentinel(): raise sirepo.util.SRException("missingCookies", None) - if expect == a.REQUIRE_USER: + if expect == a.REQUIRE_PLAN: + qcall.auth.require_plan() + elif expect == a.REQUIRE_USER: qcall.auth.require_user() elif expect == a.ALLOW_SIM_TYPELESS_REQUIRE_EMAIL_USER: qcall.auth.require_email_user() diff --git a/sirepo/api_perm.py b/sirepo/api_perm.py index 1c28eb750b..36f6f3af96 100644 --- a/sirepo/api_perm.py +++ b/sirepo/api_perm.py @@ -32,7 +32,9 @@ class APIPerm(aenum.Flag): REQUIRE_USER = aenum.auto() #: only usable on internal test systems INTERNAL_TEST = aenum.auto() - #: a user with a a premium subscription is required + #: a user with an active plan (any type) is required + REQUIRE_PLAN = aenum.auto() + #: a user with a premium plan is required REQUIRE_PREMIUM = aenum.auto() diff --git a/sirepo/auth/__init__.py b/sirepo/auth/__init__.py index ffb7877963..dbbbb810b6 100644 --- a/sirepo/auth/__init__.py +++ b/sirepo/auth/__init__.py @@ -48,7 +48,7 @@ _GUEST_USER_DISPLAY_NAME = "Guest User" _PAYMENT_PLAN_BASIC = "basic" -_PAYMENT_PLAN_PREMIUM = sirepo.auth_role.ROLE_PAYMENT_PLAN_PREMIUM +_PAYMENT_PLAN_PREMIUM = sirepo.auth_role.ROLE_PLAN_PREMIUM _ALL_PAYMENT_PLANS = ( _PAYMENT_PLAN_BASIC, _PAYMENT_PLAN_PREMIUM, @@ -172,11 +172,9 @@ def check_sim_type_role(self, sim_type, force_sim_type_required_for_api=False): return u = self.logged_in_user() r = sirepo.auth_role.for_sim_type(t) - if self.qcall.auth_db.model("UserRole").has_role( - role=r - ) and not self.qcall.auth_db.model("UserRole").is_expired(role=r): + if self.qcall.auth_db.model("UserRole").has_active_role(role=r): return - elif r in sirepo.auth_role.for_proprietary_oauth_sim_types(): + if r in sirepo.auth_role.for_proprietary_oauth_sim_types(): oauth.raise_authorize_redirect(self.qcall, sirepo.auth_role.sim_type(r)) if r in sirepo.auth_role.for_moderated_sim_types(): auth_role_moderation.raise_control_for_user(self.qcall, u, r) @@ -184,7 +182,7 @@ def check_sim_type_role(self, sim_type, force_sim_type_required_for_api=False): def _assert_role_user(self): u = self.logged_in_user() - if not self.qcall.auth_db.model("UserRole").has_role( + if not self.qcall.auth_db.model("UserRole").has_active_role( role=sirepo.auth_role.ROLE_USER, ): raise sirepo.util.Forbidden( @@ -274,8 +272,8 @@ def is_logged_in(self, state=None): return s in (_STATE_COMPLETE_REGISTRATION, _STATE_LOGGED_IN) def is_premium_user(self): - return self.qcall.auth_db.model("UserRole").has_role( - role=sirepo.auth_role.ROLE_PAYMENT_PLAN_PREMIUM, + return self.qcall.auth_db.model("UserRole").has_active_role( + role=sirepo.auth_role.ROLE_PLAN_PREMIUM, ) def logged_in_user(self, check_path=True): @@ -493,7 +491,7 @@ def parse_display_name(self, value): def require_adm(self): u = self.require_user() - if not self.qcall.auth_db.model("UserRole").has_role( + if not self.qcall.auth_db.model("UserRole").has_active_role( role=sirepo.auth_role.ROLE_ADM, ): raise sirepo.util.Forbidden( @@ -515,9 +513,20 @@ def require_email_user(self): if m != METHOD_EMAIL: raise sirepo.util.Forbidden(f"method={m} is not email for uid={i}") + def require_plan(self): + from sirepo import auth_role_moderation + + u = self.require_user() + for r in sirepo.auth_role.PLAN_ROLES: + if self.qcall.auth_db.model("UserRole").has_active_role(r): + return + auth_role_moderation.raise_control_for_user( + self.qcall, u, sirepo.auth_role.ROLE_PLAN_TRIAL + ) + def require_premium(self): if not self.is_premium_user(): - raise sirepo.util.Forbidden(f"not premium user") + raise sirepo.util.Forbidden("not premium user") def require_user(self): """Asserts whether user is logged in @@ -763,7 +772,7 @@ def _method_user_model(self, module, uid): def _plan(self, data): r = data.roles - if sirepo.auth_role.ROLE_PAYMENT_PLAN_PREMIUM in r: + if sirepo.auth_role.ROLE_PLAN_PREMIUM in r: data.paymentPlan = _PAYMENT_PLAN_PREMIUM data.upgradeToPlan = None else: diff --git a/sirepo/auth_db/__init__.py b/sirepo/auth_db/__init__.py index 85ad2dec52..1547573755 100644 --- a/sirepo/auth_db/__init__.py +++ b/sirepo/auth_db/__init__.py @@ -188,8 +188,11 @@ def execute(self, statement): statement.execution_options(synchronize_session="fetch") ) - def execute_sql(self, text): - return self.execute(sqlalchemy.text(text + ";")) + def execute_sql(self, text, params=None): + q = sqlalchemy.text(text + ";") + if params: + q = q.bindparams(**params) + return self.execute(q) def metadata(self): return UserDbBase.metadata diff --git a/sirepo/auth_db/user.py b/sirepo/auth_db/user.py index 15c16b357c..f67fd7d328 100644 --- a/sirepo/auth_db/user.py +++ b/sirepo/auth_db/user.py @@ -36,9 +36,9 @@ def add_roles(self, roles, expiration=None): u = self.logged_in_user() for r in roles: try: - # Check here, because sqlite doesn't through IntegrityErrors + # Check here, because sqlite doesn't throw IntegrityErrors # at the point of the new() operation. - if not self.has_role(r, uid=u): + if not self._has_role(r, uid=u): self.new(uid=u, role=r, expiration=expiration).save() except sqlalchemy.exc.IntegrityError: # role already exists @@ -46,7 +46,7 @@ def add_roles(self, roles, expiration=None): sim_data.audit_proprietary_lib_files(qcall=self.auth_db.qcall) def add_role_or_update_expiration(self, role, expiration): - if not self.has_role(role): + if not self._has_role(role): self.add_roles(roles=[role], expiration=expiration) return r = self.search_by(uid=self.logged_in_user(), role=role) @@ -71,22 +71,22 @@ def delete_roles(self, roles, uid=None): def get_roles(self): return self.search_all_for_column("role", uid=self.logged_in_user()) - def has_role(self, role, uid=None): - return bool( - self.unchecked_search_by(uid=uid or self.logged_in_user(), role=role) - ) + def get_roles_and_expiration(self): + return [ + PKDict(role=r.role, expiration=r.expiration) + for r in self.query().filter_by(uid=self.logged_in_user()) + ] - def is_expired(self, role): - u = self.logged_in_user() - assert self.has_role(role=role), f"No role for uid={u} and role={role}" - r = self.search_by(uid=u, role=role) - if not r.expiration: - # Roles with no expiration can't expire - return False - return r.expiration < sirepo.srtime.utc_now() + def has_active_role(self, role, uid=None): + r = self._has_role(role, uid=uid) + return r and not self._is_expired_role(r) + + def has_expired_role(self, role): + r = self._has_role(role) + return r and self._is_expired_role(r) def uids_of_paid_users(self): - return self.uids_with_roles(sirepo.auth_role.PAID_USER_ROLES) + return self.uids_with_roles(sirepo.auth_role.PLAN_ROLES_PAID) def uids_with_roles(self, roles): a = sirepo.auth_role.get_all() @@ -103,6 +103,14 @@ def uids_with_roles(self, roles): .all() ] + def _has_role(self, role, uid=None): + return self.unchecked_search_by(uid=uid or self.logged_in_user(), role=role) + + def _is_expired_role(self, role_record): + return ( + role_record.expiration and role_record.expiration < sirepo.srtime.utc_now() + ) + class UserRoleModeration(sirepo.auth_db.UserDbBase): __tablename__ = "user_role_moderation_t" diff --git a/sirepo/auth_role.py b/sirepo/auth_role.py index 5750e2f7d3..a3166a9746 100644 --- a/sirepo/auth_role.py +++ b/sirepo/auth_role.py @@ -10,9 +10,11 @@ import sirepo.feature_config ROLE_ADM = "adm" +ROLE_PLAN_PREMIUM = "premium" +ROLE_PLAN_TRIAL = "trial" ROLE_USER = "user" -ROLE_PAYMENT_PLAN_PREMIUM = "premium" -PAID_USER_ROLES = (ROLE_PAYMENT_PLAN_PREMIUM,) +PLAN_ROLES_PAID = frozenset((ROLE_PLAN_PREMIUM,)) +PLAN_ROLES = PLAN_ROLES_PAID.union((ROLE_PLAN_TRIAL,)) _SIM_TYPE_ROLE_PREFIX = "sim_type_" @@ -58,7 +60,8 @@ def get_all(): for_sim_type(t) for t in sirepo.feature_config.auth_controlled_sim_types() ] + [ ROLE_ADM, - ROLE_PAYMENT_PLAN_PREMIUM, + ROLE_PLAN_PREMIUM, + ROLE_PLAN_TRIAL, ROLE_USER, ] diff --git a/sirepo/auth_role_moderation.py b/sirepo/auth_role_moderation.py index f10399dfdd..77e0447ab6 100644 --- a/sirepo/auth_role_moderation.py +++ b/sirepo/auth_role_moderation.py @@ -1,22 +1,25 @@ -# -*- coding: utf-8 -*- """Moderate user roles :copyright: Copyright (c) 2022 RadiaSoft LLC. All Rights Reserved. :license: http://www.apache.org/licenses/LICENSE-2.0.html """ + from pykern import pkconfig -from pykern.pkdebug import pkdexc, pkdp, pkdlog -from pykern.pkcollections import PKDict from pykern import pkjinja +from pykern.pkcollections import PKDict +from pykern.pkdebug import pkdexc, pkdp, pkdlog import datetime -import sirepo.quest +import getpass import sirepo.auth_role +import sirepo.const import sirepo.feature_config +import sirepo.quest import sirepo.simulation_db import sirepo.smtp import sirepo.uri import sirepo.uri_router import sqlalchemy +import sqlalchemy.exc _STATUS_TO_SUBJECT = PKDict( approve="Access Request Approved", @@ -42,14 +45,38 @@ class API(sirepo.quest.API): "require_adm", uid="Str", role="Str", status="AuthModerationStatus" ) async def api_admModerate(self): + def _role_info(role, status): + res = PKDict( + role=role, + status=status, + moderator_uid=self.auth.logged_in_user(), + product_name=sirepo.simulation_db.SCHEMA_COMMON.productInfo.shortName, + ) + if role == sirepo.auth_role.ROLE_PLAN_TRIAL: + return res.pkupdate( + role_display_name="Trial", + expiration=datetime.datetime.utcnow() + + datetime.timedelta( + days=sirepo.feature_config.cfg().trial_expiration_days + ), + additional_text=f"Your trial is active for {sirepo.feature_config.cfg().trial_expiration_days} days.", + ) + return res.pkupdate( + role_display_name=sirepo.simulation_db.SCHEMA_COMMON.appInfo[ + sirepo.auth_role.sim_type(role) + ].longName + ) + def _send_moderation_status_email(info): sirepo.smtp.send( recipient=info.user_name, - subject=f"Sirepo {info.app_name}: {_STATUS_TO_SUBJECT[info.status]}", + subject=f"Sirepo {info.role_display_name}: {_STATUS_TO_SUBJECT[info.status]}", body=pkjinja.render_resource( f"auth_role_moderation/{info.status}_email", PKDict( - app_name=info.app_name, + additional_text=info.get("additional_text"), + product_name=info.product_name, + role_display_name=info.role_display_name, display_name=info.display_name, link=self.absolute_uri( self.uri_for_app_root( @@ -62,7 +89,9 @@ def _send_moderation_status_email(info): def _set_moderation_status(info): if info.status == "approve": - self.auth_db.model("UserRole").add_roles(roles=[info.role]) + self.auth_db.model("UserRole").add_roles( + roles=[info.role], expiration=info.get("expiration") + ) self.auth_db.model("UserRoleModeration").set_status( role=info.role, status=info.status, @@ -87,14 +116,7 @@ def _set_moderation_status(info): req.req_data.uid, req.req_data.role, ) - p = PKDict( - app_name=sirepo.simulation_db.SCHEMA_COMMON.appInfo[ - sirepo.auth_role.sim_type(i.role) - ].longName, - role=i.role, - status=req.req_data.status, - moderator_uid=self.auth.logged_in_user(), - ) + p = _role_info(i.role, req.req_data.status) pkdlog("status={} uid={} role={}", p.status, i.uid, i.role) # Force METHOD_EMAIL. We are sending them an email so we will # need an email for them. We only have emails for METHOD_EMAIL @@ -153,9 +175,18 @@ def _send_request_email(info): req = self.parse_post() u = self.auth.logged_in_user() - r = sirepo.auth_role.for_sim_type(req.type) - if self.auth_db.model("UserRole").has_role(role=r): + r = sirepo.auth_role.ROLE_PLAN_TRIAL + # SECURITY: type has been validated to be a known sim type. If it is + # not in moderated_sim_types then we know the user is just requesting + # access to start using Sirepo (ROLE_PLAN_TRIAL). + if req.type in sirepo.feature_config.cfg().moderated_sim_types: + r = sirepo.auth_role.for_sim_type(req.type) + if self.auth_db.model("UserRole").has_active_role(role=r): raise sirepo.util.Redirect(sirepo.uri.local_route(req.type)) + if self.auth_db.model("UserRole").has_expired_role(role=r): + raise AssertionError( + f"uid={u} trying to request moderation for expired role={r}" + ) try: self.auth_db.model( "UserRoleModeration", @@ -183,7 +214,7 @@ def _send_request_email(info): email_addr=self.auth.logged_in_user_name(), link=l, reason=req.req_data.reason, - role=sirepo.auth_role.for_sim_type(req.type), + role=r, sim_type=req.type, uid=u, ).pkupdate(self.user_agent_headers()) @@ -200,6 +231,10 @@ def _datetime_to_str(rows): def raise_control_for_user(qcall, uid, role): + if qcall.auth_db.model("UserRole").has_expired_role(role): + if role == sirepo.auth_role.ROLE_PLAN_TRIAL: + raise sirepo.util.PlanExpired(f"uid={uid} role={role} expired") + raise sirepo.util.Forbidden(f"uid={uid} role={role} expired") s = qcall.auth_db.model("UserRoleModeration").get_status(uid=uid, role=role) if s in _ACTIVE: raise sirepo.util.SRException("moderationPending", None) @@ -207,15 +242,17 @@ def raise_control_for_user(qcall, uid, role): raise sirepo.util.Forbidden(f"uid={uid} role={role} already denied") assert s is None, f"Unexpected status={s} for uid={uid} and role={role}" qcall.auth.require_email_user() - raise sirepo.util.SRException("moderationRequest", None) + raise sirepo.util.SRException("moderationRequest", PKDict(role=role)) def init_apis(*args, **kwargs): global _cfg _cfg = pkconfig.init( - moderator_email=pkconfig.Required( - str, "The email address to send moderation emails to" + moderator_email=pkconfig.RequiredUnlessDev( + f"{getpass.getuser()}+moderator@{sirepo.const.LOCALHOST_FQDN}", + str, + "The email address to send moderation emails to", ), ) x = frozenset(_STATUS_TO_SUBJECT.keys()) diff --git a/sirepo/const.py b/sirepo/const.py index 54c362dc4d..afde1f433b 100644 --- a/sirepo/const.py +++ b/sirepo/const.py @@ -16,6 +16,8 @@ #: where template resources and template non-sim user files live LIB_DIR = "lib" +LOCALHOST_FQDN = "localhost.localdomain" + # matches requirements for uid and isn't actually put in the db MOCK_UID = "someuser" diff --git a/sirepo/db_upgrade.py b/sirepo/db_upgrade.py index a7e3917b00..8d1fbae76d 100644 --- a/sirepo/db_upgrade.py +++ b/sirepo/db_upgrade.py @@ -8,9 +8,11 @@ from pykern.pkcollections import PKDict from pykern.pkdebug import pkdp, pkdlog, pkdexc import contextlib +import datetime import shutil import sirepo.auth_db import sirepo.auth_role +import sirepo.feature_config import sirepo.file_lock import sirepo.job import sirepo.quest @@ -97,8 +99,23 @@ def _20240524_add_role_user(qcall): ) qcall.auth_db.drop_table("user_role_invite_t") qcall.auth_db.execute_sql( - f"INSERT INTO user_role_t (uid, role, expiration)" - + f'SELECT uid, "{sirepo.auth_role.ROLE_USER}", NULL from user_registration_t' + f"""INSERT INTO user_role_t (uid, role, expiration) + SELECT uid, '{sirepo.auth_role.ROLE_USER}', NULL from user_registration_t""" + ) + + +def _20250114_add_role_plan_trial(qcall): + """Give all existing users a trial plan with expiration""" + qcall.auth_db.execute_sql( + """INSERT INTO user_role_t (uid, role, expiration) + SELECT uid, :role, :expiration FROM user_registration_t""", + PKDict( + role=sirepo.auth_role.ROLE_PLAN_TRIAL, + expiration=datetime.datetime.utcnow() + + datetime.timedelta( + days=sirepo.feature_config.cfg().trial_expiration_days + ), + ), ) diff --git a/sirepo/feature_config.py b/sirepo/feature_config.py index 8d1bd4f9f6..7c064d84e7 100644 --- a/sirepo/feature_config.py +++ b/sirepo/feature_config.py @@ -216,6 +216,11 @@ def _test(msg): show_open_shadow=_test('Show "Open as a New Shadow Simulation" menu item'), show_rsopt_ml=_test('Show "Export ML Script" menu item'), ), + trial_expiration_days=( + 30, + pkconfig.parse_positive_int, + "number of days a sirepo trial is active", + ), trust_sh_env=( False, bool, diff --git a/sirepo/job_api.py b/sirepo/job_api.py index f103f3441e..4728438901 100644 --- a/sirepo/job_api.py +++ b/sirepo/job_api.py @@ -54,12 +54,12 @@ async def api_admJobs(self): _request_content=self._parse_post_just_data(), ) - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_plan") async def api_analysisJob(self): # TODO(robnagler): computeJobHash has to be checked return await self._request_api() - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_plan") async def api_beginSession(self): """Starts beginSession request asynchronously @@ -72,7 +72,7 @@ async def api_beginSession(self): ) @sirepo.quest.Spec( - "require_user", + "require_plan", sid="SimId", model="AnalysisModel", frame="DataFileIndex", @@ -87,7 +87,7 @@ async def api_downloadDataFile( ) @sirepo.quest.Spec( - "require_user", + "require_plan", sid="SimId", model="AnalysisModel", frame="DataFileIndex", @@ -148,7 +148,7 @@ def _content_too_large(req): f"frame={frame} not found sid={req.id} sim_type={req.type}", ) - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_plan") async def api_globalResources(self): assert ( sirepo.feature_config.cfg().enable_global_resources @@ -184,13 +184,13 @@ async def api_jobSupervisorPing(self): e = "unexpected exception" return PKDict(state="error", error=e) - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_plan") async def api_ownJobs(self): return await self._request_api( _request_content=self._parse_post_just_data(), ) - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_plan") async def api_runCancel(self): try: return await self._request_api() @@ -199,19 +199,19 @@ async def api_runCancel(self): # Always true from the client's perspective return self.reply_dict({"state": "canceled"}) - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_plan") async def api_runSimulation(self): r = self._request_content(PKDict()) if r.isParallel: r.isPremiumUser = self.auth.is_premium_user() return await self._request_api(_request_content=r) - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_plan") async def api_runStatus(self): # runStatus receives models when an animation status if first queried return await self._request_api(_request_content=self._request_content(PKDict())) - @sirepo.quest.Spec("require_premium") + @sirepo.quest.Spec("require_plan") async def api_sbatchLogin(self): r = self._request_content( PKDict(computeJobHash="unused", jobRunMode=sirepo.job.SBATCH), @@ -221,7 +221,7 @@ async def api_sbatchLogin(self): r.pkdel("data") return await self._request_api(_request_content=r) - @sirepo.quest.Spec("require_premium") + @sirepo.quest.Spec("require_plan") async def api_sbatchLoginStatus(self): return await self._request_api( _request_content=self._request_content( @@ -229,7 +229,7 @@ async def api_sbatchLoginStatus(self): ) ) - @sirepo.quest.Spec("require_user", frame_id="SimFrameId") + @sirepo.quest.Spec("require_plan", frame_id="SimFrameId") async def api_simulationFrame(self, frame_id): return await template_common.sim_frame( frame_id, @@ -243,11 +243,11 @@ async def api_simulationFrame(self, frame_id): self, ) - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_plan") async def api_statefulCompute(self): return await self._request_compute(op_key="stful") - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_plan") async def api_statelessCompute(self): return await self._request_compute(op_key="stlss") diff --git a/sirepo/job_driver/docker.py b/sirepo/job_driver/docker.py index 08c302d88c..15281f2690 100644 --- a/sirepo/job_driver/docker.py +++ b/sirepo/job_driver/docker.py @@ -4,7 +4,6 @@ :license: http://www.apache.org/licenses/LICENSE-2.0.html """ -from __future__ import absolute_import, division, print_function from pykern import pkconfig, pkio from pykern.pkcollections import PKDict from pykern.pkdebug import pkdp, pkdlog, pkdexc, pkdc @@ -13,6 +12,7 @@ import io import os import re +import sirepo.const import sirepo.util import subprocess import tornado.ioloop @@ -246,7 +246,7 @@ def _init_dev_hosts(cls): ), "neither cfg.tls_dir and cfg.hosts nor must be set to get auto-config" # dev mode only; see _cfg_tls_dir and _cfg_hosts cls.cfg.tls_dir = srdb.root().join("docker_tls") - cls.cfg.hosts = ("localhost.localdomain",) + cls.cfg.hosts = (sirepo.const.LOCALHOST_FQDN,) d = cls.cfg.tls_dir.join(cls.cfg.hosts[0]) if d.check(dir=True): return diff --git a/sirepo/package_data/auth_role_moderation/approve_email.jinja b/sirepo/package_data/auth_role_moderation/approve_email.jinja index b993859404..09b52945f5 100644 --- a/sirepo/package_data/auth_role_moderation/approve_email.jinja +++ b/sirepo/package_data/auth_role_moderation/approve_email.jinja @@ -1,3 +1,4 @@ -Your request for {{ app_name }} access has been approved. +Your request for Sirepo {{ role_display_name }} access has been approved. {{ link }} +{{ additional_text }} diff --git a/sirepo/package_data/auth_role_moderation/clarify_email.jinja b/sirepo/package_data/auth_role_moderation/clarify_email.jinja index 65d7f66ec2..4ca448a9f1 100644 --- a/sirepo/package_data/auth_role_moderation/clarify_email.jinja +++ b/sirepo/package_data/auth_role_moderation/clarify_email.jinja @@ -1,10 +1,10 @@ Dear {{ display_name }}, -You recently tried to login to {{ app_name }}. We need some more information in order to process your login request. +You recently tried to login to {{ product_name }}. We need some more information in order to process your login request. -Would you please let us know how you heard about {{ app_name }}? +Would you please let us know how you heard about {{ product_name }}? -How do you intend to use {{ app_name }}? +How do you intend to use {{ product_name }}? We need this information to prevent abuse of our servers. @@ -13,4 +13,3 @@ Thank you for your understanding. Sincerely, Sirepo Support - diff --git a/sirepo/package_data/auth_role_moderation/deny_email.jinja b/sirepo/package_data/auth_role_moderation/deny_email.jinja index 7fddfe0a60..f8a44b90ec 100644 --- a/sirepo/package_data/auth_role_moderation/deny_email.jinja +++ b/sirepo/package_data/auth_role_moderation/deny_email.jinja @@ -1 +1 @@ -Your request for {{ app_name }} access has been denied. Community access requires an email address associated with a university, company or research institution. Access is not allowed from countries of particular concern, as designated by the U.S. Department of State, https://www.state.gov/countries-of-particular-concern-special-watch-list-countries-entities-of-particular-concern/ +Your request for {{ product_name }} {{ role_display_name }} access has been denied. Community access requires an email address associated with a university, company or research institution. Access is not allowed from countries of particular concern, as designated by the U.S. Department of State, https://www.state.gov/countries-of-particular-concern-special-watch-list-countries-entities-of-particular-concern/ diff --git a/sirepo/package_data/auth_role_moderation/moderation_email.jinja b/sirepo/package_data/auth_role_moderation/moderation_email.jinja index 8c8a579cb4..39fa75fbea 100644 --- a/sirepo/package_data/auth_role_moderation/moderation_email.jinja +++ b/sirepo/package_data/auth_role_moderation/moderation_email.jinja @@ -1,5 +1,6 @@ Name: {{ display_name }} Simulation Type: {{ sim_type }} +Role: {{ role }} Reason: {{ reason }} Email: {{ email_addr }} DNS: {{ domain_name }} diff --git a/sirepo/package_data/static/html/http-plan-required.html b/sirepo/package_data/static/html/http-plan-required.html new file mode 100644 index 0000000000..c93086ff2a --- /dev/null +++ b/sirepo/package_data/static/html/http-plan-required.html @@ -0,0 +1,8 @@ +
+
+
+

Plan Required

+

An is required to use Sirepo.

+
+
+
diff --git a/sirepo/package_data/static/js/sirepo-components.js b/sirepo/package_data/static/js/sirepo-components.js index 2b5e1be54b..dc0c954782 100644 --- a/sirepo/package_data/static/js/sirepo-components.js +++ b/sirepo/package_data/static/js/sirepo-components.js @@ -398,11 +398,11 @@ SIREPO.app.directive('canceledDueToTimeoutAlert', function(authState) { template: ` `, controller: function($scope, appState) { - $scope.authState = authState; + $scope.upgradeToLink = `please upgrade to a ${authState.upgradeToPlan} plan`; $scope.getTime = function() { return appState.formatTime($scope.simState.getCanceledAfterSecs()); @@ -1791,6 +1791,20 @@ SIREPO.app.directive('pendingLinkToSimulations', function() { }; }); + +SIREPO.app.directive('plansLink', function() { + return { + restrict: 'A', + scope: { + linkText: '@', + }, + template: '{{ linkText }}', + controller: function($scope) { + $scope.plansUrl = SIREPO.APP_SCHEMA.constants.plansUrl; + }, + }; +}); + SIREPO.app.directive('safePath', function() { // keep in sync with sirepo.srschem.py _NAME_ILLEGALS @@ -3576,6 +3590,7 @@ SIREPO.app.directive('emailLogin', function(requestSender, errorService) {
+

Your email address must be associated with a university, company, or research institution.

By signing up for Sirepo you agree to Sirepo's privacy policy and terms and conditions, and to receive informational and marketing communications from RadiaSoft. You may unsubscribe at any time.

@@ -4346,17 +4361,20 @@ SIREPO.app.directive('moderationRequest', function(appState, errorService, panel template: `
- +
Response submitted.
`, - controller: function(requestSender, $scope) { + controller: function(requestSender, $route, $scope) { $scope.data = {}; $scope.submitted = false; $scope.disableSubmit = true; + $scope.moderationRequestReason = { + trial: `To prevent abuse of our systems all new users must supply a reason for requesting access to ${SIREPO.APP_SCHEMA.productInfo.shortName}. In a few sentences please describe how you plan to use ${SIREPO.APP_SCHEMA.productInfo.shortName}` + }[$route.current.params.role] ?? 'Please describe your reason for requesting access'; $scope.submitRequest = function () { const handleResponse = (data) => { if (data.state === 'error') { @@ -4762,11 +4780,7 @@ SIREPO.app.directive('sbatchLoginModal', function() {