EmailDone1
Some checks failed
K8S Fission Deployment / Deployment fission functions (push) Failing after 22s

This commit is contained in:
QuangMinh_123
2025-12-06 05:58:48 +07:00
parent e9e6e3f636
commit c871216ae0
15 changed files with 1144 additions and 303 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,7 +1,7 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
{
"name": "fission:ailbl-tag",
"name": "fission:ailbl-user-email",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
// "image": "mcr.microsoft.com/devcontainers/rust:0-1-bullseye",
// Use docker compose file
@@ -21,17 +21,17 @@
},
"extensions": [
// VS Code specific
"ms-azuretools.vscode-docker" ,
"dbaeumer.vscode-eslint" ,
"EditorConfig.EditorConfig" ,
"ms-azuretools.vscode-docker",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
// Python specific
"ms-python.python" ,
"ms-python.black-formatter" ,
"ms-python.python",
"ms-python.black-formatter",
// C++ specific
"ms-vscode.cpptools" ,
"twxs.cmake" ,
"ms-vscode.cpptools",
"twxs.cmake",
// Markdown specific
"yzhang.markdown-all-in-one" ,
"yzhang.markdown-all-in-one",
// YAML formatter
"kennylong.kubernetes-yaml-formatter",
// hightlight and format `pyproject.toml`
@@ -39,7 +39,7 @@
]
}
},
"mounts": [ ],
"mounts": [],
// "runArgs": [
// "--env-file",
// ".devcontainer/.env"
@@ -47,4 +47,4 @@
"postStartCommand": "/workspaces/${localWorkspaceFolderBasename}/.devcontainer/initscript.sh",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": []
}
}

View File

@@ -135,7 +135,6 @@ spec:
port:
number: 80
EOF
# ## install without helm
# kubectl create -k "github.com/fission/fission/crds/v1?ref=${FISSION_VER}"
# kubectl create namespace $FISSION_NAMESPACE

View File

@@ -3,11 +3,13 @@
"secrets": {
"fission-ailbl-user-email-env": {
"literals": [
"S3_BUCKET=ailbl",
"S3_ENDPOINT_URL=http://160.30.113.113:9000",
"S3_ACCESS_KEY_ID=quyen",
"S3_SECRET_ACCESS_KEY=12345678",
"S3_PREFIX=user/avatar"
"PG_HOST=160.30.113.113",
"PG_PORT=45432",
"PG_DB=postgres",
"PG_USER=postgres",
"PG_PASS=q2q32RQx9R9qVAp3vkVrrASnSUUhzKvC",
"KRATOS_ADMIN_ENDPOINT=http://160.30.113.113:4434",
"KRATOS_PUBLIC_ENDPOINT=http://160.30.113.113:4433"
]
}
}

View File

