async def _shutdown(self) -> None: """ Internal method to clean up on starlette server close. """ # we want to make sure that any emails that still need to be sent are either # preserved or sent properly. if not self.STUBBED: # No need to shut down workers that never got made. logging.info(_("EMAIL: Shutting down email workers")) for worker in self.workers: worker.cancel() try: await worker except asyncio.CancelledError: logging.debug(_("EMAIL: Worker cancelled"))
async def send_plain_email(self, subject: str, message: str, to: typing.List[str]): """ Sends a plain text email. subject: subject of the email Message: Content of the message To: list of email addresses to send to As emails are sent using a pool, there is no way to confirm that the email sent. This returns nothing. """ if self.STUBBED: self.stubs.append({ "Type": "plain", "Subject": subject, "Message": message, "To": to }) logging.debug(_("Email send attempt was stubbed")) return message = MIMEText(message) message['From'] = self.dsn.username + "@" + self.dsn.hostname message['To'] = to message['Subject'] = subject await self.mail_queue.put(message)
async def send_html_template_email(self, subject: str, template: str, content: typing.Dict[str, typing.Any], to): """ Generates and sends an HTML email from a template. subject: subject line of the email template: valid jinja template content: the dictionary to pass on to the jinja template handler. As emails are sent using a pool, there is no way to confirm that the email sent. This returns nothing. """ if self.STUBBED: self.stubs.append({ "Type": "html", "Subject": subject, "Template": template, "Content": content, "To": to }) logging.debug(_("Email send attempt was stubbed")) return template = self.jinja.get_template(template) message = MIMEText(template.render(content), "html") message['To'] = to message['Subject'] = subject await self.mail_queue.put(message)
def response_contains_graphql_error(response: dict, error_message: str) -> bool: """Given a graphql result and an error message, checks through the result to see if the error message is included in response['errors']. """ if response is None: raise Exception(_('Response is null.')) if 'errors' not in response: return False if response['errors'] == []: return False found_error = False for error in response['errors']: if error['message'] == _(error_message): found_error = True return found_error
async def resolve_identity(self, info, name): """Looks up and returns an identity object based on the given user name.""" query = Identity.join(Actor, Actor.id == Identity.actor_id) \ .select().where(Identity.user_name == name) identity_ = await query.gino.load(Identity.distinct(Identity.id) \ .load(actor=Actor.distinct(Actor.id))).first() if identity_ is None: raise GraphQLError(_('Identity does not exist!')) identity_object = IdentityObjectType( display_name=identity_.display_name, user_name=identity_.user_name, uri=f'{BASE_URL}/u/{identity_.user_name}', created=identity_.created) return identity_object
async def _startup(self) -> None: """ Startup function intended to be ran on application start. Returns: none """ # pylint: disable=attribute-defined-outside-init # disable this warning, as startup is basically a second init function # Using a url might not be the *best* way to do this but it cant be the worst # Server must be properly setup if STUBBED is false self.STUBBED = ( # pylint: disable=invalid-name self.config('DEBUG', cast=bool, default=False) and (not self.config('DEV_EMAIL', cast=bool, default=False))) if not self.config('MAIL_DSN', default=False) and not self.STUBBED: sys.exit( "Configuration failure:\n" "The configuration setting MAIL_DSN was not set, as a result, " "the email subcomponent could not be enabled.\n" "Please set this option before attempting to run again.") if self.STUBBED: logging.info( _("Email has been stubbed according to settings in the config file. " "No emails can be sent.")) else: self.dsn = self.config('MAIL_DSN', cast=URL) self.mail_queue = asyncio.Queue( ) # No max size, we dont want to drop emails self.workers = [ asyncio.create_task(self._send_mail_worker()) for _ in range(self.config('MAIL_WORKER_COUNT', default=10)) ] jinja_template = [] if self.config('MAIL_JINJA_DIR', default=False): jinja_template = self.config('MAIL_JINJA_DIR', cast=str) self.jinja = jinja2.Environment(loader=jinja2.ChoiceLoader([ jinja2.FileSystemLoader(jinja_template), jinja2.PackageLoader('email', 'templates') ]))
def test_identity_query(gino_db): client = TestClient(app) # Test account (actually identity) querying response = client.post('/graphql', data=json.dumps({ 'query': """ query { identity(name: "test") { displayName, userName } } """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) response_body = json.loads(response.content) assert response_body['data']['identity']['displayName'] == 'test' assert response_body['data']['identity']['userName'] == 'test' # This should throw a graphql error because the account doesn't exist response = client.post( '/graphql', data=json.dumps( {'query': """{identity(name: "test123") { displayName }}"""}), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) response_body = json.loads(response.content) assert response_contains_graphql_error(json.loads(response.content), _('Identity does not exist!'))
async def mutate(self, info, user_name, email_address, password): """Creates a user account using the given user name, email address, and password. """ try: validate_email(email_address) except EmailSyntaxError: raise GraphQLError( _('Email address as entered is not a valid email address.')) except EmailUndeliverableError: pass password = password.strip() if len(password) < 5: raise GraphQLError( _('Password is too short! Should be at least five characters in length.' )) if ALLOWED_NAME_CHARACTERS_RE.match(user_name) is None: raise GraphQLError( _('Invalid user name. Characters allowed are a-z and _.')) email_account_used_by = await Account.select('id') \ .where(Account.email_address == email_address).gino.scalar() if not email_account_used_by is None: raise GraphQLError( _('This email address is already in use for another account.')) user_name_used_by = await Identity.select('id') \ .where( (Identity.user_name == user_name) | (Identity.display_name == user_name) ).gino.scalar() if not user_name_used_by is None: raise GraphQLError( _('This user name is already in use. User names must be unique.' )) created_ = pendulum.now().naive() actor = ActorSchema() actor.id = f'{BASE_URL}/u/{user_name}' actor.type = 'Person' actor.url = actor.id actor.followers = f'{BASE_URL}/u/{user_name}/followers' actor.following = f'{BASE_URL}/u/{user_name}/following' actor.inbox = f'{BASE_URL}/u/{user_name}/inbox' actor.outbox = f'{BASE_URL}/u/{user_name}/outbox' actor.name = user_name actor.preferredUsername = user_name actor_model = actor.to_model() actor_model.generate_keys() await actor_model.create() identity_model = Identity() identity_model.actor_id = actor_model.id identity_model.display_name = user_name identity_model.user_name = user_name identity_model.disabled = False identity_model.created = created_ identity_model.last_updated = created_ await identity_model.create() await actor_model.update(identity_id=identity_model.id).apply() account_model = Account() account_model.email_address = email_address account_model.primary_identity_id = identity_model.id account_model.created = created_ account_model.set_password(password) await account_model.create() await identity_model.update(account_id=account_model.id).apply() new_identity = IdentityObjectType(display_name=user_name, user_name=user_name, uri=f'{BASE_URL}/u/{user_name}', avatar='', created=account_model.created) return RegisterUser(identity=new_identity)
class Arguments: """Graphene arguments meta class.""" user_name = graphene.String( description=_('The email address for the account to login as.')) password = graphene.String( description=_('Password to use for this login attempt.'))
"""Included this code to change gino's logging level. This prevents some double logging that was making my lose my mind with uvicorn's defaults. """ import logging import lamia.config as CONFIG from lamia.translation import _ logging.basicConfig() logging.getLogger('gino').setLevel(logging.WARN) # Debug messages only when in debug mode if CONFIG.DEBUG: logging.getLogger().setLevel(CONFIG.DEBUG) # This should be translated to true to show that translation is not failing logging.debug(_("Translation is working: False"))
"""This module pulls in the configuration details from a lamia.config or lamia.dev.config file. """ import os from starlette.config import Config from lamia.translation import gettext as _ if os.path.exists('lamia.dev.config') or os.environ.get('DEBUG', False): DEV_CONFIG = True config = Config('lamia.dev.config') # pylint: disable=invalid-name else: DEV_CONFIG = False config = Config('lamia.config') # pylint: disable=invalid-name DEBUG = config('DEBUG', cast=bool, default=False) SITE_NAME = config('SITE_NAME', cast=str, default=_('A Lamia Community')) TEMPLATE_RELOAD = config( "TEMPLATE_RELOAD", cast=bool, default=False, ) BASE_URL = config('BASE_URL', cast=str)
def test_registration(gino_db): client = TestClient(app) # Test account creation response = client.post('/graphql', data=json.dumps({ 'query': """ mutation { registerUser(userName: "******", emailAddress: "*****@*****.**", password: "******") { identity { displayName, userName } } } """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) response_body = json.loads(response.content) assert response_body['data']['registerUser']['identity'][ 'displayName'] == 'test' assert response_body['data']['registerUser']['identity'][ 'userName'] == 'test' # This should raise a graphql error due to the empty password response = client.post('/graphql', data=json.dumps({ 'query': """ mutation { registerUser(userName: "******", emailAddress: "*****@*****.**", password: "") { identity {displayName}}} """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) assert response_contains_graphql_error( json.loads(response.content), _('Password is too short! Should be at least five characters in length.' )) # This should raise a graphql error due to the duplicate name response = client.post('/graphql', data=json.dumps({ 'query': """ mutation { registerUser(userName: "******", emailAddress: "*****@*****.**", password: "******") { identity {displayName}}} """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) assert response_contains_graphql_error( json.loads(response.content), _('This user name is already in use. User names must be unique.')) # This should raise a graphql error due to the duplicate email response = client.post('/graphql', data=json.dumps({ 'query': """ mutation { registerUser(userName: "******", emailAddress: "*****@*****.**", password: "******") { identity {displayName}}} """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) assert response_contains_graphql_error( json.loads(response.content), _('This email address is already in use for another account.')) # This should raise a graphql error due to the invalid username response = client.post('/graphql', data=json.dumps({ 'query': """ mutation { registerUser(userName: "******", emailAddress: "*****@*****.**", password: "******") { identity {displayName}}} """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) assert response_contains_graphql_error( json.loads(response.content), _('Invalid user name. Characters allowed are a-z and _.')) # This should raise a graphql error due to the invalid email syntax response = client.post('/graphql', data=json.dumps({ 'query': """ mutation { registerUser(userName: "******", emailAddress: "test_abc", password: "******") { identity {displayName}}} """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) response_body = json.loads(response.content) assert response_contains_graphql_error( json.loads(response.content), _('Email address as entered is not a valid email address.'))
def test_login(gino_db): client = TestClient(app) # Test account creation response = client.post('/graphql', data=json.dumps({ 'query': """ mutation {loginUser(userName: "******", password: "******") {token}} """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) response_body = json.loads(response.content) access_token = response_body['data'].get('loginUser', {}).get('token') assert access_token is not None response = client.post('/graphql', data=json.dumps({ 'query': """ mutation {loginUser(userName: "******", password: "******") {token}} """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) response_body = json.loads(response.content) assert access_token is not None response = client.post('/graphql', data=json.dumps({ 'query': """ mutation {loginUser(userName: "******", password: "******") {token}} """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) response_body = json.loads(response.content) assert response_contains_graphql_error(json.loads(response.content), _('Invalid username or password.')) response = client.post('/graphql', data=json.dumps({ 'query': """ mutation {loginUser(userName: "******", password: "******") {token}} """ }), headers={ 'Accept': 'application/json', 'content-type': 'application/json' }) response_body = json.loads(response.content) assert response_contains_graphql_error(json.loads(response.content), _('Invalid username or password.'))