2022-06-09 10:19:49 +02:00
from abc import ABC , abstractmethod
from dataclasses import dataclass
from enum import Enum
2022-06-28 17:51:44 +02:00
from typing import Optional
2022-06-20 14:34:20 +02:00
2022-06-28 17:51:44 +02:00
from arrow import Arrow
2022-06-15 08:24:11 +02:00
from newrelic import agent
2023-11-08 09:58:01 +01:00
from sqlalchemy import or_
2022-06-09 10:19:49 +02:00
from app . db import Session
2022-06-28 17:51:44 +02:00
from app . email_utils import send_welcome_email
2024-10-16 16:57:59 +02:00
from app . partner_user_utils import create_partner_user , create_partner_subscription
2023-11-08 09:58:01 +01:00
from app . utils import sanitize_email , canonicalize_email
2023-03-13 13:01:00 +01:00
from app . errors import (
AccountAlreadyLinkedToAnotherPartnerException ,
2023-03-13 14:57:00 +01:00
AccountIsUsingAliasAsEmail ,
2023-08-30 13:49:47 +02:00
AccountAlreadyLinkedToAnotherUserException ,
2023-03-13 13:01:00 +01:00
)
2022-06-09 10:19:49 +02:00
from app . log import LOG
2022-06-20 14:34:20 +02:00
from app . models import (
PartnerSubscription ,
Partner ,
PartnerUser ,
User ,
2023-03-13 13:01:00 +01:00
Alias ,
2022-06-20 14:34:20 +02:00
)
2024-10-16 16:57:59 +02:00
from app . user_audit_log_utils import emit_user_audit_log , UserAuditLogAction
2022-06-09 10:19:49 +02:00
from app . utils import random_string
class SLPlanType ( Enum ) :
Free = 1
Premium = 2
@dataclass
class SLPlan :
type : SLPlanType
expiration : Optional [ Arrow ]
@dataclass
class PartnerLinkRequest :
name : str
email : str
external_user_id : str
plan : SLPlan
2022-06-16 10:25:50 +02:00
from_partner : bool
2022-06-09 10:19:49 +02:00
@dataclass
class LinkResult :
user : User
strategy : str
def set_plan_for_partner_user ( partner_user : PartnerUser , plan : SLPlan ) :
sub = PartnerSubscription . get_by ( partner_user_id = partner_user . id )
if plan . type == SLPlanType . Free :
if sub is not None :
LOG . i (
f " Deleting partner_subscription [user_id= { partner_user . user_id } ] [partner_id= { partner_user . partner_id } ] "
)
PartnerSubscription . delete ( sub . id )
2022-06-15 08:24:11 +02:00
agent . record_custom_event ( " PlanChange " , { " plan " : " free " } )
2022-06-09 10:19:49 +02:00
else :
if sub is None :
LOG . i (
f " Creating partner_subscription [user_id= { partner_user . user_id } ] [partner_id= { partner_user . partner_id } ] "
)
2024-10-16 16:57:59 +02:00
create_partner_subscription (
partner_user = partner_user ,
expiration = plan . expiration ,
msg = " Upgraded via partner. User did not have a previous partner subscription " ,
2022-06-09 10:19:49 +02:00
)
2022-06-15 08:24:11 +02:00
agent . record_custom_event ( " PlanChange " , { " plan " : " premium " , " type " : " new " } )
2022-06-09 10:19:49 +02:00
else :
2022-06-15 08:24:11 +02:00
if sub . end_at != plan . expiration :
LOG . i (
f " Updating partner_subscription [user_id= { partner_user . user_id } ] [partner_id= { partner_user . partner_id } ] "
)
agent . record_custom_event (
" PlanChange " , { " plan " : " premium " , " type " : " extension " }
)
sub . end_at = plan . expiration
2024-10-16 16:57:59 +02:00
emit_user_audit_log (
user = partner_user . user ,
action = UserAuditLogAction . SubscriptionExtended ,
message = " Extended partner subscription " ,
)
2022-06-09 10:19:49 +02:00
Session . commit ( )
def set_plan_for_user ( user : User , plan : SLPlan , partner : Partner ) :
partner_user = PartnerUser . get_by ( partner_id = partner . id , user_id = user . id )
if partner_user is None :
return
return set_plan_for_partner_user ( partner_user , plan )
def ensure_partner_user_exists_for_user (
link_request : PartnerLinkRequest , sl_user : User , partner : Partner
) - > PartnerUser :
# Find partner_user by user_id
2022-06-10 12:23:04 +02:00
res = PartnerUser . get_by ( user_id = sl_user . id )
if res and res . partner_id != partner . id :
raise AccountAlreadyLinkedToAnotherPartnerException ( )
2022-06-09 10:19:49 +02:00
if not res :
2024-10-16 16:57:59 +02:00
res = create_partner_user (
user = sl_user ,
2022-06-09 10:19:49 +02:00
partner_id = partner . id ,
partner_email = link_request . email ,
external_user_id = link_request . external_user_id ,
)
Session . commit ( )
LOG . i (
f " Created new partner_user for partner: { partner . id } user: { sl_user . id } external_user_id: { link_request . external_user_id } . PartnerUser.id is { res . id } "
)
return res
class ClientMergeStrategy ( ABC ) :
def __init__ (
self ,
link_request : PartnerLinkRequest ,
user : Optional [ User ] ,
partner : Partner ,
) :
if self . __class__ == ClientMergeStrategy :
raise RuntimeError ( " Cannot directly instantiate a ClientMergeStrategy " )
self . link_request = link_request
self . user = user
self . partner = partner
@abstractmethod
def process ( self ) - > LinkResult :
pass
class NewUserStrategy ( ClientMergeStrategy ) :
def process ( self ) - > LinkResult :
# Will create a new SL User with a random password
2023-11-08 09:58:01 +01:00
canonical_email = canonicalize_email ( self . link_request . email )
2022-06-09 10:19:49 +02:00
new_user = User . create (
2023-11-08 09:58:01 +01:00
email = canonical_email ,
2022-06-09 10:19:49 +02:00
name = self . link_request . name ,
password = random_string ( 20 ) ,
2022-06-29 16:55:20 +02:00
activated = True ,
2022-06-16 10:25:50 +02:00
from_partner = self . link_request . from_partner ,
2022-06-09 10:19:49 +02:00
)
2024-10-16 16:57:59 +02:00
partner_user = create_partner_user (
user = new_user ,
2022-06-09 10:19:49 +02:00
partner_id = self . partner . id ,
external_user_id = self . link_request . external_user_id ,
partner_email = self . link_request . email ,
)
LOG . i (
f " Created new user for login request for partner: { self . partner . id } external_user_id: { self . link_request . external_user_id } . New user { new_user . id } partner_user: { partner_user . id } "
)
set_plan_for_partner_user (
partner_user ,
self . link_request . plan ,
)
Session . commit ( )
2022-06-28 11:57:21 +02:00
if not new_user . created_by_partner :
send_welcome_email ( new_user )
2022-06-15 08:24:11 +02:00
agent . record_custom_event ( " PartnerUserCreation " , { " partner " : self . partner . name } )
2022-06-09 10:19:49 +02:00
return LinkResult (
user = new_user ,
strategy = self . __class__ . __name__ ,
)
2022-06-16 10:25:50 +02:00
class ExistingUnlinkedUserStrategy ( ClientMergeStrategy ) :
2022-06-09 10:19:49 +02:00
def process ( self ) - > LinkResult :
2024-02-22 17:38:34 +01:00
# IF it was scheduled to be deleted. Unschedule it.
self . user . delete_on = None
2022-06-09 10:19:49 +02:00
partner_user = ensure_partner_user_exists_for_user (
self . link_request , self . user , self . partner
)
set_plan_for_partner_user ( partner_user , self . link_request . plan )
return LinkResult (
user = self . user ,
strategy = self . __class__ . __name__ ,
)
class LinkedWithAnotherPartnerUserStrategy ( ClientMergeStrategy ) :
def process ( self ) - > LinkResult :
2023-08-30 13:49:47 +02:00
raise AccountAlreadyLinkedToAnotherUserException ( )
2022-06-09 10:19:49 +02:00
def get_login_strategy (
link_request : PartnerLinkRequest , user : Optional [ User ] , partner : Partner
) - > ClientMergeStrategy :
if user is None :
# We couldn't find any SimpleLogin user with the requested e-mail
return NewUserStrategy ( link_request , user , partner )
# Check if user is already linked with another partner_user
other_partner_user = PartnerUser . get_by ( partner_id = partner . id , user_id = user . id )
if other_partner_user is not None :
return LinkedWithAnotherPartnerUserStrategy ( link_request , user , partner )
# There is a SimpleLogin user with the partner_user's e-mail
2022-06-16 10:25:50 +02:00
return ExistingUnlinkedUserStrategy ( link_request , user , partner )
2022-06-09 10:19:49 +02:00
2024-10-16 16:57:59 +02:00
def check_alias ( email : str ) :
2023-03-13 13:01:00 +01:00
alias = Alias . get_by ( email = email )
if alias is not None :
2023-03-13 14:57:00 +01:00
raise AccountIsUsingAliasAsEmail ( )
2023-03-13 13:01:00 +01:00
2022-06-09 10:19:49 +02:00
def process_login_case (
link_request : PartnerLinkRequest , partner : Partner
) - > LinkResult :
2022-07-04 11:51:43 +02:00
# Sanitize email just in case
link_request . email = sanitize_email ( link_request . email )
2022-06-09 10:19:49 +02:00
# Try to find a SimpleLogin user registered with that partner user id
partner_user = PartnerUser . get_by (
partner_id = partner . id , external_user_id = link_request . external_user_id
)
if partner_user is None :
2023-11-08 09:58:01 +01:00
canonical_email = canonicalize_email ( link_request . email )
2022-06-09 10:19:49 +02:00
# We didn't find any SimpleLogin user registered with that partner user id
2023-05-08 18:47:10 +02:00
# Make sure they aren't using an alias as their link email
check_alias ( link_request . email )
2023-11-08 09:58:01 +01:00
check_alias ( canonical_email )
2022-06-09 10:19:49 +02:00
# Try to find it using the partner's e-mail address
2023-11-08 09:58:01 +01:00
users = User . filter (
or_ ( User . email == link_request . email , User . email == canonical_email )
) . all ( )
if len ( users ) > 1 :
user = [ user for user in users if user . email == canonical_email ] [ 0 ]
elif len ( users ) == 1 :
user = users [ 0 ]
else :
user = None
2022-06-09 10:19:49 +02:00
return get_login_strategy ( link_request , user , partner ) . process ( )
else :
# We found the SL user registered with that partner user id
# We're done
set_plan_for_partner_user ( partner_user , link_request . plan )
# It's the same user. No need to do anything
return LinkResult (
user = partner_user . user ,
strategy = " Link " ,
)
def link_user (
link_request : PartnerLinkRequest , current_user : User , partner : Partner
) - > LinkResult :
2022-07-04 11:51:43 +02:00
# Sanitize email just in case
link_request . email = sanitize_email ( link_request . email )
2024-02-22 17:38:34 +01:00
# If it was scheduled to be deleted. Unschedule it.
current_user . delete_on = None
2022-06-09 10:19:49 +02:00
partner_user = ensure_partner_user_exists_for_user (
link_request , current_user , partner
)
set_plan_for_partner_user ( partner_user , link_request . plan )
2022-06-15 08:24:11 +02:00
agent . record_custom_event ( " AccountLinked " , { " partner " : partner . name } )
2022-06-09 10:19:49 +02:00
Session . commit ( )
return LinkResult (
user = current_user ,
strategy = " Link " ,
)
def switch_already_linked_user (
link_request : PartnerLinkRequest , partner_user : PartnerUser , current_user : User
) :
# Find if the user has another link and unlink it
other_partner_user = PartnerUser . get_by (
user_id = current_user . id ,
partner_id = partner_user . partner_id ,
)
if other_partner_user is not None :
LOG . i (
f " Deleting previous partner_user: { other_partner_user . id } from user: { current_user . id } "
)
2024-10-16 16:57:59 +02:00
emit_user_audit_log (
user = other_partner_user . user ,
action = UserAuditLogAction . UnlinkAccount ,
message = f " Deleting partner_user { other_partner_user . id } (external_user_id= { other_partner_user . external_user_id } | partner_email= { other_partner_user . partner_email } ) from user { current_user . id } , as we received a new link request for the same partner " ,
)
2022-06-09 10:19:49 +02:00
PartnerUser . delete ( other_partner_user . id )
LOG . i ( f " Linking partner_user: { partner_user . id } to user: { current_user . id } " )
# Link this partner_user to the current user
2024-10-16 16:57:59 +02:00
emit_user_audit_log (
user = partner_user . user ,
action = UserAuditLogAction . UnlinkAccount ,
message = f " Unlinking from partner, as user will now be tied to another external account. old=(id= { partner_user . user . id } | email= { partner_user . user . email } ) | new=(id= { current_user . id } | email= { current_user . email } ) " ,
)
2022-06-09 10:19:49 +02:00
partner_user . user_id = current_user . id
2024-10-16 16:57:59 +02:00
emit_user_audit_log (
user = current_user ,
action = UserAuditLogAction . LinkAccount ,
message = f " Linking user { current_user . id } ( { current_user . email } ) to partner_user: { partner_user . id } (external_user_id= { partner_user . external_user_id } | partner_email= { partner_user . partner_email } ) " ,
)
2022-06-09 10:19:49 +02:00
# Set plan
set_plan_for_partner_user ( partner_user , link_request . plan )
Session . commit ( )
return LinkResult (
user = current_user ,
strategy = " Link " ,
)
def process_link_case (
link_request : PartnerLinkRequest ,
current_user : User ,
partner : Partner ,
) - > LinkResult :
2022-07-04 11:51:43 +02:00
# Sanitize email just in case
link_request . email = sanitize_email ( link_request . email )
2022-06-09 10:19:49 +02:00
# Try to find a SimpleLogin user linked with this Partner account
partner_user = PartnerUser . get_by (
partner_id = partner . id , external_user_id = link_request . external_user_id
)
if partner_user is None :
# There is no SL user linked with the partner. Proceed with linking
return link_user ( link_request , current_user , partner )
# There is a SL user registered with the partner. Check if is the current one
2022-08-11 10:38:44 +02:00
if partner_user . user_id == current_user . id :
2022-06-09 10:19:49 +02:00
# Update plan
set_plan_for_partner_user ( partner_user , link_request . plan )
# It's the same user. No need to do anything
return LinkResult (
user = current_user ,
strategy = " Link " ,
)
else :
return switch_already_linked_user ( link_request , partner_user , current_user )