@@ -1,89 +0,0 @@
import crud
from flask import jsonify, request
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
def main():
"""
```fission
{
"name": "avatar-admin-get-insert-delete-put",
"http_triggers": {
"avatar-admin-get-insert-delete-put-http": {
"url": "/ailbl/admin/avatars",
"methods": ["PUT", "POST", "DELETE", "GET"]
}
}
}
```
"""
try:
if request.method == "PUT":
return make_update_avatar_request()
elif request.method == "DELETE":
return make_delete_avatar_request()
elif request.method == "POST":
return make_insert_request()
elif request.method == "GET":
return make_get_avatar_request()
else:
return {"error": "Method not allow"}, 405
except Exception as ex:
return jsonify({"error": str(ex)}), 500
def make_insert_request():
try:
user_id = request.headers.get("X-User")
file = request.files.get("avatar")
if not user_id or not file:
return jsonify({"error": "user_id or file is required"}), 400
if file.mimetype not in ALLOWED_IMAGE_TYPES:
return jsonify(
{"error": "Invalid file type. Only JPG, PNG, GIF, WEBP are allowed."}
), 400
response, status = crud.update_or_create_avatar(user_id, file)
return jsonify(response), status
except Exception as e:
return jsonify({"error": str(e)}), 500
def make_get_avatar_request():
try:
user_id = request.headers.get("X-User")
if not user_id:
return jsonify({"error": "user_id is required"}), 400
return crud.get_avatar_url(user_id)
except Exception as e:
return jsonify({"error": str(e)}), 500
def make_delete_avatar_request():
try:
user_id = request.headers.get("X-User")
if not user_id:
return jsonify({"error": "user_id is required"}), 400
response, status = crud.delete_avatar(user_id)
return jsonify(response), status
except Exception as e:
return jsonify({"error": str(e)}), 500
def make_update_avatar_request():
try:
user_id = request.headers.get("X-User")
file = request.files.get("avatar")
if not user_id or not file:
return jsonify({"error": "user_id or file is required"}), 400
if file.mimetype not in ALLOWED_IMAGE_TYPES:
return jsonify(
{"error": "Invalid file type. Only JPG, PNG, GIF, WEBP are allowed."}
), 400
response, status = crud.update_or_create_avatar(user_id, file)
return jsonify(response), status
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -0,0 +1,199 @@
import dataclasses
from typing import Optional
import typing
import crud
from flask import jsonify, request
from helpers import CORS_HEADERS, db_rows_to_array, kratos, str_to_bool
from pydantic import BaseModel, Field, ValidationError
from helpers import kratos, init_db_connection
from schemas import UserEmailRequest
@dataclasses.dataclass # Filter user bao nhieu email
class EmailFilter:
ids: typing.Optional[typing.List[str]] = None
email: typing.Optional[str] = None
provider: typing.Optional[str] = None
created_from: typing.Optional[str] = None
created_to: typing.Optional[str] = None
modified_from: typing.Optional[str] = None
modified_to: typing.Optional[str] = None
keywords: typing.Optional[str] = None
primary: typing.Optional[bool] = None
@classmethod
def from_request_queries(cls) -> "EmailFilter":
return cls(
ids=request.args.getlist("filter[ids]"),
email=request.args.get("filter[email]"),
provider=request.args.get("filter[provider]"),
created_from=request.args.get("filter[created_from]"),
created_to=request.args.get("filter[created_to]"),
modified_from=request.args.get("filter[modified_from]"),
modified_to=request.args.get("filter[modified_to]"),
keywords=request.args.get("filter[key]"),
primary=str_to_bool(request.args.get("filter[primary]"))
)
@dataclasses.dataclass
class Page:
page: int = 0
size: int = 10
asc: bool = False
@classmethod
def from_request_queries(cls) -> "Page":
return Page(
page=int(request.args.get("page", 0)),
size=int(request.args.get("size", 10)),
asc=request.args.get("asc", type=str_to_bool) or False
)
@dataclasses.dataclass
class EmailPage(Page):
sortby: typing.Optional[str] = None
filter: EmailFilter = dataclasses.field(
default_factory=EmailFilter.from_request_queries)
@classmethod
def from_request_queries(cls) -> "EmailPage":
base = Page.from_request_queries()
return cls(**dataclasses.asdict(base), sortby=request.args.get("sortby"))
def main():
"""
```fission
{
"name": "email-admin-insert-get-filter",
"http_triggers": {
"email-admin-insert-get-filter-http": {
"url": "/ailbl/admin/users/{UserId}/emails ",
"methods": ["POST", "GET"]
}
}
}
```
"""
try:
if request.method == "POST":
return insert_email()
elif request.method == "GET":
return filter_emails()
else:
return {"error": "Method not allow"}, 405
except Exception as ex:
return jsonify({"error": str(ex)}), 500
def insert_email():
user_id = request.headers.get("X-Fission-Params-UserId")
if not user_id:
return jsonify({"errorCode": "USER_ID_REQUIRED"}), 400, CORS_HEADERS
try:
data = request.get_json()
if not data:
return jsonify({"errorCode": "NO_DATA_PROVIDED"}), 400, CORS_HEADERS
parsed = UserEmailRequest(**data)
except ValidationError as e:
return jsonify({"errorCode": "VALIDATION_ERROR", "details": e.errors()}), 422, CORS_HEADERS
except Exception as e:
return jsonify({"errorCode": "BAD_REQUEST"}), 400, CORS_HEADERS
try:
add_email, status_code, headers = crud.add_email_to_user(
user_id, parsed.email)
if parsed.is_primary:
# update email kratos
identity = kratos.get_identity(user_id)
traits = identity.traits
traits["email"] = parsed.email
res = kratos.update_identity(
id=user_id,
update_identity_body={
"schema_id": identity.schema_id,
"traits": traits,
"state": identity.state,
},
)
return jsonify(add_email), status_code, headers
except Exception as e:
return jsonify({"error": str(e)}), 500
def filter_emails():
paging = EmailPage.from_request_queries()
user_id = request.headers.get(
"X-Fission-Params-UserId") # X-Fission lay tren path
if not user_id:
return jsonify({"errorCode": "USER_ID_REQUIRED"}), 400, CORS_HEADERS
conn = None
try:
conn = init_db_connection()
with conn.cursor() as cursor:
records = __filter_email(cursor, paging, user_id)
return jsonify(
records,
), 200, CORS_HEADERS
except Exception as e:
# current_app.logger.error(f"[filter_emails] DB Error: {e}")
return jsonify({"errorCode": "DATABASE_ERROR"}), 500, CORS_HEADERS
finally:
if conn:
conn.close()
def __filter_email(cursor, paging: EmailPage, user_id: str):
conditions = ["user_id = %(user_id)s"]
values = {"user_id": user_id}
if paging.filter.ids:
conditions.append("id = ANY(%(ids)s)")
values["ids"] = paging.filter.ids
if paging.filter.email:
conditions.append("LOWER(email) LIKE %(email)s")
values["email"] = f"%{paging.filter.email.lower()}%"
if paging.filter.provider:
conditions.append("LOWER(provider) LIKE %(provider)s")
values["provider"] = f"%{paging.filter.provider.lower()}%"
if paging.filter.created_from:
conditions.append("created >= %(created_from)s")
values["created_from"] = paging.filter.created_from
if paging.filter.created_to:
conditions.append("created <= %(created_to)s")
values["created_to"] = paging.filter.created_to
if paging.filter.modified_from:
conditions.append("modified >= %(modified_from)s")
values["modified_from"] = paging.filter.modified_from
if paging.filter.modified_to:
conditions.append("modified <= %(modified_to)s")
values["modified_to"] = paging.filter.modified_to
if paging.filter.keywords:
conditions.append(
"(LOWER(email) LIKE %(keywords)s OR LOWER(provider) LIKE %(keywords)s)")
values["keywords"] = f"%{paging.filter.keywords.lower()}%"
where_clause = " AND ".join(conditions)
if where_clause:
where_clause = "WHERE " + where_clause
order_clause = ""
if paging.sortby:
direction = "ASC" if paging.asc else "DESC"
order_clause = f"ORDER BY {paging.sortby} {direction}"
sql = f"""
SELECT *, COUNT(*) OVER() AS total
FROM ailbl_user_email
{where_clause}
{order_clause}
LIMIT %(limit)s OFFSET %(offset)s
"""
values["limit"] = paging.size
values["offset"] = paging.page * paging.size
cursor.execute(sql, values)
return db_rows_to_array(cursor, cursor.fetchall())

View File

