Api invitation link (#105)
* feat(api): invitation token to add team members * feat(api): invitation link change password * feat(db): changed base_auth structure * feat(api): invitation link - regenerate/reset * feat(api): invitation link - restore deleted user * feat(api): invitation link for forget password * feat(api): changed email body for invite user and reset password
This commit is contained in:
parent
5b0dc11543
commit
42884550df
14 changed files with 253 additions and 167 deletions
|
|
@ -52,6 +52,8 @@
|
||||||
"S3_HOST": "",
|
"S3_HOST": "",
|
||||||
"S3_KEY": "",
|
"S3_KEY": "",
|
||||||
"S3_SECRET": "",
|
"S3_SECRET": "",
|
||||||
|
"invitation_link": "/api/users/invitation?token=%s",
|
||||||
|
"change_password_link": "/changepassword?invitation=%s&&pass=%s",
|
||||||
"version_number": "1.2.0"
|
"version_number": "1.2.0"
|
||||||
},
|
},
|
||||||
"lambda_timeout": 150,
|
"lambda_timeout": 150,
|
||||||
|
|
|
||||||
0
api/chalicelib/blueprints/app/__init__.py
Normal file
0
api/chalicelib/blueprints/app/__init__.py
Normal file
|
|
@ -509,8 +509,8 @@ def reset_password_handler(step):
|
||||||
if "email" not in data or len(data["email"]) < 5:
|
if "email" not in data or len(data["email"]) < 5:
|
||||||
return {"errors": ["please provide a valid email address"]}
|
return {"errors": ["please provide a valid email address"]}
|
||||||
return reset_password.step1(data)
|
return reset_password.step1(data)
|
||||||
elif step == "2":
|
# elif step == "2":
|
||||||
return reset_password.step2(data)
|
# return reset_password.step2(data)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/{projectId}/metadata', methods=['GET'])
|
@app.route('/{projectId}/metadata', methods=['GET'])
|
||||||
|
|
@ -587,9 +587,8 @@ def async_basic_emails(step):
|
||||||
if data.pop("auth") != environ["async_Token"]:
|
if data.pop("auth") != environ["async_Token"]:
|
||||||
return {}
|
return {}
|
||||||
if step.lower() == "member_invitation":
|
if step.lower() == "member_invitation":
|
||||||
email_helper.send_team_invitation(recipient=data["email"], user_name=data["userName"],
|
email_helper.send_team_invitation(recipient=data["email"], invitation_link=data["invitationLink"],
|
||||||
temp_password=data["tempPassword"], client_id=data["clientId"],
|
client_id=data["clientId"], sender_name=data["senderName"])
|
||||||
sender_name=data["senderName"])
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/{projectId}/sample_rate', methods=['GET'])
|
@app.route('/{projectId}/sample_rate', methods=['GET'])
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,6 @@ def run_scheduled_jobs(event):
|
||||||
jobs.execute_jobs()
|
jobs.execute_jobs()
|
||||||
|
|
||||||
|
|
||||||
@app.schedule(Cron('0/60', '*', '*', '*', '?', '*'))
|
|
||||||
def clear_password_reset(event):
|
|
||||||
reset_password.cron()
|
|
||||||
|
|
||||||
|
|
||||||
# Run every monday.
|
# Run every monday.
|
||||||
@app.schedule(Cron('5', '0', '?', '*', 'MON', '*'))
|
@app.schedule(Cron('5', '0', '?', '*', 'MON', '*'))
|
||||||
def weekly_report2(event):
|
def weekly_report2(event):
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,38 @@ def add_member(context):
|
||||||
return users.create_member(tenant_id=context['tenantId'], user_id=context['userId'], data=data)
|
return users.create_member(tenant_id=context['tenantId'], user_id=context['userId'], data=data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/users/invitation', methods=['GET'], authorizer=None)
|
||||||
|
def process_invitation_link():
|
||||||
|
params = app.current_request.query_params
|
||||||
|
if params is None or len(params.get("token", "")) < 64:
|
||||||
|
return {"errors": ["please provide a valid invitation"]}
|
||||||
|
user = users.get_by_invitation_token(params["token"])
|
||||||
|
if user is None:
|
||||||
|
return {"errors": ["invitation not found"]}
|
||||||
|
if user["expired"]:
|
||||||
|
return {"errors": ["expired invitation, please ask your admin to send a new one"]}
|
||||||
|
pass_token = users.allow_password_change(user_id=user["userId"])
|
||||||
|
return Response(
|
||||||
|
status_code=307,
|
||||||
|
body='',
|
||||||
|
headers={'Location': environ["SITE_URL"] + environ["change_password_link"] % (params["token"], pass_token),
|
||||||
|
'Content-Type': 'text/plain'})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/users/invitation/password', methods=['POST', 'PUT'], authorizer=None)
|
||||||
|
def change_password_by_invitation():
|
||||||
|
data = app.current_request.json_body
|
||||||
|
if data is None or len(data.get("invitation", "")) < 64 or len(data.get("pass", "")) < 8:
|
||||||
|
return {"errors": ["please provide a valid invitation & pass"]}
|
||||||
|
user = users.get_by_invitation_token(token=data["token"], pass_token=data["pass"])
|
||||||
|
if user is None:
|
||||||
|
return {"errors": ["invitation not found"]}
|
||||||
|
if user["expiredChange"]:
|
||||||
|
return {"errors": ["expired change, please re-use the invitation link"]}
|
||||||
|
|
||||||
|
return users.set_password_invitation(new_password=data["password"], user_id=user["userId"])
|
||||||
|
|
||||||
|
|
||||||
@app.route('/client/members/{memberId}', methods=['PUT', 'POST'])
|
@app.route('/client/members/{memberId}', methods=['PUT', 'POST'])
|
||||||
def edit_member(memberId, context):
|
def edit_member(memberId, context):
|
||||||
data = app.current_request.json_body
|
data = app.current_request.json_body
|
||||||
|
|
@ -363,6 +395,11 @@ def edit_member(memberId, context):
|
||||||
user_id_to_update=memberId)
|
user_id_to_update=memberId)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/client/members/{memberId}/reset', methods=['GET'])
|
||||||
|
def reset_reinvite_member(memberId, context):
|
||||||
|
return users.reset_member(tenant_id=context['tenantId'], editor_id=context['userId'], user_id_to_update=memberId)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/client/members/{memberId}', methods=['DELETE'])
|
@app.route('/client/members/{memberId}', methods=['DELETE'])
|
||||||
def delete_member(memberId, context):
|
def delete_member(memberId, context):
|
||||||
return users.delete_member(tenant_id=context["tenantId"], user_id=context['userId'], id_to_delete=memberId)
|
return users.delete_member(tenant_id=context["tenantId"], user_id=context['userId'], id_to_delete=memberId)
|
||||||
|
|
|
||||||
|
|
@ -18,48 +18,23 @@ def step1(data):
|
||||||
a_users = users.get_by_email_only(data["email"])
|
a_users = users.get_by_email_only(data["email"])
|
||||||
if len(a_users) > 1:
|
if len(a_users) > 1:
|
||||||
print(f"multiple users found for [{data['email']}] please contact our support")
|
print(f"multiple users found for [{data['email']}] please contact our support")
|
||||||
return {"errors": ["please contact our support"]}
|
return {"errors": ["multiple users, please contact our support"]}
|
||||||
elif len(a_users) == 1:
|
elif len(a_users) == 1:
|
||||||
a_users = a_users[0]
|
a_users = a_users[0]
|
||||||
reset_token = secrets.token_urlsafe(6)
|
invitation_link=users.generate_new_invitation(user_id=a_users["id"])
|
||||||
users.update(tenant_id=a_users["tenantId"], user_id=a_users["id"],
|
email_helper.send_forgot_password(recipient=data["email"], invitation_link=invitation_link)
|
||||||
changes={"token": reset_token})
|
|
||||||
email_helper.send_reset_code(recipient=data["email"], reset_code=reset_token)
|
|
||||||
else:
|
else:
|
||||||
print(f"invalid email address [{data['email']}]")
|
print(f"invalid email address [{data['email']}]")
|
||||||
return {"errors": ["invalid email address"]}
|
return {"errors": ["invalid email address"]}
|
||||||
return {"data": {"state": "success"}}
|
return {"data": {"state": "success"}}
|
||||||
|
|
||||||
|
|
||||||
def step2(data):
|
# def step2(data):
|
||||||
print("====================== change password 2 ===============")
|
# print("====================== change password 2 ===============")
|
||||||
user = users.get_by_email_reset(data["email"], data["code"])
|
# user = users.get_by_email_reset(data["email"], data["code"])
|
||||||
if not user:
|
# if not user:
|
||||||
print("error: wrong email or reset code")
|
# print("error: wrong email or reset code")
|
||||||
return {"errors": ["wrong email or reset code"]}
|
# return {"errors": ["wrong email or reset code"]}
|
||||||
users.update(tenant_id=user["tenantId"], user_id=user["id"],
|
# users.update(tenant_id=user["tenantId"], user_id=user["id"],
|
||||||
changes={"token": None, "password": data["password"], "generatedPassword": False})
|
# changes={"token": None, "password": data["password"], "generatedPassword": False})
|
||||||
return {"data": {"state": "success"}}
|
# return {"data": {"state": "success"}}
|
||||||
|
|
||||||
|
|
||||||
def cron():
|
|
||||||
with pg_client.PostgresClient() as cur:
|
|
||||||
cur.execute(
|
|
||||||
cur.mogrify("""\
|
|
||||||
SELECT user_id
|
|
||||||
FROM public.basic_authentication
|
|
||||||
WHERE token notnull
|
|
||||||
AND (token_requested_at isnull or (EXTRACT(EPOCH FROM token_requested_at)*1000)::BIGINT < %(time)s);""",
|
|
||||||
{"time": chalicelib.utils.TimeUTC.TimeUTC.now(delta_days=-1)})
|
|
||||||
)
|
|
||||||
results = cur.fetchall()
|
|
||||||
if len(results) == 0:
|
|
||||||
return
|
|
||||||
results = tuple([r["user_id"] for r in results])
|
|
||||||
cur.execute(
|
|
||||||
cur.mogrify("""\
|
|
||||||
UPDATE public.basic_authentication
|
|
||||||
SET token = NULL, token_requested_at = NULL
|
|
||||||
WHERE user_id in %(ids)s;""",
|
|
||||||
{"ids": results})
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,30 @@ from chalicelib.utils.TimeUTC import TimeUTC
|
||||||
from chalicelib.utils.helper import environ
|
from chalicelib.utils.helper import environ
|
||||||
|
|
||||||
from chalicelib.core import tenants
|
from chalicelib.core import tenants
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
|
||||||
def create_new_member(email, password, admin, name, owner=False):
|
def __generate_invitation_token():
|
||||||
|
return secrets.token_urlsafe(64)
|
||||||
|
|
||||||
|
|
||||||
|
def __is_authorized_to_manage_users():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def create_new_member(email, invitation_token, admin, name, owner=False):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
query = cur.mogrify(f"""\
|
query = cur.mogrify(f"""\
|
||||||
WITH u AS (
|
WITH u AS (INSERT INTO public.users (email, role, name, data)
|
||||||
INSERT INTO public.users (email, role, name, data)
|
VALUES (%(email)s, %(role)s, %(name)s, %(data)s)
|
||||||
VALUES (%(email)s, %(role)s, %(name)s, %(data)s)
|
RETURNING user_id,email,role,name,appearance
|
||||||
RETURNING user_id,email,role,name,appearance
|
),
|
||||||
),
|
au AS (INSERT INTO public.basic_authentication (user_id, generated_password, invitation_token, invited_at)
|
||||||
au AS (INSERT
|
VALUES ((SELECT user_id FROM u), TRUE, %(invitation_token)s, timezone('utc'::text, now()))
|
||||||
INTO public.basic_authentication (user_id, password, generated_password)
|
RETURNING invitation_token
|
||||||
VALUES ((SELECT user_id FROM u), crypt(%(password)s, gen_salt('bf', 12)), TRUE))
|
)
|
||||||
SELECT u.user_id AS id,
|
SELECT u.user_id,
|
||||||
|
u.user_id AS id,
|
||||||
u.email,
|
u.email,
|
||||||
u.role,
|
u.role,
|
||||||
u.name,
|
u.name,
|
||||||
|
|
@ -30,18 +40,19 @@ def create_new_member(email, password, admin, name, owner=False):
|
||||||
(CASE WHEN u.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
(CASE WHEN u.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||||
(CASE WHEN u.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
(CASE WHEN u.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||||
(CASE WHEN u.role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
(CASE WHEN u.role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
||||||
u.appearance
|
au.invitation_token
|
||||||
FROM u;""",
|
FROM u,au;""",
|
||||||
{"email": email, "password": password,
|
{"email": email, "password": invitation_token,
|
||||||
"role": "owner" if owner else "admin" if admin else "member", "name": name,
|
"role": "owner" if owner else "admin" if admin else "member", "name": name,
|
||||||
"data": json.dumps({"lastAnnouncementView": TimeUTC.now()})})
|
"data": json.dumps({"lastAnnouncementView": TimeUTC.now()}),
|
||||||
|
"invitation_token": invitation_token})
|
||||||
cur.execute(
|
cur.execute(
|
||||||
query
|
query
|
||||||
)
|
)
|
||||||
return helper.dict_to_camel_case(cur.fetchone())
|
return helper.dict_to_camel_case(cur.fetchone())
|
||||||
|
|
||||||
|
|
||||||
def restore_member(user_id, email, password, admin, name, owner=False):
|
def restore_member(user_id, email, invitation_token, admin, name, owner=False):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
query = cur.mogrify(f"""\
|
query = cur.mogrify(f"""\
|
||||||
UPDATE public.users
|
UPDATE public.users
|
||||||
|
|
@ -58,31 +69,61 @@ def restore_member(user_id, email, password, admin, name, owner=False):
|
||||||
TRUE AS change_password,
|
TRUE AS change_password,
|
||||||
(CASE WHEN role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
(CASE WHEN role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||||
(CASE WHEN role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
(CASE WHEN role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||||
(CASE WHEN role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
(CASE WHEN role = 'member' THEN TRUE ELSE FALSE END) AS member;""",
|
||||||
appearance;""",
|
|
||||||
{"user_id": user_id, "email": email,
|
{"user_id": user_id, "email": email,
|
||||||
"role": "owner" if owner else "admin" if admin else "member", "name": name})
|
"role": "owner" if owner else "admin" if admin else "member", "name": name})
|
||||||
cur.execute(
|
cur.execute(
|
||||||
query
|
query
|
||||||
)
|
)
|
||||||
result = helper.dict_to_camel_case(cur.fetchone())
|
result = cur.fetchone()
|
||||||
query = cur.mogrify("""\
|
query = cur.mogrify("""\
|
||||||
UPDATE public.basic_authentication
|
UPDATE public.basic_authentication
|
||||||
SET password= crypt(%(password)s, gen_salt('bf', 12)),
|
SET generated_password = TRUE,
|
||||||
generated_password= TRUE,
|
invitation_token = %(invitation_token)s,
|
||||||
token=NULL,
|
invited_at = timezone('utc'::text, now()),
|
||||||
token_requested_at=NULL
|
change_pwd_expire_at = NULL,
|
||||||
WHERE user_id=%(user_id)s;""",
|
change_pwd_token = NULL
|
||||||
{"user_id": user_id, "password": password})
|
WHERE user_id=%(user_id)s
|
||||||
|
RETURNING invitation_token;""",
|
||||||
|
{"user_id": user_id, "invitation_token": invitation_token})
|
||||||
cur.execute(
|
cur.execute(
|
||||||
query
|
query
|
||||||
)
|
)
|
||||||
|
result["invitation_token"] = cur.fetchone()["invitation_token"]
|
||||||
|
|
||||||
return result
|
return helper.dict_to_camel_case(result)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_new_invitation(user_id):
|
||||||
|
invitation_token = __generate_invitation_token()
|
||||||
|
with pg_client.PostgresClient() as cur:
|
||||||
|
query = cur.mogrify("""\
|
||||||
|
UPDATE public.basic_authentication
|
||||||
|
SET invitation_token = %(invitation_token)s,
|
||||||
|
invited_at = timezone('utc'::text, now()),
|
||||||
|
change_pwd_expire_at = NULL,
|
||||||
|
change_pwd_token = NULL
|
||||||
|
WHERE user_id=%(user_id)s
|
||||||
|
RETURNING invitation_token;""",
|
||||||
|
{"user_id": user_id, "invitation_token": invitation_token})
|
||||||
|
cur.execute(
|
||||||
|
query
|
||||||
|
)
|
||||||
|
return __get_invitation_link(cur.fetchone().pop("invitation_token"))
|
||||||
|
|
||||||
|
|
||||||
|
def reset_member(tenant_id, editor_id, user_id_to_update):
|
||||||
|
admin = get(tenant_id=tenant_id, user_id=editor_id)
|
||||||
|
if not admin["admin"] and not admin["superAdmin"]:
|
||||||
|
return {"errors": ["unauthorized"]}
|
||||||
|
user = get(tenant_id=tenant_id, user_id=user_id_to_update)
|
||||||
|
if not user:
|
||||||
|
return {"errors": ["user not found"]}
|
||||||
|
return {"data": {"invitationLink": generate_new_invitation(user_id_to_update)}}
|
||||||
|
|
||||||
|
|
||||||
def update(tenant_id, user_id, changes):
|
def update(tenant_id, user_id, changes):
|
||||||
AUTH_KEYS = ["password", "generatedPassword", "token"]
|
AUTH_KEYS = ["password", "generatedPassword", "invitationToken", "invitedAt", "changePwdExpireAt", "changePwdToken"]
|
||||||
if len(changes.keys()) == 0:
|
if len(changes.keys()) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -93,13 +134,6 @@ def update(tenant_id, user_id, changes):
|
||||||
if key == "password":
|
if key == "password":
|
||||||
sub_query_bauth.append("password = crypt(%(password)s, gen_salt('bf', 12))")
|
sub_query_bauth.append("password = crypt(%(password)s, gen_salt('bf', 12))")
|
||||||
sub_query_bauth.append("changed_at = timezone('utc'::text, now())")
|
sub_query_bauth.append("changed_at = timezone('utc'::text, now())")
|
||||||
elif key == "token":
|
|
||||||
if changes[key] is not None:
|
|
||||||
sub_query_bauth.append("token = %(token)s")
|
|
||||||
sub_query_bauth.append("token_requested_at = timezone('utc'::text, now())")
|
|
||||||
else:
|
|
||||||
sub_query_bauth.append("token = NULL")
|
|
||||||
sub_query_bauth.append("token_requested_at = NULL")
|
|
||||||
else:
|
else:
|
||||||
sub_query_bauth.append(f"{helper.key_to_snake_case(key)} = %({key})s")
|
sub_query_bauth.append(f"{helper.key_to_snake_case(key)} = %({key})s")
|
||||||
else:
|
else:
|
||||||
|
|
@ -166,26 +200,43 @@ def create_member(tenant_id, user_id, data):
|
||||||
return {"errors": ["invalid user name"]}
|
return {"errors": ["invalid user name"]}
|
||||||
if name is None:
|
if name is None:
|
||||||
name = data["email"]
|
name = data["email"]
|
||||||
temp_pass = helper.generate_salt()[:8]
|
invitation_token = __generate_invitation_token()
|
||||||
user = get_deleted_user_by_email(email=data["email"])
|
user = get_deleted_user_by_email(email=data["email"])
|
||||||
if user is not None:
|
if user is not None:
|
||||||
new_member = restore_member(email=data["email"], password=temp_pass,
|
new_member = restore_member(email=data["email"], invitation_token=invitation_token,
|
||||||
admin=data.get("admin", False), name=name, user_id=user["userId"])
|
admin=data.get("admin", False), name=name, user_id=user["userId"])
|
||||||
else:
|
else:
|
||||||
new_member = create_new_member(email=data["email"], password=temp_pass,
|
new_member = create_new_member(email=data["email"], invitation_token=invitation_token,
|
||||||
admin=data.get("admin", False), name=name)
|
admin=data.get("admin", False), name=name)
|
||||||
|
new_member["invitationLink"] = __get_invitation_link(new_member.pop("invitationToken"))
|
||||||
helper.async_post(environ['email_basic'] % 'member_invitation',
|
helper.async_post(environ['email_basic'] % 'member_invitation',
|
||||||
{
|
{
|
||||||
"email": data["email"],
|
"email": data["email"],
|
||||||
"userName": data["email"],
|
"invitationLink": new_member["invitationLink"],
|
||||||
"tempPassword": temp_pass,
|
|
||||||
"clientId": tenants.get_by_tenant_id(tenant_id)["name"],
|
"clientId": tenants.get_by_tenant_id(tenant_id)["name"],
|
||||||
"senderName": admin["name"]
|
"senderName": admin["name"]
|
||||||
})
|
})
|
||||||
return {"data": new_member}
|
return {"data": new_member}
|
||||||
|
|
||||||
|
|
||||||
|
def __get_invitation_link(invitation_token):
|
||||||
|
return environ["SITE_URL"] + environ["invitation_link"] % invitation_token
|
||||||
|
|
||||||
|
|
||||||
|
def allow_password_change(user_id, delta_min=10):
|
||||||
|
pass_token = secrets.token_urlsafe(8)
|
||||||
|
with pg_client.PostgresClient() as cur:
|
||||||
|
query = cur.mogrify(f"""UPDATE public.basic_authentication
|
||||||
|
SET change_pwd_expire_at = timezone('utc'::text, now()+INTERVAL '%(delta)s MINUTES'),
|
||||||
|
change_pwd_token = %(pass_token)s
|
||||||
|
WHERE user_id = %(user_id)s""",
|
||||||
|
{"user_id": user_id, "delta": delta_min, "pass_token": pass_token})
|
||||||
|
cur.execute(
|
||||||
|
query
|
||||||
|
)
|
||||||
|
return pass_token
|
||||||
|
|
||||||
|
|
||||||
def get(user_id, tenant_id):
|
def get(user_id, tenant_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|
@ -367,6 +418,15 @@ def change_password(tenant_id, user_id, email, old_password, new_password):
|
||||||
"jwt": authenticate(email, new_password)["jwt"]}
|
"jwt": authenticate(email, new_password)["jwt"]}
|
||||||
|
|
||||||
|
|
||||||
|
def set_password_invitation(user_id, new_password):
|
||||||
|
changes = {"password": new_password, "generatedPassword": False,
|
||||||
|
"invitationToken": None, "invitedAt": None,
|
||||||
|
"changePwdExpireAt": None, "changePwdToken": None}
|
||||||
|
user = update(tenant_id=-1, user_id=user_id, changes=changes)
|
||||||
|
return {"data": user,
|
||||||
|
"jwt": authenticate(user["email"], new_password)["jwt"]}
|
||||||
|
|
||||||
|
|
||||||
def count_members():
|
def count_members():
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute("""SELECT COUNT(user_id)
|
cur.execute("""SELECT COUNT(user_id)
|
||||||
|
|
@ -409,6 +469,23 @@ def get_deleted_user_by_email(email):
|
||||||
return helper.dict_to_camel_case(r)
|
return helper.dict_to_camel_case(r)
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_invitation_token(token, pass_token=None):
|
||||||
|
with pg_client.PostgresClient() as cur:
|
||||||
|
cur.execute(
|
||||||
|
cur.mogrify(
|
||||||
|
f"""SELECT
|
||||||
|
*,
|
||||||
|
DATE_PART('day',timezone('utc'::text, now())- invited_at)>=1 AS expired,
|
||||||
|
change_pwd_expire_at <= timezone('utc'::text, now()) AS expired_change
|
||||||
|
FROM public.users INNER JOIN public.basic_authentication USING(user_id)
|
||||||
|
WHERE invitation_token = %(token)s {"AND change_pwd_token = %(pass_token)s" if pass_token else ""}
|
||||||
|
LIMIT 1;""",
|
||||||
|
{"token": token, "pass_token": token})
|
||||||
|
)
|
||||||
|
r = cur.fetchone()
|
||||||
|
return helper.dict_to_camel_case(r)
|
||||||
|
|
||||||
|
|
||||||
def auth_exists(user_id, tenant_id, jwt_iat, jwt_aud):
|
def auth_exists(user_id, tenant_id, jwt_iat, jwt_aud):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,18 @@ from chalicelib.utils.TimeUTC import TimeUTC
|
||||||
from chalicelib.utils.email_handler import __get_html_from_file, send_html, __escape_text_html
|
from chalicelib.utils.email_handler import __get_html_from_file, send_html, __escape_text_html
|
||||||
|
|
||||||
|
|
||||||
def send_team_invitation(recipient, user_name, temp_password, client_id, sender_name):
|
def send_team_invitation(recipient, client_id, sender_name, invitation_link):
|
||||||
BODY_HTML = __get_html_from_file("chalicelib/utils/html/invitation.html",
|
BODY_HTML = __get_html_from_file("chalicelib/utils/html/invitation.html",
|
||||||
formatting_variables={"userName": __escape_text_html(user_name),
|
formatting_variables={"invitationLink": invitation_link,
|
||||||
"password": temp_password, "clientId": client_id,
|
"clientId": client_id,
|
||||||
"sender": sender_name})
|
"sender": sender_name})
|
||||||
SUBJECT = "Welcome to OpenReplay"
|
SUBJECT = "Welcome to OpenReplay"
|
||||||
send_html(BODY_HTML, SUBJECT, recipient)
|
send_html(BODY_HTML, SUBJECT, recipient)
|
||||||
|
|
||||||
|
|
||||||
def send_reset_code(recipient, reset_code):
|
def send_forgot_password(recipient, invitation_link):
|
||||||
BODY_HTML = __get_html_from_file("chalicelib/utils/html/reset_password.html",
|
BODY_HTML = __get_html_from_file("chalicelib/utils/html/reset_password.html",
|
||||||
formatting_variables={"code": reset_code})
|
formatting_variables={"invitationLink": invitation_link})
|
||||||
SUBJECT = "Password recovery"
|
SUBJECT = "Password recovery"
|
||||||
send_html(BODY_HTML, SUBJECT, recipient)
|
send_html(BODY_HTML, SUBJECT, recipient)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -447,10 +447,10 @@ width: 25%!important
|
||||||
<p style="font-size: 14px; line-height: 16px; text-align: center; margin: 0;">
|
<p style="font-size: 14px; line-height: 16px; text-align: center; margin: 0;">
|
||||||
Please use this link to login:</p>
|
Please use this link to login:</p>
|
||||||
<p style="font-size: 14px; line-height: 21px; text-align: center; margin: 0;">
|
<p style="font-size: 14px; line-height: 21px; text-align: center; margin: 0;">
|
||||||
<span style="font-size: 18px;"><a href="%(frontend_url)s"
|
<span style="font-size: 18px;"><a href="%(invitationLink)s"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
style="text-decoration: underline; color: #009193;"
|
style="text-decoration: underline; color: #009193;"
|
||||||
target="_blank" title="OpenReplay Login">%(frontend_url)s</a></span><span
|
target="_blank" title="OpenReplay Login">%(invitationLink)s</a></span><span
|
||||||
style="font-size: 18px; line-height: 21px;"></span></p>
|
style="font-size: 18px; line-height: 21px;"></span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -485,40 +485,18 @@ width: 25%!important
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif">
|
<td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif">
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div style="color:#555555;font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue','Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
|
|
||||||
<div style="font-size: 12px; line-height: 14px; color: #555555; font-family: -apple-system,BlinkMacSystemFont,'Helvetica Neue','Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif;">
|
|
||||||
<p style="font-size: 14px; line-height: 16px; text-align: center; margin: 0;">
|
|
||||||
Your login credentials</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!--[if mso]></td></tr></table><![endif]-->
|
<!--[if mso]></td></tr></table><![endif]-->
|
||||||
<!--[if mso]>
|
<!--[if mso]>
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif">
|
<td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif">
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div style="color:#555555;font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue','Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
|
|
||||||
<div style="font-size: 12px; line-height: 14px; color: #555555; font-family: -apple-system,BlinkMacSystemFont,'Helvetica Neue','Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif;">
|
|
||||||
<p style="font-size: 14px; line-height: 16px; text-align: center; margin: 0;">
|
|
||||||
<strong>Username / Email</strong></p>
|
|
||||||
<p style="font-size: 14px; line-height: 16px; text-align: center; margin: 0;">
|
|
||||||
<span style="text-decoration: none; color: #009193;">%(userName)s</span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!--[if mso]></td></tr></table><![endif]-->
|
<!--[if mso]></td></tr></table><![endif]-->
|
||||||
<!--[if mso]>
|
<!--[if mso]>
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif">
|
<td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif">
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div style="color:#555555;font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue','Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
|
|
||||||
<div style="font-size: 12px; line-height: 14px; color: #555555; font-family: -apple-system,BlinkMacSystemFont,'Helvetica Neue','Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif;">
|
|
||||||
<p style="font-size: 14px; line-height: 16px; text-align: center; margin: 0;">
|
|
||||||
<strong>Password</strong></p>
|
|
||||||
<p style="font-size: 14px; line-height: 16px; text-align: center; margin: 0;">
|
|
||||||
%(password)s</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!--[if mso]></td></tr></table><![endif]-->
|
<!--[if mso]></td></tr></table><![endif]-->
|
||||||
<table border="0" cellpadding="0" cellspacing="0" class="divider"
|
<table border="0" cellpadding="0" cellspacing="0" class="divider"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
|
|
|
||||||
|
|
@ -452,10 +452,10 @@ width: 25%!important
|
||||||
<div style="color:#555555;font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue','Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
|
<div style="color:#555555;font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue','Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
|
||||||
<div style="font-size: 12px; line-height: 14px; color: #555555; font-family: -apple-system,BlinkMacSystemFont,'Helvetica Neue','Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif;">
|
<div style="font-size: 12px; line-height: 14px; color: #555555; font-family: -apple-system,BlinkMacSystemFont,'Helvetica Neue','Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif;">
|
||||||
<p style="font-size: 14px; line-height: 16px; text-align: center; margin: 0;">
|
<p style="font-size: 14px; line-height: 16px; text-align: center; margin: 0;">
|
||||||
Use the code below to reset your password (valid for 24 hours only):</p>
|
Use the link below to reset your password (valid for 24 hours only):</p>
|
||||||
<p style="font-size: 14px; line-height: 21px; text-align: center; margin: 0;">
|
<p style="font-size: 14px; line-height: 21px; text-align: center; margin: 0;">
|
||||||
<br/>
|
<br/>
|
||||||
<span style="font-size: 18px;"><b>%(code)s</b></span><span
|
<span style="font-size: 18px;"><a href="%(invitationLink)s"><b>%(invitationLink)s</b></a></span><span
|
||||||
style="font-size: 18px; line-height: 21px;"></span></p>
|
style="font-size: 18px; line-height: 21px;"></span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,22 @@
|
||||||
BEGIN;
|
BEGIN;
|
||||||
CREATE INDEX sessions_session_id_project_id_start_ts_durationNN_idx ON sessions (session_id, project_id, start_ts) WHERE duration IS NOT NULL;
|
CREATE INDEX sessions_session_id_project_id_start_ts_durationNN_idx ON sessions (session_id, project_id, start_ts) WHERE duration IS NOT NULL;
|
||||||
CREATE INDEX clicks_label_session_id_timestamp_idx ON events.clicks (label,session_id,timestamp);
|
CREATE INDEX clicks_label_session_id_timestamp_idx ON events.clicks (label, session_id, timestamp);
|
||||||
CREATE INDEX pages_base_path_session_id_timestamp_idx ON events.pages (base_path,session_id,timestamp);
|
CREATE INDEX pages_base_path_session_id_timestamp_idx ON events.pages (base_path, session_id, timestamp);
|
||||||
CREATE INDEX ON unstarted_sessions(project_id);
|
CREATE INDEX ON unstarted_sessions (project_id);
|
||||||
CREATE INDEX ON assigned_sessions(session_id);
|
CREATE INDEX ON assigned_sessions (session_id);
|
||||||
CREATE INDEX ON technical_info(session_id);
|
CREATE INDEX ON technical_info (session_id);
|
||||||
CREATE INDEX inputs_label_session_id_timestamp_idx ON events.inputs (label,session_id,timestamp);
|
CREATE INDEX inputs_label_session_id_timestamp_idx ON events.inputs (label, session_id, timestamp);
|
||||||
|
|
||||||
CREATE INDEX clicks_url_idx ON events.clicks (url);
|
CREATE INDEX clicks_url_idx ON events.clicks (url);
|
||||||
CREATE INDEX clicks_url_gin_idx ON events.clicks USING GIN (url gin_trgm_ops);
|
CREATE INDEX clicks_url_gin_idx ON events.clicks USING GIN (url gin_trgm_ops);
|
||||||
CREATE INDEX clicks_url_session_id_timestamp_selector_idx ON events.clicks (url, session_id, timestamp,selector);
|
CREATE INDEX clicks_url_session_id_timestamp_selector_idx ON events.clicks (url, session_id, timestamp, selector);
|
||||||
|
|
||||||
COMMIT ;
|
ALTER TABLE public.basic_authentication
|
||||||
|
RENAME COLUMN token TO invitation_token;
|
||||||
|
ALTER TABLE public.basic_authentication
|
||||||
|
RENAME COLUMN token_requested_at TO invited_at;
|
||||||
|
ALTER TABLE public.basic_authentication
|
||||||
|
ADD COLUMN change_pwd_expire_at timestamp without time zone NULL DEFAULT NULL;
|
||||||
|
ALTER TABLE basic_authentication
|
||||||
|
ADD COLUMN change_pwd_token text NULL DEFAULT NULL;
|
||||||
|
COMMIT;
|
||||||
|
|
@ -60,7 +60,7 @@ CREATE TABLE users
|
||||||
"role": "dev",
|
"role": "dev",
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"cpu": true,
|
"cpu": true,
|
||||||
"fps": false,
|
"fps": false,
|
||||||
"avgCpu": true,
|
"avgCpu": true,
|
||||||
"avgFps": true,
|
"avgFps": true,
|
||||||
"errors": true,
|
"errors": true,
|
||||||
|
|
@ -121,19 +121,21 @@ CREATE TABLE users
|
||||||
jwt_iat timestamp without time zone NULL DEFAULT NULL,
|
jwt_iat timestamp without time zone NULL DEFAULT NULL,
|
||||||
data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||||
weekly_report boolean NOT NULL DEFAULT TRUE,
|
weekly_report boolean NOT NULL DEFAULT TRUE,
|
||||||
origin user_origin NULL DEFAULT NULL,
|
origin user_origin NULL DEFAULT NULL,
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE basic_authentication
|
CREATE TABLE basic_authentication
|
||||||
(
|
(
|
||||||
user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE,
|
user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE,
|
||||||
password text DEFAULT NULL,
|
password text DEFAULT NULL,
|
||||||
generated_password boolean NOT NULL DEFAULT false,
|
generated_password boolean NOT NULL DEFAULT false,
|
||||||
token text NULL DEFAULT NULL,
|
invitation_token text NULL DEFAULT NULL,
|
||||||
token_requested_at timestamp without time zone NULL DEFAULT NULL,
|
invited_at timestamp without time zone NULL DEFAULT NULL,
|
||||||
changed_at timestamp,
|
change_pwd_token text NULL DEFAULT NULL,
|
||||||
|
change_pwd_expire_at timestamp without time zone NULL DEFAULT NULL,
|
||||||
|
changed_at timestamp,
|
||||||
UNIQUE (user_id)
|
UNIQUE (user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -576,7 +578,7 @@ create table assigned_sessions
|
||||||
created_at timestamp default timezone('utc'::text, now()) NOT NULL,
|
created_at timestamp default timezone('utc'::text, now()) NOT NULL,
|
||||||
provider_data jsonb default '{}'::jsonb NOT NULL
|
provider_data jsonb default '{}'::jsonb NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX ON assigned_sessions(session_id);
|
CREATE INDEX ON assigned_sessions (session_id);
|
||||||
|
|
||||||
-- --- events_common.sql ---
|
-- --- events_common.sql ---
|
||||||
|
|
||||||
|
|
@ -680,7 +682,7 @@ CREATE INDEX pages_path_idx ON events.pages (path);
|
||||||
CREATE INDEX pages_visually_complete_idx ON events.pages (visually_complete) WHERE visually_complete > 0;
|
CREATE INDEX pages_visually_complete_idx ON events.pages (visually_complete) WHERE visually_complete > 0;
|
||||||
CREATE INDEX pages_dom_building_time_idx ON events.pages (dom_building_time) WHERE dom_building_time > 0;
|
CREATE INDEX pages_dom_building_time_idx ON events.pages (dom_building_time) WHERE dom_building_time > 0;
|
||||||
CREATE INDEX pages_load_time_idx ON events.pages (load_time) WHERE load_time > 0;
|
CREATE INDEX pages_load_time_idx ON events.pages (load_time) WHERE load_time > 0;
|
||||||
CREATE INDEX pages_base_path_session_id_timestamp_idx ON events.pages (base_path,session_id,timestamp);
|
CREATE INDEX pages_base_path_session_id_timestamp_idx ON events.pages (base_path, session_id, timestamp);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE events.clicks
|
CREATE TABLE events.clicks
|
||||||
|
|
@ -695,10 +697,10 @@ CREATE INDEX ON events.clicks (session_id);
|
||||||
CREATE INDEX ON events.clicks (label);
|
CREATE INDEX ON events.clicks (label);
|
||||||
CREATE INDEX clicks_label_gin_idx ON events.clicks USING GIN (label gin_trgm_ops);
|
CREATE INDEX clicks_label_gin_idx ON events.clicks USING GIN (label gin_trgm_ops);
|
||||||
CREATE INDEX ON events.clicks (timestamp);
|
CREATE INDEX ON events.clicks (timestamp);
|
||||||
CREATE INDEX clicks_label_session_id_timestamp_idx ON events.clicks (label,session_id,timestamp);
|
CREATE INDEX clicks_label_session_id_timestamp_idx ON events.clicks (label, session_id, timestamp);
|
||||||
CREATE INDEX clicks_url_idx ON events.clicks (url);
|
CREATE INDEX clicks_url_idx ON events.clicks (url);
|
||||||
CREATE INDEX clicks_url_gin_idx ON events.clicks USING GIN (url gin_trgm_ops);
|
CREATE INDEX clicks_url_gin_idx ON events.clicks USING GIN (url gin_trgm_ops);
|
||||||
CREATE INDEX clicks_url_session_id_timestamp_selector_idx ON events.clicks (url, session_id, timestamp,selector);
|
CREATE INDEX clicks_url_session_id_timestamp_selector_idx ON events.clicks (url, session_id, timestamp, selector);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE events.inputs
|
CREATE TABLE events.inputs
|
||||||
|
|
@ -715,7 +717,7 @@ CREATE INDEX ON events.inputs (label, value);
|
||||||
CREATE INDEX inputs_label_gin_idx ON events.inputs USING GIN (label gin_trgm_ops);
|
CREATE INDEX inputs_label_gin_idx ON events.inputs USING GIN (label gin_trgm_ops);
|
||||||
CREATE INDEX inputs_label_idx ON events.inputs (label);
|
CREATE INDEX inputs_label_idx ON events.inputs (label);
|
||||||
CREATE INDEX ON events.inputs (timestamp);
|
CREATE INDEX ON events.inputs (timestamp);
|
||||||
CREATE INDEX inputs_label_session_id_timestamp_idx ON events.inputs (label,session_id,timestamp);
|
CREATE INDEX inputs_label_session_id_timestamp_idx ON events.inputs (label, session_id, timestamp);
|
||||||
|
|
||||||
CREATE TABLE events.errors
|
CREATE TABLE events.errors
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,30 @@
|
||||||
BEGIN;
|
BEGIN;
|
||||||
CREATE INDEX sessions_session_id_project_id_start_ts_durationNN_idx ON sessions (session_id, project_id, start_ts) WHERE duration IS NOT NULL;
|
CREATE INDEX sessions_session_id_project_id_start_ts_durationNN_idx ON sessions (session_id, project_id, start_ts) WHERE duration IS NOT NULL;
|
||||||
CREATE INDEX clicks_label_session_id_timestamp_idx ON events.clicks (label,session_id,timestamp);
|
CREATE INDEX clicks_label_session_id_timestamp_idx ON events.clicks (label, session_id, timestamp);
|
||||||
CREATE INDEX pages_base_path_session_id_timestamp_idx ON events.pages (base_path,session_id,timestamp);
|
CREATE INDEX pages_base_path_session_id_timestamp_idx ON events.pages (base_path, session_id, timestamp);
|
||||||
CREATE INDEX ON unstarted_sessions(project_id);
|
CREATE INDEX ON unstarted_sessions (project_id);
|
||||||
CREATE INDEX ON assigned_sessions(session_id);
|
CREATE INDEX ON assigned_sessions (session_id);
|
||||||
CREATE INDEX ON technical_info(session_id);
|
CREATE INDEX ON technical_info (session_id);
|
||||||
CREATE INDEX inputs_label_session_id_timestamp_idx ON events.inputs (label,session_id,timestamp);
|
CREATE INDEX inputs_label_session_id_timestamp_idx ON events.inputs (label, session_id, timestamp);
|
||||||
|
|
||||||
ALTER TABLE events.clicks ADD COLUMN
|
ALTER TABLE events.clicks
|
||||||
url text DEFAULT '' NOT NULL;
|
ADD COLUMN
|
||||||
ALTER TABLE events.clicks ADD COLUMN
|
url text DEFAULT '' NOT NULL;
|
||||||
selector text DEFAULT '' NOT NULL;
|
ALTER TABLE events.clicks
|
||||||
|
ADD COLUMN
|
||||||
|
selector text DEFAULT '' NOT NULL;
|
||||||
|
|
||||||
CREATE INDEX clicks_url_idx ON events.clicks (url);
|
CREATE INDEX clicks_url_idx ON events.clicks (url);
|
||||||
CREATE INDEX clicks_url_gin_idx ON events.clicks USING GIN (url gin_trgm_ops);
|
CREATE INDEX clicks_url_gin_idx ON events.clicks USING GIN (url gin_trgm_ops);
|
||||||
CREATE INDEX clicks_url_session_id_timestamp_selector_idx ON events.clicks (url, session_id, timestamp,selector);
|
CREATE INDEX clicks_url_session_id_timestamp_selector_idx ON events.clicks (url, session_id, timestamp, selector);
|
||||||
|
|
||||||
COMMIT ;
|
|
||||||
|
ALTER TABLE public.basic_authentication
|
||||||
|
RENAME COLUMN token TO invitation_token;
|
||||||
|
ALTER TABLE public.basic_authentication
|
||||||
|
RENAME COLUMN token_requested_at TO invited_at;
|
||||||
|
ALTER TABLE public.basic_authentication
|
||||||
|
ADD COLUMN change_pwd_expire_at timestamp without time zone NULL DEFAULT NULL;
|
||||||
|
ALTER TABLE basic_authentication
|
||||||
|
ADD COLUMN change_pwd_token text NULL DEFAULT NULL;
|
||||||
|
COMMIT;
|
||||||
|
|
@ -122,12 +122,14 @@ CREATE TABLE users
|
||||||
|
|
||||||
CREATE TABLE basic_authentication
|
CREATE TABLE basic_authentication
|
||||||
(
|
(
|
||||||
user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE,
|
user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE,
|
||||||
password text DEFAULT NULL,
|
password text DEFAULT NULL,
|
||||||
generated_password boolean NOT NULL DEFAULT false,
|
generated_password boolean NOT NULL DEFAULT false,
|
||||||
token text NULL DEFAULT NULL,
|
invitation_token text NULL DEFAULT NULL,
|
||||||
token_requested_at timestamp without time zone NULL DEFAULT NULL,
|
invited_at timestamp without time zone NULL DEFAULT NULL,
|
||||||
changed_at timestamp,
|
change_pwd_token text NULL DEFAULT NULL,
|
||||||
|
change_pwd_expire_at timestamp without time zone NULL DEFAULT NULL,
|
||||||
|
changed_at timestamp,
|
||||||
UNIQUE (user_id)
|
UNIQUE (user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -558,7 +560,7 @@ create table assigned_sessions
|
||||||
created_at timestamp default timezone('utc'::text, now()) NOT NULL,
|
created_at timestamp default timezone('utc'::text, now()) NOT NULL,
|
||||||
provider_data jsonb default '{}'::jsonb NOT NULL
|
provider_data jsonb default '{}'::jsonb NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX ON assigned_sessions(session_id);
|
CREATE INDEX ON assigned_sessions (session_id);
|
||||||
|
|
||||||
-- --- events_common.sql ---
|
-- --- events_common.sql ---
|
||||||
|
|
||||||
|
|
@ -674,27 +676,27 @@ CREATE INDEX pages_timestamp_metgt0_idx ON events.pages (timestamp) WHERE respon
|
||||||
time_to_interactive > 0;
|
time_to_interactive > 0;
|
||||||
CREATE INDEX pages_session_id_speed_indexgt0nn_idx ON events.pages (session_id, speed_index) WHERE speed_index > 0 AND speed_index IS NOT NULL;
|
CREATE INDEX pages_session_id_speed_indexgt0nn_idx ON events.pages (session_id, speed_index) WHERE speed_index > 0 AND speed_index IS NOT NULL;
|
||||||
CREATE INDEX pages_session_id_timestamp_dom_building_timegt0nn_idx ON events.pages (session_id, timestamp, dom_building_time) WHERE dom_building_time > 0 AND dom_building_time IS NOT NULL;
|
CREATE INDEX pages_session_id_timestamp_dom_building_timegt0nn_idx ON events.pages (session_id, timestamp, dom_building_time) WHERE dom_building_time > 0 AND dom_building_time IS NOT NULL;
|
||||||
CREATE INDEX pages_base_path_session_id_timestamp_idx ON events.pages (base_path,session_id,timestamp);
|
CREATE INDEX pages_base_path_session_id_timestamp_idx ON events.pages (base_path, session_id, timestamp);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE events.clicks
|
CREATE TABLE events.clicks
|
||||||
(
|
(
|
||||||
session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE,
|
session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE,
|
||||||
message_id bigint NOT NULL,
|
message_id bigint NOT NULL,
|
||||||
timestamp bigint NOT NULL,
|
timestamp bigint NOT NULL,
|
||||||
label text DEFAULT NULL,
|
label text DEFAULT NULL,
|
||||||
url text DEFAULT '' NOT NULL;
|
url text DEFAULT '' NOT NULL;
|
||||||
selector text DEFAULT '' NOT NULL;
|
selector text DEFAULT '' NOT NULL;
|
||||||
PRIMARY KEY (session_id, message_id)
|
PRIMARY KEY (session_id, message_id)
|
||||||
);
|
);
|
||||||
CREATE INDEX ON events.clicks (session_id);
|
CREATE INDEX ON events.clicks (session_id);
|
||||||
CREATE INDEX ON events.clicks (label);
|
CREATE INDEX ON events.clicks (label);
|
||||||
CREATE INDEX clicks_label_gin_idx ON events.clicks USING GIN (label gin_trgm_ops);
|
CREATE INDEX clicks_label_gin_idx ON events.clicks USING GIN (label gin_trgm_ops);
|
||||||
CREATE INDEX ON events.clicks (timestamp);
|
CREATE INDEX ON events.clicks (timestamp);
|
||||||
CREATE INDEX clicks_label_session_id_timestamp_idx ON events.clicks (label,session_id,timestamp);
|
CREATE INDEX clicks_label_session_id_timestamp_idx ON events.clicks (label, session_id, timestamp);
|
||||||
CREATE INDEX clicks_url_idx ON events.clicks (url);
|
CREATE INDEX clicks_url_idx ON events.clicks (url);
|
||||||
CREATE INDEX clicks_url_gin_idx ON events.clicks USING GIN (url gin_trgm_ops);
|
CREATE INDEX clicks_url_gin_idx ON events.clicks USING GIN (url gin_trgm_ops);
|
||||||
CREATE INDEX clicks_url_session_id_timestamp_selector_idx ON events.clicks (url, session_id, timestamp,selector);
|
CREATE INDEX clicks_url_session_id_timestamp_selector_idx ON events.clicks (url, session_id, timestamp, selector);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE events.inputs
|
CREATE TABLE events.inputs
|
||||||
|
|
@ -711,7 +713,7 @@ CREATE INDEX ON events.inputs (label, value);
|
||||||
CREATE INDEX inputs_label_gin_idx ON events.inputs USING GIN (label gin_trgm_ops);
|
CREATE INDEX inputs_label_gin_idx ON events.inputs USING GIN (label gin_trgm_ops);
|
||||||
CREATE INDEX inputs_label_idx ON events.inputs (label);
|
CREATE INDEX inputs_label_idx ON events.inputs (label);
|
||||||
CREATE INDEX ON events.inputs (timestamp);
|
CREATE INDEX ON events.inputs (timestamp);
|
||||||
CREATE INDEX inputs_label_session_id_timestamp_idx ON events.inputs (label,session_id,timestamp);
|
CREATE INDEX inputs_label_session_id_timestamp_idx ON events.inputs (label, session_id, timestamp);
|
||||||
|
|
||||||
CREATE TABLE events.errors
|
CREATE TABLE events.errors
|
||||||
(
|
(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue