예제 #1
0
async def get(request: Request, *, session: Session):
    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "SELECT notify_when, notify_by, notify_all, telegram_user_id "
                "FROM users WHERE id = %s LIMIT 1", (session.user_id, ))
            notification_settings = await cur.fetchone()
            if not notification_settings["notify_all"]:
                await cur.execute(
                    "SELECT herbs.id, herbs.latin_name, herbs.english_name, herbs.botanic_name "
                    "FROM notify_herbs JOIN herbs ON notify_herbs.herb_id = herbs.id "
                    "WHERE user_id = %s", (session.user_id, ))
                herbs = await cur.fetchall()
            else:
                herbs = True

    return web.json_response({
        "when": [
            x.name for x in [
                NotificationWhen(notification_settings["notify_when"]) & y
                for y in NotificationWhen
            ] if x != NotificationWhen.NONE
        ],
        "by": [
            x.name for x in [
                NotificationBy(notification_settings["notify_by"]) & y
                for y in NotificationBy if y != NotificationBy.NONE
            ] if x != NotificationWhen.NONE
        ],
        "telegram_linked":
        notification_settings["telegram_user_id"] is not None,
        "herbs":
        herbs
    })
예제 #2
0
async def post(request: Request, *, params):
    token = request.match_info["token"].strip()
    logging.debug("Resetting password for recovery token {}".format(token))

    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            # Recupera utente da token
            await cur.execute(
                "SELECT users.id FROM users JOIN password_reset_tokens ON users.id = password_reset_tokens.user_id "
                "WHERE token = %s AND privileges & 2 > 0 AND `expire` > UNIX_TIMESTAMP() LIMIT 1",
                (token, ))
            db_user = await cur.fetchone()
            if not db_user:
                raise NotFoundError("Token non valido o scaduto.")

            # Cripta nuova password
            bcrypted_password = bcrypt.hashpw(params["password"],
                                              bcrypt.gensalt())

            # Aggiorna db
            await cur.execute(
                "UPDATE users SET password = %s WHERE id = %s LIMIT 1",
                (bcrypted_password, db_user["id"]))
            await cur.execute(
                "DELETE FROM password_reset_tokens WHERE token = %s LIMIT 1",
                (token, ))
            await conn.commit()

    # Ok!
    return web.json_response({"message": "ok"})
예제 #3
0
async def post(request: Request, *, params, session: Session):
    updates = ", ".join([
        "notify_{name} = %({name})s".format(name=k)
        for k in ("when", "by") if k in params
    ] + ["notify_all = %(notify_all)s"] if "herbs" in params else [])

    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "UPDATE users SET {} WHERE id = %(user_id)s LIMIT 1".format(
                    updates), {
                        "when":
                        sum([NotificationWhen[k] for k in params["when"]]),
                        "by":
                        sum([NotificationBy[k] for k in params["by"]]),
                        "user_id":
                        session.user_id,
                        "notify_all":
                        "herbs" in params and type(params["herbs"]) is bool
                    })
            if "herbs" in params:
                await cur.execute(
                    "DELETE FROM notify_herbs WHERE user_id = %s",
                    (session.user_id, ))
                if type(params["herbs"]) is list:
                    for herb_id in params["herbs"]:
                        await cur.execute(
                            "INSERT IGNORE INTO notify_herbs (user_id, herb_id) VALUES (%s, %s)",
                            (session.user_id, herb_id))
            await conn.commit()
    return web.json_response({"message": "ok"})
예제 #4
0
    async def load_from_api_key(cls,
                                key: str,
                                already_hashed: bool = False) -> ApiKeySession:
        """
        Ritorna un oggetto `ApiKeySession` in base al valore di `key`

        :param key: api key. In chiaro o hash della key.
        :param already_hashed: se `True`, `key` è l'hash SHA512 della api key, altrimenti è in chiaro.
        :return: oggetto `ApiKeySession` che rappresenta la sessione dell'utente
        :raise: `SessionError` se la api key non è presente nel database
        """
        cls.logger.debug("Loading session for api key {}".format(key))
        if not already_hashed:
            hashed_key = general.sha512(key)
        else:
            hashed_key = key
        async with EmaNews().db.acquire() as conn:
            async with conn.cursor() as cur:
                await cur.execute(
                    "SELECT user_id FROM api_keys WHERE key_hash = %s LIMIT 1",
                    (hashed_key, ))
                key_record = await cur.fetchone()
                if key_record is None:
                    raise SessionError("Invalid api key")
                return ApiKeySession(key_record["user_id"], hashed_key)