@@ -0,0 +1,190 @@
from typing import Optional
import crud
from email_validator import EmailNotValidError, validate_email
from flask import jsonify, request
from pydantic import BaseModel, Field, ValidationError
from sqlalchemy.orm import Session
from helpers import kratos, init_db_connection, CORS_HEADERS
def main():
"""
```fission
{
"name": "email-admin-delete",
"http_triggers": {
"email-admin-delete-http": {
"url": "/ailbl/admin/users/{UserId}/emails/{UserEmailId}",
"methods": ["DELETE"]
}
}
}
```
"""
try:
if request.method == "DELETE":
return delete_email()
else:
return {"error": "Method not allow"}, 405
except Exception as ex:
return jsonify({"error": str(ex)}), 500
def delete_email():
user_id = request.headers.get("X-Fission-Params-UserId")
email_id = request.headers.get("X-Fission-Params-UserEmailId")
if not user_id:
return jsonify({"errorCode": "USER_ID_REQUIRED"}), 400, CORS_HEADERS
if not email_id:
return jsonify({"errorCode": "USER_EMAIL_ID_REQUIRED"}), 400, CORS_HEADERS
# check if email exists
if exists_email(email_id) is False:
return jsonify({"errorCode": "EMAIL_NOT_FOUND"}), 404, CORS_HEADERS
# check if email is primary = email chinh
# config , status = get_config_account(user_id)
# if status == 200:
# email_primary = config["profile_setting"]["primary"]["email"]
# if check_is_primary(email_primary, email_id):
# return jsonify({"errorCode": "CANNOT_DELETE_PRIMARY_EMAIL"}), 400, CORS_HEADERS
# Proceed to delete the email
conn = None
cursor = None
try:
conn = init_db_connection()
cursor = conn.cursor()
cursor.execute(
"""
DELETE FROM ailbl_user_email
WHERE id = %s AND user_id = %s
RETURNING id;
""",
(email_id, user_id)
)
result = cursor.fetchone()
if not result:
return jsonify({"errorCode": "EMAIL_NOT_FOUND"}), 404, CORS_HEADERS
conn.commit()
return jsonify({
"id": email_id,
"status": "deleted"
}), 200, CORS_HEADERS
except Exception as e:
if conn:
conn.rollback()
# current_app.logger.error(f"[delete_email] Database error: {str(e)}")
return jsonify({"errorCode": "DATABASE_ERROR"}), 500, CORS_HEADERS
finally:
if cursor:
cursor.close()
if conn:
conn.close()
def exists_email(email_id: str) -> bool:
conn = None
cursor = None
try:
conn = init_db_connection()
with conn.cursor() as cursor:
cursor.execute("""
SELECT 1
FROM ailbl_user_email
WHERE id = %s;
""", (email_id,))
row = cursor.fetchone()
return row is not None
except Exception as e:
# current_app.logger.error(f"[exists_email] DB Error: {e}")
return False
finally:
if cursor:
cursor.close()
if conn:
conn.close()
# update email : set as primary
# def update_email():
# user_id = request.headers.get("X-Fission-Params-UserId")
# email_id = request.headers.get("X-Fission-Params-UserEmailId")
# request_data = request.get_json()
# is_primary = request_data.get("is_primary")
# if not user_id:
# return jsonify({"errorCode": "USER_ID_REQUIRED"}), 400, CORS_HEADERS
# if not email_id:
# return jsonify({"errorCode": "USER_EMAIL_ID_REQUIRED"}), 400, CORS_HEADERS
# if not is_primary:
# return jsonify({"errorCode": "NO_UPDATES_PROVIDED"}), 400, CORS_HEADERS
# # ensure config account exists
# # create_config_account_if_not_exists(user_id)
# # check if email exists
# email = get_email_by_id(email_id=email_id)
# if not email:
# return jsonify({"errorCode": "EMAIL_NOT_FOUND"}), 404, CORS_HEADERS
# # set as primary in config account
# if is_primary:
# identity = kratos.get_identity(user_id)
# traits = identity.traits
# traits["email"] = email
# res = kratos.update_identity(
# id=user_id,
# update_identity_body={
# "schema_id": identity.schema_id,
# "traits": traits,
# "state": identity.state,
# },
# )
# # update config email
# # r, status = create_or_update_config_account(email=email, key=user_id, type="update")
# # if status != 200:
# # return jsonify({"errorCode": "FAILED_TO_UPDATE_PRIMARY_EMAIL"}), status
# return jsonify({
# "id": email_id,
# "user_id": user_id,
# "email": email,
# "status": "set_as_primary"
# }), 200, CORS_HEADERS
# def get_email_by_id(email_id: str): # GET EMAIL ra id moi update duoc
# conn = None
# cursor = None
# try:
# conn = init_db_connection()
# with conn.cursor() as cursor:
# cursor.execute("""
# SELECT email
# FROM ailbl_user_email
# WHERE id = %s;
# """, (email_id,))
# row = cursor.fetchone()
# if row:
# return row[0]
# else:
# return None
# except Exception as e:
# # current_app.logger.error(f"[get_email_by_id] DB Error: {e}")
# return {"error": "DATABASE_ERROR"}, 500
# finally:
# if cursor:
# cursor.close()
# if conn:
# conn.close()

View File

@@ -1,95 +0,0 @@
import crud
from flask import jsonify, request
# from storage.minio_client import get_minio_client, check_existing_avatar_on_minio, upload_to_minio
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
def main():
"""
```fission
{
"name": "avatar-users-get-insert-delete-put",
"http_triggers": {
"avatar-users-get-insert-delete-put-http": {
"url": "/ailbl/users/avatars",
"methods": ["PUT", "POST", "DELETE", "GET"]
}
}
}
```
"""
try:
if request.method == "PUT":
return make_update_avatar_request()
elif request.method == "DELETE":
return make_delete_avatar_request()
elif request.method == "POST":
return make_insert_request()
elif request.method == "GET":
return make_get_avatar_request()
else:
return {"error": "Method not allow"}, 405
except Exception as ex:
return jsonify({"error": str(ex)}), 500
def make_insert_request():
try:
user_id = request.headers.get("X-User") # Lay user_id tu header X-User
# Lay file tu form-data voi key la 'avatar'
file = request.files.get("avatar")
if not user_id or not file:
return jsonify({"error": "user_id or file is required"}), 400
# Check mimetype(kieu du lieu cua file anh)
if file.mimetype not in ALLOWED_IMAGE_TYPES:
return jsonify(
{"error": "Invalid file type. Only JPG, PNG, GIF, WEBP are allowed."}
), 400
response, status = crud.update_or_create_avatar(user_id, file)
return jsonify(response), status
except Exception as e:
return jsonify({"error": str(e)}), 500
def make_update_avatar_request():
try:
# Lay user_id tu header X-User, neu co giao dien roi thi cookies se tu dong duoc gui len o trong header
user_id = request.headers.get("X-User")
# Lay file tu form-data voi key la 'avatar'
file = request.files.get("avatar")
if not user_id or not file:
return jsonify({"error": "user_id or file is required"}), 400
# Check mimetype(kieu du lieu cua file anh)
if file.mimetype not in ALLOWED_IMAGE_TYPES:
return jsonify(
{"error": "Invalid file type. Only JPG, PNG, GIF, WEBP are allowed."}
), 400
response, status = crud.update_or_create_avatar(
user_id, file) # Call CRUD function to update avatar
return jsonify(response), status
except Exception as e:
return jsonify({"error": str(e)}), 500
def make_delete_avatar_request():
try:
user_id = request.headers.get("X-User") # Lay user_id tu header X-User
if not user_id:
return jsonify({"error": "user_id is required"}), 400
# Call CRUD function to delete avatar
response, status = crud.delete_avatar(user_id)
return jsonify(response), status
except Exception as e:
return jsonify({"error": str(e)}), 500
def make_get_avatar_request():
try:
user_id = request.headers.get("X-User")
if not user_id:
return jsonify({"error": "user_id is required"}), 400
return crud.get_avatar_url(user_id)
# return jsonify(response), status
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -0,0 +1,112 @@
from typing import Optional
import crud
from email_validator import EmailNotValidError, validate_email
from flask import jsonify, request
from pydantic import BaseModel, Field, ValidationError
from sqlalchemy.orm import Session
from helpers import kratos, init_db_connection, CORS_HEADERS
def main():
"""
```fission
{
"name": "email-users-delete",
"http_triggers": {
"email-users-delete-http": {
"url": "/ailbl/users/emails/{UserEmailId}",
"methods": ["DELETE"]
}
}
}
```
"""
try:
if request.method == "DELETE":
return delete_email()
else:
return {"error": "Method not allow"}, 405
except Exception as ex:
return jsonify({"error": str(ex)}), 500
def delete_email():
user_id = request.headers.get("X-UserId")
email_id = request.headers.get("X-Fission-Params-UserEmailId")
if not user_id:
return jsonify({"errorCode": "USER_ID_REQUIRED"}), 400, CORS_HEADERS
if not email_id:
return jsonify({"errorCode": "USER_EMAIL_ID_REQUIRED"}), 400, CORS_HEADERS
# check if email exists
if exists_email(email_id) is False:
return jsonify({"errorCode": "EMAIL_NOT_FOUND"}), 404, CORS_HEADERS
# check if email is primary = email chinh
# config , status = get_config_account(user_id)
# if status == 200:
# email_primary = config["profile_setting"]["primary"]["email"]
# if check_is_primary(email_primary, email_id):
# return jsonify({"errorCode": "CANNOT_DELETE_PRIMARY_EMAIL"}), 400, CORS_HEADERS
# Proceed to delete the email
conn = None
cursor = None
try:
conn = init_db_connection()
cursor = conn.cursor()
cursor.execute(
"""
DELETE FROM ailbl_user_email
WHERE id = %s AND user_id = %s
RETURNING id;
""",
(email_id, user_id)
)
result = cursor.fetchone()
if not result:
return jsonify({"errorCode": "EMAIL_NOT_FOUND"}), 404, CORS_HEADERS
conn.commit()
return jsonify({
"id": email_id,
"status": "deleted"
}), 200, CORS_HEADERS
except Exception as e:
if conn:
conn.rollback()
# current_app.logger.error(f"[delete_email] Database error: {str(e)}")
return jsonify({"errorCode": "DATABASE_ERROR"}), 500, CORS_HEADERS
finally:
if cursor:
cursor.close()
if conn:
conn.close()
def exists_email(email_id: str) -> bool:
conn = None
cursor = None
try:
conn = init_db_connection()
with conn.cursor() as cursor:
cursor.execute("""
SELECT 1
FROM ailbl_user_email
WHERE id = %s;
""", (email_id,))
row = cursor.fetchone()
return row is not None
except Exception as e:
# current_app.logger.error(f"[exists_email] DB Error: {e}")
return False
finally:
if cursor:
cursor.close()
if conn:
conn.close()

View File