예제 #5
0
async def delete(request: Request, *, session: Session):
    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "UPDATE users SET telegram_user_id = NULL WHERE id = %s LIMIT 1",
                (session.user_id, ))
            await conn.commit()
    return web.json_response({"message": "ok"})
예제 #6
0
async def get(request: Request, *, session: Session):
    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "SELECT id, name FROM api_keys WHERE user_id = %s",
                (session.user_id, ))
            results = await cur.fetchall()
    return web.json_response(results)
예제 #7
0
async def get(request: Request):
    token = request.match_info["token"].strip()
    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "SELECT id FROM password_reset_tokens WHERE token = %s AND `expire` > UNIX_TIMESTAMP() LIMIT 1",
                (token, ))
            if await cur.fetchone():
                return web.json_response({"message": "ok"})
            raise NotFoundError("Token invalido o scaduto.")
예제 #8
0
async def get(request: Request, *, session: Session):
    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "SELECT id, name, surname, privileges, email FROM users WHERE id = %s LIMIT 1", (session.user_id,)
            )
            db_user = await cur.fetchone()
    db_user["gravatar_hash"] = gravatar.get_hash(db_user["email"])
    del db_user["email"]
    return web.json_response(db_user)
예제 #9
0
    async def wrapper(*args, **kwargs) -> Response:
        resp = web.json_response({}, status=200)
        try:
            resp = await f(*args, **kwargs)
        except api.NotFoundError as e:
            resp = json_error_response(
                404, readable_exception(e, "Resource not found"))
        except api.InternalServerError as e:
            resp = json_error_response(
                500, readable_exception(e, "Internal server error"))
        except api.ForbiddenError as e:
            resp = json_error_response(
                403, readable_exception(e, "Access forbidden"))
        # except api.MissingArgumentsError as e:
        #     resp = json_error_response(400, readable_exception(e, "Missing some required arguments"))
        except api.InvalidArgumentsError as e:
            resp = json_error_response(
                400, readable_exception(e, "Some arguments are not valid"))
        except api.ConflictError as e:
            resp = json_error_response(
                409, readable_exception(e, "Resource conflict"))
        except api.NotAcceptableError as e:
            resp = json_error_response(406,
                                       readable_exception(e, "Not acceptable"))
        except api.Created as e:
            resp = json_error_response(201, readable_exception(e, "Created"))
        except api.NotAuthenticatedError:
            resp = json_error_response(401, "Not authenticated")
        except (api.ForceLogoutError, sessions.SessionError) as e:
            resp = json_error_response(200,
                                       readable_exception(e, "Disconnected"))
            resp.del_cookie("session")
        except asyncio.CancelledError:
            resp = json_error_response(400, "Request was interrupted")
        except Exception:
            # Errore non gestito
            # Stampa lo stacktrace in console
            stack_trace = str(traceback.format_exc())
            print(stack_trace)

            # Se l'api è in debug mode, invia lo stacktrace al client
            # altrimenti mostra un messaggio generico
            if EmaNews().debug:  # pragma: nocover
                msg = readable_exception(traceback.format_exc(),
                                         "Unhandled internal server error")
            else:
                msg = "Internal server error."
            resp = json_error_response(500, msg)

            # Report a sentry (se abilitato)
            raven.on_exception()
        finally:
            # Ritorna la risposta al client
            return resp
예제 #10
0
async def delete(request: Request, *, session: Session):
    id_ = request.match_info["id_"].strip()
    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "DELETE FROM api_keys WHERE id = %s AND user_id = %s LIMIT 1",
                (
                    id_,
                    session.user_id,
                ))
            await conn.commit()
    return web.json_response({"message": "ok"})
예제 #11
0
        async def wrapper(request: web.Request, *args, **kwargs) -> Response:
            # Prendi cookie di sessione. None se non presente.
            session_token_cookie = request.cookies.get("session")

            # Prendi api key da header `X-EmaNews-Token` o parametro GET `apikey`.
            # None se non presente.
            api_key = next(
                (x for x in (request.headers["X-EmaNews-Token"].strip(
                ) if "X-EmaNews-Token" in request.headers else None,
                             request.query["apikey"].strip() if "apikey" in
                             request.query else None) if x is not None and x),
                None)

            if session_token_cookie is not None:
                # Cookie di sessione presente, prova a caricare la sessione da redis
                try:
                    session = await sessions.SessionFactory.load_from_redis(
                        session_token_cookie)
                except sessions.SessionError as e:  # pragma: nocover
                    # Errore durante il caricamento della sessione, cancellala
                    await sessions.SessionFactory.delete_from_redis(
                        session_token_cookie)
                    # Rilancia l'eccezione per inviare l'errore al client
                    raise api.ForceLogoutError(e)
            elif api_key is not None:
                # Api key presente in header/querystring
                try:
                    # Carica sessione
                    session = await sessions.SessionFactory.load_from_api_key(
                        api_key)
                except sessions.SessionError as e:
                    # API key non valida
                    raise api.ForbiddenError(e)
            else:
                # Cookie sessione e api key mancante!
                raise api.NotAuthenticatedError()

            # Controllo privilegi utente in base alla sessione caricata
            async with EmaNews().db.acquire() as conn:
                async with conn.cursor() as cur:
                    await cur.execute(
                        "SELECT privileges FROM users WHERE id = %s LIMIT 1",
                        (session.user_id, ))
                    privs = await cur.fetchone()
                    if not privs:  # pragma: nocover
                        raise sessions.SessionError("User not found")
                    if not (privs["privileges"]
                            & required_privileges):  # pragma: nocover
                        raise api.ForbiddenError("Insufficient privileges")

            # Sessione creata e privilegi corretti, chiama l'api handler
            return await f(request, *args, session=session, **kwargs)
예제 #12
0
async def handle(request: Request, *, params):
    ok_response = web.json_response({"message": "ok"})
    logging.debug("Requested password reset for {}".format(params["email"]))

    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "SELECT * FROM users WHERE email = %s AND privileges & 2 > 0 LIMIT 1",
                (params["email"], ))
            db_user = await cur.fetchone()
            if not db_user:
                logging.debug("Failed silently")
                return ok_response

            # Cancella token scaduti per questo utente
            await cur.execute(
                "DELETE FROM password_reset_tokens WHERE `expire` <= UNIX_TIMESTAMP() AND user_id = %s",
                (db_user["id"]))

            # Recupera eventuale token esistente
            await cur.execute(
                "SELECT token FROM password_reset_tokens WHERE user_id = %s AND `expire` > UNIX_TIMESTAMP() LIMIT 1",
                (db_user["id"], ))
            db_token = await cur.fetchone()
            if not db_token:
                # Nessun token, generane uno
                logging.debug("Generating new token")
                token = general.random_string_secure(64)
                await cur.execute(
                    "INSERT INTO password_reset_tokens (user_id, token, `expire`) "
                    "VALUES (%s, %s, UNIX_TIMESTAMP() + 3600)",
                    (db_user["id"], token))
                await conn.commit()
            else:
                # Token esistente
                logging.debug("Using pre-existing token")
                token = db_token["token"]

            # Invia email
            await EmaNews().mailgun_client.send(
                to=params["email"],
                subject="Recupero password EmaNews",
                html=
                "Clicca sul seguente link per reimpostare la tua password del tuo account EmaNews: "
                "<a href='{}/password_reset/{}'>Reimposta password</a>.<br>"
                "Il link scade tra 60 minuti. Se non sei stato tu a richiedere il reset "
                "della tua password, ignora questa email.".format(
                    Config()["WEB_BASE_URL"].rstrip("/"), token))
    return ok_response
예제 #13
0
async def post(request: Request, *, session: Session, params):
    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            found = False
            while not found:  # pragma: nocover
                key = general.random_string_secure(32)
                key_hash = general.sha512(key)
                await cur.execute(
                    "SELECT id FROM api_keys WHERE key_hash = %s LIMIT 1",
                    (key_hash, ))
                found = not await cur.fetchone()
            await cur.execute(
                "INSERT INTO api_keys (name, key_hash, user_id) VALUES (%s, %s, %s)",
                (params["name"], key_hash, session.user_id))
            await conn.commit()
    return web.json_response({"message": "ok", "key": key})