@@ -0,0 +1,200 @@
import dataclasses
from typing import Optional
import typing
import crud
from flask import jsonify, request
from helpers import CORS_HEADERS, db_rows_to_array, kratos, str_to_bool
from pydantic import BaseModel, Field, ValidationError
from helpers import kratos, init_db_connection
from schemas import UserEmailRequest
@dataclasses.dataclass # Filter user bao nhieu email
class EmailFilter:
ids: typing.Optional[typing.List[str]] = None
email: typing.Optional[str] = None
provider: typing.Optional[str] = None
created_from: typing.Optional[str] = None
created_to: typing.Optional[str] = None
modified_from: typing.Optional[str] = None
modified_to: typing.Optional[str] = None
keywords: typing.Optional[str] = None
primary: typing.Optional[bool] = None
@classmethod
def from_request_queries(cls) -> "EmailFilter":
return cls(
ids=request.args.getlist("filter[ids]"),
email=request.args.get("filter[email]"),
provider=request.args.get("filter[provider]"),
created_from=request.args.get("filter[created_from]"),
created_to=request.args.get("filter[created_to]"),
modified_from=request.args.get("filter[modified_from]"),
modified_to=request.args.get("filter[modified_to]"),
keywords=request.args.get("filter[key]"),
primary=str_to_bool(request.args.get("filter[primary]"))
)
@dataclasses.dataclass
class Page:
page: int = 0
size: int = 10
asc: bool = False
@classmethod
def from_request_queries(cls) -> "Page":
return Page(
page=int(request.args.get("page", 0)),
size=int(request.args.get("size", 10)),
asc=request.args.get("asc", type=str_to_bool) or False
)
@dataclasses.dataclass
class EmailPage(Page):
sortby: typing.Optional[str] = None
filter: EmailFilter = dataclasses.field(
default_factory=EmailFilter.from_request_queries)
@classmethod
def from_request_queries(cls) -> "EmailPage":
base = Page.from_request_queries()
return cls(**dataclasses.asdict(base), sortby=request.args.get("sortby"))
def main():
"""
```fission
{
"name": "email-users-insert-get-filter",
"http_triggers": {
"email-users-insert-get-filter-http": {
"url": "/ailbl/users/emails",
"methods": ["POST", "GET"]
}
}
}
```
"""
try:
if request.method == "POST":
return user_insert_email()
elif request.method == "GET":
return user_filter_emails()
else:
return {"error": "Method not allow"}, 405
except Exception as ex:
return jsonify({"error": str(ex)}), 500
def user_insert_email():
user_id = request.headers.get("X-UserId")
if not user_id:
return jsonify({"errorCode": "USER_ID_REQUIRED"}), 400, CORS_HEADERS
try:
data = request.get_json()
if not data:
return jsonify({"errorCode": "NO_DATA_PROVIDED"}), 400, CORS_HEADERS
parsed = UserEmailRequest(**data) # parsed luu du lieu gi ?
except ValidationError as e:
return jsonify({"errorCode": "VALIDATION_ERROR", "details": e.errors()}), 422, CORS_HEADERS
except Exception as e:
return jsonify({"errorCode": "BAD_REQUEST"}), 400, CORS_HEADERS
try:
add_email, status_code, headers = crud.add_email_to_user(
user_id, parsed.email)
if parsed.is_primary:
# update email kratos
identity = kratos.get_identity(user_id)
traits = identity.traits
traits["email"] = parsed.email
res = kratos.update_identity(
id=user_id,
update_identity_body={
"schema_id": identity.schema_id,
"traits": traits,
"state": identity.state,
},
)
return jsonify(add_email), status_code, headers
except Exception as e:
return jsonify({"error": str(e)}), 500
def user_filter_emails():
paging = EmailPage.from_request_queries()
user_id = request.headers.get(
"X-UserId") # X-Fission lay tren path
if not user_id:
return jsonify({"errorCode": "USER_ID_REQUIRED"}), 400, CORS_HEADERS
conn = None
try:
conn = init_db_connection()
with conn.cursor() as cursor:
records = __filter_email(cursor, paging, user_id) # Xu ly gi day ?
return jsonify(
records,
), 200, CORS_HEADERS
except Exception as e:
# current_app.logger.error(f"[filter_emails] DB Error: {e}")
return jsonify({"errorCode": "DATABASE_ERROR"}), 500, CORS_HEADERS
finally:
if conn:
conn.close()
def __filter_email(cursor, paging: EmailPage, user_id: str):
conditions = ["user_id = %(user_id)s"]
values = {"user_id": user_id}
if paging.filter.ids:
conditions.append("id = ANY(%(ids)s)")
values["ids"] = paging.filter.ids
if paging.filter.email:
conditions.append("LOWER(email) LIKE %(email)s")
values["email"] = f"%{paging.filter.email.lower()}%"
if paging.filter.provider:
conditions.append("LOWER(provider) LIKE %(provider)s")
values["provider"] = f"%{paging.filter.provider.lower()}%"
if paging.filter.created_from:
conditions.append("created >= %(created_from)s")
values["created_from"] = paging.filter.created_from
if paging.filter.created_to:
conditions.append("created <= %(created_to)s")
values["created_to"] = paging.filter.created_to
if paging.filter.modified_from:
conditions.append("modified >= %(modified_from)s")
values["modified_from"] = paging.filter.modified_from
if paging.filter.modified_to:
conditions.append("modified <= %(modified_to)s")
values["modified_to"] = paging.filter.modified_to
if paging.filter.keywords:
conditions.append(
"(LOWER(email) LIKE %(keywords)s OR LOWER(provider) LIKE %(keywords)s)")
values["keywords"] = f"%{paging.filter.keywords.lower()}%"
where_clause = " AND ".join(conditions)
if where_clause:
where_clause = "WHERE " + where_clause
order_clause = ""
if paging.sortby:
direction = "ASC" if paging.asc else "DESC"
order_clause = f"ORDER BY {paging.sortby} {direction}"
sql = f"""
SELECT *, COUNT(*) OVER() AS total
FROM ailbl_user_email
{where_clause}
{order_clause}
LIMIT %(limit)s OFFSET %(offset)s
"""
values["limit"] = paging.size
values["offset"] = paging.page * paging.size
cursor.execute(sql, values)
return db_rows_to_array(cursor, cursor.fetchall())