예제 #14
0
async def post(request: Request, *, params):
    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            # Controlla se l'indirizzo email è stato già usato
            await cur.execute("SELECT id FROM users WHERE email = %s LIMIT 1", (params["email"],))
            other_user = await cur.fetchone()
            if other_user:
                # Già usato
                raise ConflictError("Esiste già un altro utente registrato con questo indirizzo email.")

            # Ok, hash password con bcrypt
            bcrypted_password = bcrypt.hashpw(params["password"], bcrypt.gensalt())

            # Insert in db
            await cur.execute(
                "INSERT INTO users (name, surname, email, password, privileges) "
                "VALUES (%s, %s, %s, %s, %s)",
                (
                    params["name"],
                    params["surname"],
                    params["email"],
                    bcrypted_password.decode(),
                    int(Privileges.PENDING_ACTIVATION)
                )
            )

            # Genera e salva token attivazione account
            token = general.random_string_secure(64)
            await cur.execute("INSERT INTO activation_tokens (user_id, token) VALUES (%s, %s)",
                              (cur.lastrowid, token))
            await conn.commit()

            # Invio email
            await EmaNews().mailgun_client.send(
                to=params["email"],
                subject="Conferma il tuo account EmaNews",
                html="Clicca sul seguente link per attivare il tuo account EmaNews: "
                     "<a href='{}/activate/{}'>Attiva account</a>".format(
                    Config()["WEB_BASE_URL"].rstrip("/"), token
                )
            )

    # Ok!
    return web.json_response({
        "message": "ok"
    })
예제 #15
0
async def get(request: Request, *, session: Session):
    token = ""

    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "SELECT telegram_user_id FROM users WHERE id = %s LIMIT 1",
                (session.user_id, ))
            telegram_user_id = await cur.fetchone()
            if telegram_user_id["telegram_user_id"] is not None:
                raise NotAcceptableError("Account già collegato con Telegram")

            await cur.execute(
                "DELETE FROM telegram_link_tokens WHERE user_id = %s AND `expire` < UNIX_TIMESTAMP()",
                (session.user_id, ))
            await conn.commit()

            await cur.execute(
                "SELECT token FROM telegram_link_tokens WHERE user_id = %s AND `expire` > UNIX_TIMESTAMP() LIMIT 1",
                (session.user_id, ))
            token = await cur.fetchone()
            if token is None:
                found = False
                while not found:  # pragma: nocover
                    token = general.random_string_secure(16)
                    await cur.execute(
                        "SELECT id FROM telegram_link_tokens WHERE token = %s LIMIT 1",
                        (token, ))
                    found = not await cur.fetchone()
                await cur.execute(
                    "INSERT INTO telegram_link_tokens (token, user_id, `expire`) VALUES (%s, %s, %s)",
                    (token, session.user_id, int(time.time()) + 3600))
            else:
                token = token["token"]
            await conn.commit()

    # Ok!
    bot_username = (await EmaNews().bot.get_me())["username"]
    return web.json_response({
        "telegram_link":
        "https://telegram.me/{}?start={}".format(bot_username, token)
    })
예제 #16
0
async def handle(request: Request):
    token = request.match_info["token"].strip()

    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "SELECT id, user_id FROM activation_tokens WHERE token = %s LIMIT 1",
                (token, ))
            db_token = await cur.fetchone()
            if not db_token:
                raise NotFoundError("Token di attivazione non valido")
            await cur.execute(
                "DELETE FROM activation_tokens WHERE id = %s LIMIT 1",
                (db_token["id"], ))
            await cur.execute(
                "UPDATE users SET privileges = %s WHERE id = %s LIMIT 1",
                (int(Privileges.NORMAL), db_token["user_id"]))
            await conn.commit()

    # Ok!
    return web.json_response({"message": "ok"})
예제 #17
0
async def handle(request: Request, *, params):
    async with EmaNews().db.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("SELECT * FROM users WHERE email = %s LIMIT 1",
                              (params["email"], ))
            db_user = await cur.fetchone()
            if not db_user:
                raise NotFoundError(
                    "L'indirizzo email inserito non è associato a nessun account"
                )
            if not bcrypt.checkpw(params["password"], db_user["password"]):
                raise ForbiddenError("La password inserita è errata")
            if db_user["privileges"] & Privileges.PENDING_ACTIVATION:
                raise ForbiddenError(
                    "L'account non è ancora stato attivato. Per favore, controlla la tua casella "
                    "di posta elettronica e clicca sul link che hai ricevuto per verificare "
                    "il tuo account.")

    session = await SessionFactory.new_redis_session(db_user["id"])
    resp = web.json_response({"message": "ok"})
    resp.set_cookie("session",
                    session.token,
                    max_age=RedisSession.SESSION_EXPIRE_TIME)  # , secure=True)
    return resp