View File

@@ -1,65 +1,286 @@
import io
from flask import Response
from helpers import S3_BUCKET, get_secret, s3_client
from PIL import Image
import json
import urllib.parse
import uuid
from typing import List
from flask import current_app
import requests
from helpers import CORS_HEADERS, get_secret, init_db_connection, kratos
import uuid as uuid_lib
# Create&Update function to upload or update user avatar S3/Minio
def update_or_create_avatar(user_id: str, file):
def get_user_email_from_kratos(user_id: str): # Get email from Kratos
try:
file_data = file.read()
# Bản chất là đường dẫn trong bucket + tên file = user_id
object_name = f"{get_secret('S3_PREFIX')}/{user_id}"
result = s3_client.put_object(
Bucket=S3_BUCKET,
Key=object_name,
Body=io.BytesIO(file_data),
ContentLength=len(file_data),
ContentType=file.content_type,
)
return result, 200
current_app.logger.info(
f"[get_user_email_from_kratos] Checking Kratos connection for user_id={user_id}")
identity = kratos.get_identity(id=user_id)
current_app.logger.info(
f"[get_user_email_from_kratos] Kratos response: {identity}")
return identity.traits.get("email")
except Exception as e:
return {"error": str(e)}, 500
current_app.logger.error(
f"[get_user_email_from_kratos] Error connecting to Kratos: {e}")
return None
def get_avatar_url(user_id: str): # Read function to get user avatar from S3/Minio
def get_emails_by_user(user_id: str):
conn = None
try:
response = s3_client.get_object(
Bucket=S3_BUCKET,
Key=f"{get_secret('S3_PREFIX')}/{user_id}"
)
# image_data = response["body"].read(content_type)
image_data = response['Body'].read()
with Image.open(io.BytesIO(image_data)) as img:
fmt = img.format.lower() # ví dụ: 'jpeg', 'png', 'webp'
content_type = f"image/{'jpeg' if fmt == 'jpg' else fmt}"
# return Response(
# io.BytesIO(image_data),
# content_type=content_type,
# direct_passthrough=True,
# )
return Response(
image_data,
content_type=content_type,
direct_passthrough=True
), 200
conn = init_db_connection()
with conn.cursor() as cursor:
cursor.execute("""
SELECT id, user_id, email, provider, created, modified
FROM ailbl_user_email
WHERE user_id = %s
""", (user_id,))
rows = cursor.fetchall()
emails = []
for row in rows:
emails.append({
"id": row[0],
"user_id": row[1],
"email": row[2],
"provider": row[3],
"created": row[4].isoformat(),
"modified": row[5].isoformat()
})
return emails, 200, CORS_HEADERS
except Exception as e:
return {"error": str(e)}, 500
current_app.logger.error(f"[get_emails_by_user] DB Error: {str(e)}")
return {"error": "DATABASE_ERROR"}, 500, CORS_HEADERS
finally:
if conn:
conn.close()
# Delete Function to delete user avatar from S3/Minio
def delete_avatar(user_id: str) -> dict:
# def create_or_update_config_account(email: str, key: str, type: str = "create"):
# url_get_config = f"{get_secret('API_CONFIG_USER')}/{get_secret('KEY_CONFIG_USER')}"
# if type == "update":
# url_get_config = f"{get_secret('API_CONFIG_USER')}/{get_secret('PREFIX_KEY_CONFIG_USER')}_{key}"
# headers = {}
# payload = {}
# response = requests.request(
# "GET", url_get_config, headers=headers, data=payload)
# if response.status_code != 200:
# url_get_config = (
# f"{get_secret('API_CONFIG_USER')}/{get_secret('KEY_CONFIG_USER')}"
# )
# response = requests.request(
# "GET", url_get_config, headers=headers, data=payload
# )
# if response.status_code != 200:
# return {"error": "Failed to retrieve configuration"}, response.status_code
# config_user = response.json()
# if email != "":
# config_user["profile_setting"]["primary"]["email"] = email
# config_user["user_id"] = key
# json_str = json.dumps(config_user) # Convert dict to JSON string
# encoded_json = urllib.parse.quote(json_str) # URL encode
# if (
# type == "update"
# and url_get_config
# != f"{get_secret('API_CONFIG_USER')}/{get_secret('KEY_CONFIG_USER')}"
# ):
# url_save = f"{get_secret('API_CONFIG_USER')}/{get_secret('PREFIX_KEY_CONFIG_USER')}_{key}"
# payload = (
# f"content={encoded_json}"
# f"&mime_type=application/json"
# f"&public=true"
# f"&active=true"
# f"&refer=GEOHUB"
# )
# method = "PUT"
# else:
# url_save = f"{get_secret('API_CONFIG_USER')}"
# payload = f"content={encoded_json}&key={get_secret('PREFIX_KEY_CONFIG_USER')}_{key}&mime_type=application/json&public=true&active=true"
# method = "POST"
# headers = {"Content-Type": "application/x-www-form-urlencoded"}
# response_save = requests.request(
# method, url_save, headers=headers, data=payload)
# return response_save.json(), response_save.status_code
# def get_config_account(key: str):
# url_get_config = (
# f"{get_secret('API_CONFIG_USER')}/{get_secret('PREFIX_KEY_CONFIG_USER')}_{key}"
# )
# headers = {}
# payload = {}
# response = requests.request(
# "GET", url_get_config, headers=headers, data=payload)
# if response.status_code != 200:
# email = get_user_email_from_kratos(key)
# if not email:
# return {"error": "User email not found"}, 404
# res, status = create_or_update_config_account(
# email=email, key=key, type="create"
# )
# if status != 200:
# return res, status
# email_new, email_status, headers = add_email_to_user(
# user_id=key, email=email)
# if email_status != 200:
# return email_new, email_status
# response = requests.request(
# "GET", url_get_config, headers=headers, data=payload
# )
# return response.json(), response.status_code
# def create_config_account_if_not_exists(user_id: str):
# url_get_config = (
# f"{get_secret('API_CONFIG_USER')}/{get_secret('PREFIX_KEY_CONFIG_USER')}_{user_id}"
# )
# headers = {}
# payload = {}
# response = requests.request(
# "GET", url_get_config, headers=headers, data=payload)
# if response.status_code != 200:
# email = get_user_email_from_kratos(user_id)
# if not email:
# return {"error": "User email not found"}, 404
# res, status = create_or_update_config_account(
# email=email, key=user_id, type="create"
# )
# if status != 200:
# return res, status
# email_new, email_status, headers = add_email_to_user(
# user_id=user_id, email=email)
# if email_status != 200:
# return email_new, email_status
# return {"message": "Config account exists or created successfully"}, 200
def add_email_to_user(user_id: str, email: str): # Add email
conn = None
cursor = None
try:
result = s3_client.delete_object(
Bucket=S3_BUCKET,
Key=f"{get_secret('S3_PREFIX')}/{user_id}"
)
return result, 200
conn = init_db_connection()
with conn.cursor() as cursor:
cursor.execute("""
SELECT user_id FROM ailbl_user_email WHERE email = %s;
""", (email,))
existing = cursor.fetchone()
if existing:
return {"errorCode": "EMAIL_ALREADY_EXISTS"}, 409, CORS_HEADERS
id = str(uuid_lib.uuid4())
provider = auto_detect_provider(email)
cursor.execute("""
INSERT INTO ailbl_user_email (id, user_id, email, provider, created, modified)
VALUES (%s, %s, %s, %s, now(), now())
RETURNING id;
""", (id, user_id, email, provider))
email_id = cursor.fetchone()[0]
conn.commit()
return {
"id": email_id,
"user_id": user_id,
"email": email,
"provider": provider,
"status": "added"
}, 200, CORS_HEADERS
except Exception as e:
return {"error": str(e)}, 500
current_app.logger.error(f"[add_email_to_user] DB Error: {e}")
return {"error": "DATABASE_ERROR"}, 500, CORS_HEADERS
finally:
if cursor:
cursor.close()
if conn:
conn.close()
# Check provide lay phan duoi de xem ai la nha cung cap
def auto_detect_provider(email: str) -> str:
domain_part = email.split('@')[-1]
return domain_part.split('.')[0]
def get_max_mail(): # Setting config , setting user toi da duoc luu bao nhieu email
url = f"{get_secret('KEY_MAX_EMAIL_PER_USER')}"
try:
max_email = requests.get(url).json()
return int(max_email)
except Exception as e:
current_app.logger.error(
f"[get_max_email_per_user] Error retrieving max email per user: {str(e)}")
return None
def cout_email(user_id): # kiem tra email cua 1 user, roi dem email cua user do roi tra ra
conn = None
try:
conn = init_db_connection()
with conn.cursor() as cursor:
cursor.execute("""
SELECT COUNT(*) FROM ailbl_user_email
WHERE user_id = %s
""", (user_id,))
count = cursor.fetchone()[0]
return count
except Exception as e:
current_app.logger.error(f"[count_user_emails] DB Error: {str(e)}")
return 0
finally:
if conn:
conn.close()
current_app.logger.info("Closed DB connection")
def get_email_by_id(email_id: str): #Admin update email, update email phai goi ra duoc
conn = None
cursor = None
try:
conn = init_db_connection()
with conn.cursor() as cursor:
cursor.execute("""
SELECT email
FROM ailbl_user_email
WHERE id = %s;
""", (email_id,))
row = cursor.fetchone()
if row:
return row[0]
else:
return None
except Exception as e:
current_app.logger.error(f"[get_email_by_id] DB Error: {e}")
return {"error": "DATABASE_ERROR"}, 500
finally:
if cursor:
cursor.close()
if conn:
conn.close()
def exists_email(email_id: str) -> bool:
conn = None
cursor = None
try:
conn = init_db_connection()
with conn.cursor() as cursor:
cursor.execute("""
SELECT 1
FROM ailbl_user_email
WHERE id = %s;
""", (email_id,))
row = cursor.fetchone()
return row is not None
except Exception as e:
current_app.logger.error(f"[exists_email] DB Error: {e}")
return False
finally:
if cursor:
cursor.close()
if conn:
conn.close()

View File

@@ -1,35 +1,131 @@
import datetime
import logging
import boto3
SECRET_NAME = "fission-ailbl-user-avatar-env"
import socket
import typing
import ory_kratos_client
from ory_kratos_client.api import identity_api
from ory_kratos_client.configuration import Configuration
import psycopg2
from flask import current_app
from psycopg2.extras import LoggingConnection
CORS_HEADERS = {
"Content-Type": "application/json",
}
SECRET_NAME = "fission-ailbl-user-email-env"
CONFIG_NAME = "fission-eom-notification-config"
K8S_NAMESPACE = "default"
KRATOS_ADMIN_ENDPOINT_CONFIG_KEY = "KRATOS_ADMIN_ENDPOINT"
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def init_db_connection():
db_host = get_secret("PG_HOST", "locahost")
db_port = int(get_secret("PG_PORT", 55432))
if not check_port_open(ip=db_host, port=db_port):
raise Exception(
f"Establishing A Database Connection. {db_host}:{db_port}")
# options = get_secret("PG_DBSCHEMA")
# if options:
# options = f"-c search_path={options}" # if specific db schema
conn = psycopg2.connect(
database=get_secret("PG_DB", "postgres"),
user=get_secret("PG_USER", "postgres"),
password=get_secret("PG_PASS", "secret"),
host=get_secret("PG_HOST", "127.0.0.1"),
port=int(get_secret("PG_PORT", 5432)),
# options=options,
# cursor_factory=NamedTupleCursor,
connection_factory=LoggingConnection,
)
conn.initialize(logger)
return conn
# def db_row_to_dict(cursor, row):
# record = {}
# for i, column in enumerate(cursor.description):
# data = row[i]
# if isinstance(data, datetime.datetime):
# data = data.isoformat()
# record[column.name] = data
# return record
def db_row_to_dict(cursor, row):
record = {}
for i, column in enumerate(cursor.description):
data = row[i]
if isinstance(data, (datetime.datetime, datetime.date)):
data = data.isoformat()
record[column.name] = data
return record
def db_rows_to_array(cursor, rows):
return [db_row_to_dict(cursor, row) for row in rows]
def get_current_namespace() -> str:
try:
with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
namespace = f.read()
except:
except Exception as err:
current_app.logger.error(err)
namespace = K8S_NAMESPACE
return str(namespace)
def get_secret(key: str, default=None) -> str:
def get_secret(key: str, default=None):
namespace = get_current_namespace()
path = f"/secrets/{namespace}/{SECRET_NAME}/{key}"
try:
with open(path, "r") as f:
return f.read()
except:
except Exception as err:
current_app.logger.error(path, err)
return default
S3_BUCKET = get_secret("S3_BUCKET")
S3_PREFIX = get_secret("S3_PREFIX")
def get_config(key: str, default=None):
namespace = get_current_namespace()
path = f"/configs/{namespace}/{CONFIG_NAME}/{key}"
try:
with open(path, "r") as f:
return f.read()
except Exception as err:
current_app.logger.error(path, err)
return default
s3_client = boto3.client(
"s3",
endpoint_url=get_secret("S3_ENDPOINT_URL"),
aws_access_key_id=get_secret("S3_ACCESS_KEY_ID"),
aws_secret_access_key=get_secret("S3_SECRET_ACCESS_KEY"),
config=boto3.session.Config(signature_version="s3v4"),
)
def str_to_bool(value: str | None) -> typing.Optional[bool]:
if value is None:
return None
val = value.strip().lower()
if val in ("true", "1", "yes"):
return True
if val in ("false", "0", "no"):
return False
return None
def check_port_open(ip: str, port: int, timeout: int = 30):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
result = s.connect_ex((ip, port))
return result == 0
except Exception as err:
current_app.logger.err(f"Check port open error: {err}")
return False
kratos_config = Configuration(
host=get_secret(KRATOS_ADMIN_ENDPOINT_CONFIG_KEY))
kratos = identity_api.IdentityApi(ory_kratos_client.ApiClient(kratos_config))