예제 #18
0
async def handle(request: Request, *, session: Session, params):
    herbs = []

    # Condizioni WHERE
    filters: List[str] = []
    if params["query"]:
        # WHERE per query ricerca
        params["query"] = "%{}%".format(params["query"])
        filters.append("({})".format(" OR ".join([
            "{} LIKE %(query)s".format(x)
            for x in ('latin_name', 'botanic_name', 'english_name')
        ])))

    # Join AND delle condizioni del WHERE
    filters_string = " AND ".join(filters)

    # Clausola LIMIT
    if not params["limit"]:
        # No LIMIT
        limit = ""
    elif params["limit"] and not params["page"]:
        # LIMIT senza pagina
        limit = "LIMIT {}".format(params["limit"])
    else:
        # LIMIT con pagina
        limit = "LIMIT {}, {}".format(params["page"] * params["limit"],
                                      params["limit"])

    # Costruisci due query identiche (results_query e count_query)
    # per prendere i risultati che rispecchiano gli argomenti forniti
    # e i risultati totali (query necessaria poichè è possibile implementare la clausola LIMIT)
    results_query, count_query = [
        "SELECT {what} FROM herbs {where} {order_by} ".format(
            what=x,
            where="WHERE {}".format(filters_string) if filters_string else "",
            order_by="ORDER BY {} {}".format(params["order_by"],
                                             params["direction"]))
        for x in ("*", "COUNT(*) AS count")
    ]

    # Aggiunti il LIMIT nella query dei risultati DOPO la comprehension
    # in questo modo count_query non ha la clausola LIMIT
    results_query += limit

    # Prima connessione con SSDictCursor (per prendere X righe alla volta e non tutte le righe insieme,
    # in questo modo si riduce notevolmente l'uso di memoria durante l'esecuzione della query quando
    # ci sono query che ritornano molte righe)
    async with EmaNews().db.acquire() as conn:
        async with conn.cursor(SSDictCursor) as cur:
            logging.debug(results_query)
            logging.debug(params)
            await cur.execute(results_query, {**params})

            # Recupera tutti i chunk
            while True:
                herbs_chunk = await cur.fetchmany()
                if not herbs_chunk:
                    break

                # Per ogni erba nel chunk...
                for herb in herbs_chunk:
                    # ...recupera i documenti se richiesto
                    if params["fetch_documents"]:
                        # Nuova connessione (DictCursor classico, poichè i documenti per ogni erba sono pochi)
                        async with EmaNews().db.acquire() as doc_conn:
                            async with doc_conn.cursor() as doc_cur:
                                await doc_cur.execute(
                                    "SELECT * FROM documents WHERE herb_id = %s",
                                    (herb["id"], ))
                                herb["documents"] = await doc_cur.fetchall()

                    # Aggiungi l'erba + documenti alla lista totale delle erbe
                    herbs.append(herb)

    # Determina il numero totale di righe
    if not limit:
        # Se non c'è la clausola LIMIT, il numero totale di risultati è uguale
        # alla lunghezza di total_herbs
        total_herbs = len(herbs)
    else:
        # Altrimenti, apri una nuova connessione ed esegui count_query
        # (uguale alla query per il fetch dei risultati, ma senza clausola
        # LIMIT e con SELECT COUNT(*) invece che SELECT *
        async with EmaNews().db.acquire() as conn:
            async with conn.cursor() as cur:
                logging.debug(count_query)
                await cur.execute(count_query, {**params})
                total_herbs = await cur.fetchone()
                if not total_herbs:
                    total_herbs = 0
                else:
                    total_herbs = total_herbs["count"]

    # Ritorna il risultato al client HTTP
    return web.json_response({"herbs": herbs, "total": total_herbs})
예제 #19
0
async def handle(request: Request):
    return web.json_response({"message": "ok", "version": EmaNews().VERSION})