View File

@@ -1,6 +1,8 @@
# Flask==3.1.0
# psycopg2-binary==2.9.10
# pydantic==2.11.3
# minio==7.2.5
# Pillow==10.4.0
# boto3==1.35.70
Flask==2.2.*
requests==2.32.3
psycopg2-binary==2.9.10
sqlalchemy==2.0.40
flask-sqlalchemy==3.1.1
email-validator==2.2.0
pydantic==2.11.3
ory-kratos-client==1.3.8

View File

@@ -1,33 +1,37 @@
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, EmailStr, root_validator
from typing import Optional
from enum import IntEnum
# Validate dau vao email
class UserEmailRequest(BaseModel):
email: EmailStr = Field(..., description="Email address")
provider: Optional[str] = Field(None, description="Email service provider")
is_primary: Optional[bool] = Field(None, description= "is primary email ")
@root_validator(pre=True)
def auto_detect_provider(cls, values):
email = values.get("email")
provider = values.get("provider")
if not provider and email:
domain_part = email.split('@')[-1]
values["provider"] = domain_part.split('.')[0]
return values
class TagKind(IntEnum):
ProjectGroup = 1
ProjectData = 2
ProjectMember = 3
ProjectDiscussionTopic = 4
Project = 5
Ticket = 6
class UserEmailUpdateRequest(BaseModel):
email: Optional[EmailStr] = Field(None, description="Email address")
provider: Optional[str] = Field(None, description="Email service provider")
is_primary: Optional[bool] = Field(None, description= "is primary email ")
@root_validator(pre=True)
def auto_detect_provider(cls, values):
email = values.get("email")
provider = values.get("provider")
class TagRequest(BaseModel):
tag: str = Field(..., max_length=128)
kind: TagKind
ref: Optional[str] = Field(default=None, max_length=36)
primary_color: Optional[str] = Field(default=None, max_length=8)
secondary_color: Optional[str] = Field(default=None, max_length=8)
if not provider and email:
domain_part = email.split('@')[-1]
values["provider"] = domain_part.split('.')[0]
class TagRequestUpdate(BaseModel):
tag: str = Field(..., max_length=128)
kind: TagKind
ref: Optional[str] = Field(default=None, max_length=36)
primary_color: Optional[str] = Field(default=None, max_length=8)
secondary_color: Optional[str] = Field(default=None, max_length=8)
class TagRefRequest(BaseModel):
ref: str = Field(..., max_length=64)
sub_ref: Optional[str] = Field(default=None, max_length=1024)
return values

View File

@@ -3,5 +3,5 @@
# Do not edit the UID below: that will break 'fission spec apply'
apiVersion: fission.io/v1
kind: DeploymentConfig
name: py-ailbl-tag
uid: dfd4b9c6-7e2f-4f57-aad5-38b34209eeb1
name: py-ailbl-user-email
uid: 93d0e78a-2e6f-4137-9651-e210a3e4623a