jcloud/jcloud/api/account.py
2025-06-27 18:50:23 +08:00

1736 lines
47 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright (c) 2019, JINGROW
# For license information, please see license.txt
from __future__ import annotations
import json
import re
from typing import TYPE_CHECKING
import jingrow
import pyotp
from jingrow import _
from jingrow.core.pagetype.user.user import update_password
from jingrow.core.utils import find
from jingrow.exceptions import DoesNotExistError
from jingrow.query_builder.custom import GROUP_CONCAT
from jingrow.rate_limiter import rate_limit
from jingrow.utils import cint, get_url, random_string
from jingrow.utils.data import sha256_hash
from jingrow.utils.oauth import get_oauth2_authorize_url, get_oauth_keys
from jingrow.utils.password import get_decrypted_password
from jingrow.website.utils import build_response
from pypika.terms import ValueWrapper
from jcloud.api.site import protected
from jcloud.jcloud.pagetype.team.team import (
Team,
get_child_team_members,
get_team_members,
)
from jcloud.utils import get_country_info, get_current_team, is_user_part_of_team
from jcloud.utils.telemetry import capture
if TYPE_CHECKING:
from jcloud.jcloud.pagetype.account_request.account_request import AccountRequest
@jingrow.whitelist(allow_guest=True)
def signup(email, product=None, referrer=None):
jingrow.utils.validate_email_address(email, True)
current_user = jingrow.session.user
jingrow.set_user("Administrator")
email = email.strip().lower()
exists, enabled = jingrow.db.get_value("Team", {"user": email}, ["name", "enabled"]) or [0, 0]
account_request = None
if exists and not enabled:
jingrow.throw(_("Account {0} has been deactivated").format(email))
elif exists and enabled:
jingrow.throw(_("Account {0} is already registered").format(email))
else:
account_request = jingrow.get_pg(
{
"pagetype": "Account Request",
"email": email,
"role": "Jcloud Admin",
"referrer_id": referrer,
"send_email": True,
"product_trial": product,
}
).insert()
jingrow.set_user(current_user)
if account_request:
return account_request.name
return None
return None
@jingrow.whitelist(allow_guest=True)
@rate_limit(limit=50, seconds=60 * 60)
def signup_with_username(username, password, email=None, phone_number=None, referrer=None, product=None):
"""
使用用户名注册新账户,邮箱和手机号为可选
"""
from jingrow.utils import validate_email_address
from jingrow.utils.password import update_password
current_user = jingrow.session.user
jingrow.set_user("Administrator") # 确保有权限创建用户
try:
# 验证用户名
if not username or len(username) < 3:
jingrow.throw("用户名至少需要3个字符")
# 检查用户名是否已存在
if jingrow.db.exists("User", {"username": username}):
jingrow.throw("该用户名已被使用")
# 如果提供了邮箱,验证邮箱格式并检查是否已存在
user_email = None
if email:
try:
validate_email_address(email, True)
except:
jingrow.throw("请输入有效的邮箱地址")
if jingrow.db.exists("User", {"email": email}):
jingrow.throw("该邮箱已被注册")
user_email = email
# 如果提供了手机号,验证手机号格式并检查是否已存在
if phone_number:
if not re.match(r'^1[3-9]\d{9}$', phone_number):
jingrow.throw("请输入有效的手机号码")
if jingrow.db.exists("User", {"mobile_no": phone_number}):
jingrow.throw("该手机号已被注册")
# 创建用户,但先不设置密码
user_pg = {
"pagetype": "User",
"first_name": username,
"username": username,
"send_welcome_email": 0,
"user_type": "Website User",
"language": "zh" # 设置默认语言为中文
}
# 只有提供了邮箱时才添加到用户数据
if user_email:
user_pg["email"] = user_email
# 如果提供了手机号,添加到用户数据
if phone_number:
user_pg["mobile_no"] = phone_number
user = jingrow.get_pg(user_pg)
# 添加角色 - 设置为 Jcloud Admin
user.append("roles", {
"role": "Jcloud Admin"
})
user.insert(ignore_permissions=True)
# 在创建用户后显式更新密码,这样可以确保密码被正确加密和保存
update_password(username, password)
# 创建团队
team_pg = {
"pagetype": "Team",
"enabled": 1,
"team_name": username,
"country": "China", # 默认设置为中国
"payment_mode": "Prepaid Credits", # 设置默认支付方式为余额支付
"user": username # 使用用户名作为团队用户标识
}
# 只有当提供有效邮箱时,才设置账单邮箱和通知邮箱
if user_email:
team_pg["billing_email"] = user_email
team_pg["notify_email"] = user_email
team = jingrow.get_pg(team_pg)
# 添加团队成员 - 使用用户名
team.append("team_members", {
"user": username
})
# 如果有邮箱,添加通讯邮箱
if user_email:
team.append("communication_emails", {
"type": "invoices",
"value": user_email
})
team.append("communication_emails", {
"type": "marketplace_notifications",
"value": user_email
})
# 插入团队
team.insert(ignore_permissions=True)
# 登录用户
jingrow.local.login_manager.login_as(username)
jingrow.db.commit()
# 返回与标准流程一致的响应
return {
"dashboard_route": ""
}
finally:
# 恢复原始用户
jingrow.set_user(current_user)
@jingrow.whitelist(allow_guest=True)
@rate_limit(limit=5, seconds=60)
def send_otp(email: str):
"""
向用户发送登录验证码
"""
if not jingrow.db.exists("User", email):
jingrow.throw("该邮箱尚未注册账户")
last_otp = jingrow.cache().get_value(f"login_otp_generated_at:{email}")
if last_otp and (jingrow.utils.now_datetime() - last_otp).seconds < 30:
jingrow.throw("请在30秒后再请求新的验证码")
# 生成6位数字OTP
import random
otp = ''.join(random.choices('0123456789', k=6))
expires_in_seconds = 300 # 5分钟
jingrow.cache().set_value(
f"login_otp:{email}",
otp,
expires_in_sec=expires_in_seconds
)
jingrow.cache().set_value(
f"login_otp_generated_at:{email}",
jingrow.utils.now_datetime(),
expires_in_sec=expires_in_seconds
)
if jingrow.conf.developer_mode:
print(f"\n登录OTP给 {email}: {otp}\n")
else:
# 获取用户全名
full_name = jingrow.db.get_value("User", email, "full_name") or email.split("@")[0]
jingrow.sendmail(
subject="Jingrow 登录验证码",
recipients=email,
template="verification_code_for_login",
args={
"otp": otp,
"minutes": expires_in_seconds // 60,
"full_name": full_name
},
now=True,
)
return True
@jingrow.whitelist(allow_guest=True)
@rate_limit(limit=5, seconds=60 * 60)
def verify_otp_and_login(email: str, otp: str):
"""
验证OTP并登录用户
"""
if not jingrow.db.exists("User", email):
jingrow.throw("该邮箱尚未注册账户")
stored_otp = jingrow.cache().get_value(f"login_otp:{email}")
if not stored_otp:
jingrow.throw("验证码已过期")
if stored_otp != otp:
jingrow.throw("验证码无效")
# 清除缓存
jingrow.cache().delete_value(f"login_otp:{email}")
jingrow.cache().delete_value(f"login_otp_generated_at:{email}")
# 登录用户
jingrow.local.login_manager.login_as(email)
# 登陆重定向
return {
"dashboard_route": ""
}
@jingrow.whitelist(allow_guest=True)
@rate_limit(limit=5, seconds=60 * 60)
def verify_otp(account_request: str, otp: str):
account_request: "AccountRequest" = jingrow.get_pg("Account Request", account_request)
# ensure no team has been created with this email
if jingrow.db.exists("Team", {"user": account_request.email}) and not account_request.product_trial:
jingrow.throw("Invalid OTP. Please try again.")
if account_request.otp != otp:
jingrow.throw("Invalid OTP. Please try again.")
account_request.reset_otp()
return account_request.request_key
@jingrow.whitelist(allow_guest=True)
@rate_limit(limit=5, seconds=60)
def resend_otp(account_request: str):
account_request: "AccountRequest" = jingrow.get_pg("Account Request", account_request)
# if last OTP was sent less than 30 seconds ago, throw an error
if (
account_request.otp_generated_at
and (jingrow.utils.now_datetime() - account_request.otp_generated_at).seconds < 30
):
jingrow.throw("Please wait for 30 seconds before requesting a new OTP")
# ensure no team has been created with this email
if jingrow.db.exists("Team", {"user": account_request.email}) and not account_request.product_trial:
jingrow.throw("Invalid Email")
account_request.reset_otp()
account_request.send_verification_email()
@jingrow.whitelist(allow_guest=True)
def setup_account( # noqa: C901
key,
first_name=None,
last_name=None,
password=None,
is_invitation=False,
country=None,
user_exists=False,
accepted_user_terms=False,
invited_by_parent_team=False,
oauth_signup=False,
oauth_domain=False,
):
account_request = get_account_request_from_key(key)
if not account_request:
jingrow.throw("Invalid or Expired Key")
if not user_exists:
if not first_name:
jingrow.throw("名字是必填项")
if not password and not (oauth_signup or oauth_domain):
jingrow.throw("密码是必填项")
if not is_invitation and not country:
jingrow.throw("国家是必填项")
if not is_invitation and country:
all_countries = jingrow.db.get_all("Country", pluck="name")
country = find(all_countries, lambda x: x.lower() == country.lower())
if not country:
jingrow.throw("Please provide a valid country name")
if not accepted_user_terms:
jingrow.throw("Please accept our Terms of Service & Privacy Policy to continue")
# if the request is authenticated, set the user to Administrator
jingrow.set_user("Administrator")
team = account_request.team
email = account_request.email
role = account_request.role
jcloud_roles = account_request.jcloud_roles
if is_invitation:
# if this is a request from an invitation
# then Team already exists and will be added to that team
pg = jingrow.get_pg("Team", team)
pg.create_user_for_member(first_name, last_name, email, password, role, jcloud_roles)
else:
# Team doesn't exist, create it
Team.create_new(
account_request=account_request,
first_name=first_name,
last_name=last_name,
password=password,
country=country,
user_exists=bool(user_exists),
)
if invited_by_parent_team:
pg = jingrow.get_pg("Team", account_request.invited_by)
pg.append("child_team_members", {"child_team": team})
pg.save()
# Telemetry: Created account
capture("completed_signup", "fc_signup", account_request.email)
jingrow.local.login_manager.login_as(email)
return account_request.name
@jingrow.whitelist(allow_guest=True)
@rate_limit(limit=5, seconds=60 * 60)
def send_login_link(email):
if not jingrow.db.exists("User", email):
jingrow.throw("No registered account with this email address")
key = jingrow.generate_hash("Login Link", 20)
minutes = 10
jingrow.cache().set_value(f"one_time_login_key:{key}", email, expires_in_sec=minutes * 60)
link = get_url(f"/api/action/jcloud.api.account.login_using_key?key={key}")
if jingrow.conf.developer_mode:
print()
print(f"One time login link for {email}")
print(link)
print()
jingrow.sendmail(
subject="Login to Jingrow",
recipients=email,
template="one_time_login_link",
args={"link": link, "minutes": minutes},
now=True,
)
@jingrow.whitelist(allow_guest=True)
@rate_limit(limit=5, seconds=60 * 60)
def login_using_key(key):
cache_key = f"one_time_login_key:{key}"
email = jingrow.cache().get_value(cache_key)
if email:
jingrow.cache().delete_value(cache_key)
jingrow.local.login_manager.login_as(email)
jingrow.response.type = "redirect"
jingrow.response.location = "/dashboard"
else:
jingrow.respond_as_web_page(
_("Not Permitted"),
_("The link using which you are trying to login is invalid or expired."),
http_status_code=403,
indicator_color="red",
)
@jingrow.whitelist()
def active_servers():
team = get_current_team()
return jingrow.get_all("Server", {"team": team, "status": "Active"}, ["title", "name"])
@jingrow.whitelist()
def disable_account(totp_code: str | None):
user = jingrow.session.user
team = get_current_team(get_pg=True)
if is_2fa_enabled(user):
if not totp_code:
jingrow.throw("2FA Code is required")
if not verify_2fa(user, totp_code):
jingrow.throw("Invalid 2FA Code")
if user != team.user:
jingrow.throw("Only team owner can disable the account")
team.disable_account()
@jingrow.whitelist()
def has_active_servers(team):
return jingrow.db.exists("Server", {"status": "Active", "team": team})
@jingrow.whitelist()
def enable_account():
team = get_current_team(get_pg=True)
if jingrow.session.user != team.user:
jingrow.throw("Only team owner can enable the account")
team.enable_account()
@jingrow.whitelist()
def request_team_deletion():
team = get_current_team(get_pg=True)
pg = jingrow.get_pg({"pagetype": "Team Deletion Request", "team": team.name}).insert()
return pg.name
@jingrow.whitelist(allow_guest=True)
def delete_team(team):
from jingrow.utils.verified_command import verify_request
responses = {
"invalid": [
("Link Invalid", "This link is invalid or expired."),
{"indicator_color": "red"},
],
"confirmed": [
(
"Confirmed",
f"The process for deletion of your team {team} has been initiated. Sorry to see you go :(",
),
{"indicator_color": "green"},
],
"expired": [
("Link Expired", "This link has already been activated for verification."),
{"indicator_color": "red"},
],
}
def respond_as_web_page(key):
jingrow.respond_as_web_page(*responses[key][0], **responses[key][1])
if verify_request() or jingrow.flags.in_test:
jingrow.set_user("Administrator")
else:
return respond_as_web_page("invalid")
try:
pg = jingrow.get_last_pg("Team Deletion Request", {"team": team})
except jingrow.DoesNotExistError:
return respond_as_web_page("invalid")
if pg.status != "Pending Verification":
return respond_as_web_page("expired")
pg.status = "Deletion Verified"
pg.save()
jingrow.db.commit()
return respond_as_web_page("confirmed")
@jingrow.whitelist(allow_guest=True)
def validate_request_key(key, timezone=None):
from jcloud.utils.country_timezone import get_country_from_timezone
account_request = get_account_request_from_key(key)
if account_request:
data = get_country_info()
possible_country = data.get("country") or get_country_from_timezone(timezone)
if not (account_request.is_saas_signup() or account_request.invited_by_parent_team):
capture("clicked_verify_link", "fc_signup", account_request.email)
return {
"email": account_request.email,
"first_name": account_request.first_name,
"last_name": account_request.last_name,
"country": possible_country,
"countries": jingrow.db.get_all("Country", pluck="name"),
"user_exists": jingrow.db.exists("User", account_request.email),
"team": account_request.team,
"is_invitation": jingrow.db.get_value("Team", account_request.team, "enabled"),
"invited_by": account_request.invited_by,
"invited_by_parent_team": account_request.invited_by_parent_team,
"oauth_signup": account_request.oauth_signup,
"oauth_domain": jingrow.db.exists(
"OAuth Domain Mapping", {"email_domain": account_request.email.split("@")[1]}
),
"product_trial": jingrow.db.get_value(
"Product Trial", account_request.product_trial, ["logo", "title", "name"], as_dict=1
),
}
return None
return None
@jingrow.whitelist(allow_guest=True)
def country_list():
def get_country_list():
return jingrow.db.get_all("Country", fields=["name", "code"])
return jingrow.cache().get_value("country_list", generator=get_country_list)
def clear_country_list_cache():
jingrow.cache().delete_value("country_list")
@jingrow.whitelist()
def set_country(country):
team_pg = get_current_team(get_pg=True)
team_pg.country = country
team_pg.save()
team_pg.create_stripe_customer()
def get_account_request_from_key(key):
"""Find Account Request using `key` in the past 12 hours or if site is active"""
if not key or not isinstance(key, str):
jingrow.throw(_("Invalid Key"))
hours = 12
ar = jingrow.get_pg("Account Request", {"request_key": key})
if ar.creation > jingrow.utils.add_to_date(None, hours=-hours):
return ar
if ar.subdomain and ar.saas_app:
domain = jingrow.db.get_value("Saas Settings", ar.saas_app, "domain")
if jingrow.db.get_value("Site", ar.subdomain + "." + domain, "status") == "Active":
return ar
return None
return None
@jingrow.whitelist()
def get():
cached = jingrow.cache.get_value("cached-account.get", user=jingrow.session.user)
if cached:
return cached
value = _get()
jingrow.cache.set_value("cached-account.get", value, user=jingrow.session.user, expires_in_sec=60)
return value
def _get():
user = jingrow.session.user
if not jingrow.db.exists("User", user):
jingrow.throw(_("Account does not exist"))
team_pg = get_current_team(get_pg=True)
parent_teams = [d.parent for d in jingrow.db.get_all("Team Member", {"user": user}, ["parent"])]
teams = []
if parent_teams:
Team = jingrow.qb.PageType("Team")
teams = (
jingrow.qb.from_(Team)
.select(Team.name, Team.team_title, Team.user)
.where((Team.enabled == 1) & (Team.name.isin(parent_teams)))
.run(as_dict=True)
)
partner_billing_name = ""
if team_pg.partner_email:
partner_billing_name = jingrow.db.get_value(
"Team",
{"jerp_partner": 1, "partner_email": team_pg.partner_email},
"billing_name",
)
number_of_sites = jingrow.db.count("Site", {"team": team_pg.name, "status": ("!=", "Archived")})
return {
"user": jingrow.get_pg("User", user),
"ssh_key": get_ssh_key(user),
"team": team_pg,
"team_members": get_team_members(team_pg.name),
"child_team_members": get_child_team_members(team_pg.name),
"teams": list(teams if teams else parent_teams),
"onboarding": team_pg.get_onboarding(),
"balance": team_pg.get_balance(),
"parent_team": team_pg.parent_team or "",
"saas_site_request": team_pg.get_pending_saas_site_request(),
"feature_flags": {
"verify_cards_with_micro_charge": jingrow.db.get_single_value(
"Jcloud Settings", "verify_cards_with_micro_charge"
)
},
"partner_email": team_pg.partner_email or "",
"partner_billing_name": partner_billing_name,
"number_of_sites": number_of_sites,
"permissions": get_permissions(),
"billing_info": team_pg.billing_info(),
}
@jingrow.whitelist()
def current_team():
user = jingrow.session.user
if not jingrow.db.exists("User", user):
jingrow.throw(_("Account does not exist"))
from jcloud.api.client import get
return get("Team", jingrow.local.team().name)
def get_permissions():
user = jingrow.session.user
groups = tuple(
[*jingrow.get_all("Jcloud Permission Group User", {"user": user}, pluck="parent"), "1", "2"]
) # [1, 2] is for avoiding singleton tuples
pageperms = jingrow.db.sql(
f"""
SELECT `document_name`, GROUP_CONCAT(`action`) as `actions`
FROM `tabJcloud User Permission`
WHERE user='{user}' or `group` in {groups}
GROUP BY `document_name`
""",
as_dict=True,
)
return {perm.document_name: perm.actions.split(",") for perm in pageperms if perm.actions}
@jingrow.whitelist()
def has_method_permission(pagetype, docname, method) -> bool:
from jcloud.jcloud.pagetype.jcloud_permission_group.jcloud_permission_group import (
has_method_permission,
)
return has_method_permission(pagetype, docname, method)
@jingrow.whitelist(allow_guest=True)
def signup_settings(product=None, fetch_countries=False, timezone=None):
from jcloud.utils.country_timezone import get_country_from_timezone
settings = jingrow.get_single("Jcloud Settings")
product = jingrow.utils.cstr(product)
product_trial = None
if product:
product_trial = jingrow.db.get_value(
"Product Trial",
{"name": product, "published": 1},
["title", "logo"],
as_dict=1,
)
data = {
"enable_google_oauth": settings.enable_google_oauth,
"product_trial": product_trial,
"oauth_domains": jingrow.get_all(
"OAuth Domain Mapping", ["email_domain", "social_login_key", "provider_name"]
),
}
if fetch_countries:
data["countries"] = jingrow.db.get_all("Country", pluck="name")
data["country"] = get_country_info().get("country") or get_country_from_timezone(timezone)
return data
@jingrow.whitelist(allow_guest=True)
def guest_feature_flags():
return {
"enable_google_oauth": jingrow.db.get_single_value("Jcloud Settings", "enable_google_oauth"),
}
@jingrow.whitelist()
def create_child_team(title):
team = title.strip()
current_team = get_current_team(True)
if title in [
d.team_title for d in jingrow.get_all("Team", {"parent_team": current_team.name}, ["team_title"])
]:
jingrow.throw(f"Child Team {title} already exists.")
elif title == "Parent Team":
jingrow.throw("Child team name cannot be same as parent team")
pg = jingrow.get_pg(
{
"pagetype": "Team",
"team_title": team,
"user": current_team.user,
"parent_team": current_team.name,
"enabled": 1,
}
)
pg.insert(ignore_permissions=True, ignore_links=True)
pg.append("team_members", {"user": current_team.user})
pg.save()
current_team.append("child_team_members", {"child_team": pg.name})
current_team.save()
return "created"
def new_team(email, current_team):
jingrow.utils.validate_email_address(email, True)
jingrow.get_pg(
{
"pagetype": "Account Request",
"email": email,
"role": "Jcloud Member",
"send_email": True,
"team": email,
"invited_by": current_team,
"invited_by_parent_team": 1,
}
).insert()
return "new_team"
def get_ssh_key(user):
ssh_keys = jingrow.get_all(
"User SSH Key", {"user": user, "is_default": True}, order_by="creation desc", limit=1
)
if ssh_keys:
return jingrow.get_pg("User SSH Key", ssh_keys[0])
return None
@jingrow.whitelist()
def update_profile(first_name=None, last_name=None, email=None, username=None, mobile_no=None):
if email:
# 更新用户所有相关邮箱
update_profile_email(email)
# 验证用户名(如果提供)
if username:
if len(username) < 3:
jingrow.throw("用户名至少需要3个字符")
# 检查用户名是否已被其他用户使用
user = jingrow.session.user
username_exists = jingrow.db.exists(
"User",
{"username": username, "name": ("!=", user)}
)
if username_exists:
jingrow.throw("该用户名已被使用")
# 验证手机号(如果提供)
if mobile_no:
import re
if not re.match(r'^1[3-9]\d{9}$', mobile_no):
jingrow.throw("请输入有效的手机号码")
# 检查手机号是否已被其他用户使用
phone_exists = jingrow.db.exists(
"User",
{"mobile_no": mobile_no, "name": ("!=", user)}
)
if phone_exists:
jingrow.throw("该手机号已被注册")
user = jingrow.session.user
pg = jingrow.get_pg("User", user)
pg.first_name = first_name
pg.last_name = last_name
if username:
pg.username = username
if mobile_no:
pg.mobile_no = mobile_no
pg.save(ignore_permissions=True)
return pg
@jingrow.whitelist()
def update_profile_picture():
user = jingrow.session.user
_file = jingrow.get_pg(
{
"pagetype": "File",
"attached_to_pagetype": "User",
"attached_to_name": user,
"attached_to_field": "user_image",
"folder": "Home/Attachments",
"file_name": jingrow.local.uploaded_filename,
"is_private": 0,
"content": jingrow.local.uploaded_file,
}
)
_file.save(ignore_permissions=True)
jingrow.db.set_value("User", user, "user_image", _file.file_url)
@jingrow.whitelist()
def update_feature_flags(values=None):
jingrow.only_for("Jcloud Admin")
team = get_current_team(get_pg=True)
values = jingrow.parse_json(values)
fields = [
"benches_enabled",
"servers_enabled",
"self_hosted_servers_enabled",
"security_portal_enabled",
]
for field in fields:
if field in values:
team.set(field, values[field])
team.save()
@jingrow.whitelist(allow_guest=True)
@rate_limit(limit=5, seconds=60 * 60)
def send_reset_password_email(email: str):
valid_email = jingrow.utils.validate_email_address(email)
if not valid_email:
jingrow.throw(
f"{email} is not a valid email address",
jingrow.InvalidEmailAddressError,
)
valid_email = valid_email.strip()
key = jingrow.generate_hash()
hashed_key = sha256_hash(key)
if jingrow.db.exists("User", valid_email):
jingrow.db.set_value(
"User",
valid_email,
{
"reset_password_key": hashed_key,
"last_reset_password_key_generated_on": jingrow.utils.now_datetime(),
},
)
url = get_url("/dashboard/reset-password/" + key)
if jingrow.conf.developer_mode:
print(f"\nReset password URL for {valid_email}:")
print(url)
print()
return
jingrow.sendmail(
recipients=valid_email,
subject="Reset Password",
template="reset_password",
args={"link": url},
now=True,
)
else:
jingrow.throw(f"User {valid_email} does not exist")
@jingrow.whitelist(allow_guest=True)
def reset_password(key, password):
return update_password(new_password=password, key=key)
@jingrow.whitelist(allow_guest=True)
def get_user_for_reset_password_key(key):
if not key or not isinstance(key, str):
jingrow.throw(_("Invalid Key"))
hashed_key = sha256_hash(key)
return jingrow.db.get_value("User", {"reset_password_key": hashed_key}, "name")
@jingrow.whitelist()
def remove_team_member(user_email):
team = get_current_team(True)
team.remove_team_member(user_email)
@jingrow.whitelist()
def remove_child_team(child_team):
team = jingrow.get_pg("Team", child_team)
sites = jingrow.get_all("Site", {"status": ("!=", "Archived"), "team": team.name}, pluck="name")
if sites:
jingrow.throw("Child team has Active Sites")
team.enabled = 0
team.parent_team = ""
team.save(ignore_permissions=True)
@jingrow.whitelist()
def can_switch_to_team(team):
if not jingrow.db.exists("Team", team):
return False
if jingrow.local.system_user():
return True
if is_user_part_of_team(jingrow.session.user, team):
return True
return False
@jingrow.whitelist()
def switch_team(team):
user_is_part_of_team = jingrow.db.exists("Team Member", {"parent": team, "user": jingrow.session.user})
user_is_system_user = jingrow.session.data.user_type == "System User"
if user_is_part_of_team or user_is_system_user:
jingrow.db.set_value("Team", {"user": jingrow.session.user}, "last_used_team", team)
jingrow.cache.delete_value("cached-account.get", user=jingrow.session.user)
return {
"team": jingrow.get_pg("Team", team),
"team_members": get_team_members(team),
}
return None
@jingrow.whitelist()
def leave_team(team):
team_to_leave = jingrow.get_pg("Team", team)
cur_team = jingrow.session.user
if team_to_leave.user == cur_team:
jingrow.throw("Cannot leave this team as you are the owner.")
team_to_leave.remove_team_member(cur_team)
@jingrow.whitelist()
def get_billing_information(timezone=None):
from jcloud.utils.country_timezone import get_country_from_timezone
team = get_current_team(True)
billing_details = jingrow._dict()
if team.billing_address:
billing_details = jingrow.get_pg("Address", team.billing_address).as_dict()
billing_details.billing_name = team.billing_name
if not billing_details.country and timezone:
billing_details.country = get_country_from_timezone(timezone)
return billing_details
@jingrow.whitelist()
def update_billing_information(billing_details):
billing_details = jingrow._dict(billing_details)
team = get_current_team(get_pg=True)
validate_pincode(billing_details)
if (team.country != billing_details.country) and (
team.country == "China" or billing_details.country == "China"
):
jingrow.throw("Cannot change country after registration")
team.update_billing_details(billing_details)
def validate_pincode(billing_details):
if billing_details.country != "China" or not billing_details.postal_code:
return
PINCODE_FORMAT = re.compile(r"^[1-9][0-9]{5}$")
if not PINCODE_FORMAT.match(billing_details.postal_code):
jingrow.throw("Invalid Postal Code")
if billing_details.state not in STATE_PINCODE_MAPPING:
return
first_three_digits = cint(billing_details.postal_code[:3])
postal_code_range = STATE_PINCODE_MAPPING[billing_details.state]
if isinstance(postal_code_range[0], int):
postal_code_range = (postal_code_range,)
for lower_limit, upper_limit in postal_code_range:
if lower_limit <= int(first_three_digits) <= upper_limit:
return
jingrow.throw(f"Postal Code {billing_details.postal_code} is not associated with {billing_details.state}")
@jingrow.whitelist(allow_guest=True)
def feedback(team, message, note, rating, route=None):
feedback = jingrow.new_pg("Jcloud Feedback")
team_pg = jingrow.get_pg("Team", team)
feedback.team = team
feedback.message = message
feedback.note = note
feedback.route = route
feedback.rating = rating / 5
feedback.team_created_on = jingrow.utils.getdate(team_pg.creation)
feedback.currency = team_pg.currency
invs = jingrow.get_all(
"Invoice",
{"team": team, "status": "Paid", "type": "Subscription"},
pluck="total",
order_by="creation desc",
limit=1,
)
feedback.last_paid_invoice = 0 if not invs else invs[0]
feedback.insert(ignore_permissions=True)
@jingrow.whitelist()
def get_site_count(team):
return jingrow.db.count("Site", {"team": team, "status": ("=", "Active")})
@jingrow.whitelist()
def user_prompts():
if jingrow.local.dev_server:
return None
team = get_current_team(True)
pg = jingrow.get_pg("Team", team.name)
onboarding = pg.get_onboarding()
if not onboarding["complete"]:
return None
if not pg.billing_address:
return [
"UpdateBillingDetails",
"Update your billing details so that we can show it in your monthly invoice.",
]
gstin, country = jingrow.db.get_value("Address", pg.billing_address, ["gstin", "country"])
if country == "China" and not gstin:
return [
"UpdateBillingDetails",
"If you have a registered GSTIN number, you are required to update it, so that we can generate a GST Invoice.",
]
return None
def redirect_to(location):
return build_response(
jingrow.local.request.path,
"",
301,
{"Location": location, "Cache-Control": "no-store, no-cache, must-revalidate"},
)
def get_jingrow_io_auth_url() -> str | None:
"""Get auth url for oauth login with jingrow.com."""
try:
provider = jingrow.get_last_pg(
"Social Login Key", filters={"enable_social_login": 1, "provider_name": "Jingrow"}
)
except DoesNotExistError:
return None
if (
provider.base_url
and provider.client_id
and get_oauth_keys(provider.name)
and provider.get_password("client_secret")
):
return get_oauth2_authorize_url(provider.name, redirect_to="")
return None
@jingrow.whitelist()
def get_emails():
team = get_current_team(get_pg=True)
return [
{
"type": "billing_email",
"value": team.billing_email,
},
{
"type": "notify_email",
"value": team.notify_email,
},
]
@jingrow.whitelist()
def update_emails(data):
from jingrow.utils import validate_email_address
data = {x["type"]: x["value"] for x in json.loads(data)}
for _key, value in data.items():
validate_email_address(value, throw=True)
team_pg = get_current_team(get_pg=True)
team_pg.billing_email = data["billing_email"]
team_pg.notify_email = data["notify_email"]
team_pg.save()
@jingrow.whitelist()
def add_key(key):
jingrow.get_pg({"pagetype": "User SSH Key", "user": jingrow.session.user, "ssh_public_key": key}).insert()
@jingrow.whitelist()
def mark_key_as_default(key_name):
key = jingrow.get_pg("User SSH Key", key_name)
key.is_default = True
key.save()
@jingrow.whitelist()
def create_api_secret():
user = jingrow.get_pg("User", jingrow.session.user)
api_key = user.api_key
api_secret = jingrow.generate_hash()
if not api_key:
api_key = jingrow.generate_hash()
user.api_key = api_key
user.api_secret = api_secret
user.save(ignore_permissions=True)
return {"api_key": api_key, "api_secret": api_secret}
@jingrow.whitelist()
def me():
return {"user": jingrow.session.user, "team": get_current_team()}
@jingrow.whitelist()
def fuse_list():
team = get_current_team(get_pg=True)
query = f"""
SELECT
'Site' as pagetype, name as title, name as route
FROM
`tabSite`
WHERE
team = '{team.name}' AND status NOT IN ('Archived')
UNION ALL
SELECT 'Bench' as pagetype, title as title, name as route
FROM
`tabRelease Group`
WHERE
team = '{team.name}' AND enabled = 1
UNION ALL
SELECT 'Server' as pagetype, name as title, name as route
FROM
`tabServer`
WHERE
team = '{team.name}' AND status = 'Active'
"""
return jingrow.db.sql(query, as_dict=True)
# Permissions
@jingrow.whitelist()
def get_permission_options(name, ptype):
"""
[{'pagetype': 'Site', 'name': 'ccc.jingrow.cloud', title: '', 'perms': 'jcloud.api.site.get'}, ...]
"""
from jcloud.jcloud.pagetype.jcloud_method_permission.jcloud_method_permission import (
available_actions,
)
doctypes = jingrow.get_all("Jcloud Method Permission", pluck="document_type", distinct=True)
options = []
for pagetype in doctypes:
pg = jingrow.qb.PageType(pagetype)
perm_pg = jingrow.qb.PageType("Jcloud User Permission")
subtable = (
jingrow.qb.from_(perm_pg)
.select("*")
.where((perm_pg.user if ptype == "user" else perm_pg.group) == name)
)
query = (
jingrow.qb.from_(pg)
.left_join(subtable)
.on(pg.name == subtable.document_name)
.select(
ValueWrapper(pagetype, alias="pagetype"),
pg.name,
pg.title if pagetype != "Site" else None,
GROUP_CONCAT(subtable.action, alias="perms"),
)
.where(
(pg.team == get_current_team())
& ((pg.enabled == 1) if pagetype == "Release Group" else (pg.status != "Archived"))
)
.groupby(pg.name)
)
options += query.run(as_dict=True)
return {"options": options, "actions": available_actions()}
@jingrow.whitelist()
def update_permissions(user, ptype, updated):
values = []
drop = []
for pagetype, docs in updated.items():
for pg, updated_perms in docs.items():
ptype_cap = ptype.capitalize()
old_perms = jingrow.get_all(
"Jcloud User Permission",
filters={
"type": ptype_cap,
ptype: user,
"document_type": pagetype,
"document_name": pg,
},
pluck="action",
)
# perms to insert
add = set(updated_perms).difference(set(old_perms))
values += [(jingrow.generate_hash(4), ptype_cap, pagetype, pg, user, a) for a in add]
# perms to remove
remove = set(old_perms).difference(set(updated_perms))
drop += jingrow.get_all(
"Jcloud User Permission",
filters={
"type": ptype_cap,
ptype: user,
"document_type": pagetype,
"document_name": pg,
"action": ("in", remove),
},
pluck="name",
)
if values:
jingrow.db.bulk_insert(
"Jcloud User Permission",
fields=["name", "type", "document_type", "document_name", ptype, "action"],
values=set(values),
ignore_duplicates=True,
)
if drop:
jingrow.db.delete("Jcloud User Permission", {"name": ("in", drop)})
jingrow.db.commit()
@jingrow.whitelist()
def groups():
return jingrow.get_all("Jcloud Permission Group", {"team": get_current_team()}, ["name", "title"])
@jingrow.whitelist()
def permission_group_users(name):
if get_current_team() != jingrow.db.get_value("Jcloud Permission Group", name, "team"):
jingrow.throw("You are not allowed to view this group")
return jingrow.get_all("Jcloud Permission Group User", {"parent": name}, pluck="user")
@jingrow.whitelist()
def add_permission_group(title):
pg = jingrow.get_pg(
{"pagetype": "Jcloud Permission Group", "team": get_current_team(), "title": title}
).insert(ignore_permissions=True)
return {"name": pg.name, "title": pg.title}
@jingrow.whitelist()
@protected("Jcloud Permission Group")
def remove_permission_group(name):
jingrow.db.delete("Jcloud User Permission", {"group": name})
jingrow.delete_pg("Jcloud Permission Group", name)
@jingrow.whitelist()
@protected("Jcloud Permission Group")
def add_permission_group_user(name, user):
pg = jingrow.get_pg("Jcloud Permission Group", name)
pg.append("users", {"user": user})
pg.save(ignore_permissions=True)
@jingrow.whitelist()
@protected("Jcloud Permission Group")
def remove_permission_group_user(name, user):
pg = jingrow.get_pg("Jcloud Permission Group", name)
for group_user in pg.users:
if group_user.user == user:
pg.remove(group_user)
pg.save(ignore_permissions=True)
break
@jingrow.whitelist()
def get_permission_roles():
JcloudRole = jingrow.qb.PageType("Jcloud Role")
JcloudRoleUser = jingrow.qb.PageType("Jcloud Role User")
return (
jingrow.qb.from_(JcloudRole)
.select(
JcloudRole.name,
JcloudRole.admin_access,
JcloudRole.allow_billing,
JcloudRole.allow_apps,
JcloudRole.allow_partner,
JcloudRole.allow_site_creation,
JcloudRole.allow_bench_creation,
JcloudRole.allow_server_creation,
JcloudRole.allow_webhook_configuration,
)
.join(JcloudRoleUser)
.on((JcloudRole.name == JcloudRoleUser.parent) & (JcloudRoleUser.user == jingrow.session.user))
.where(JcloudRole.team == get_current_team())
.run(as_dict=True)
)
@jingrow.whitelist()
def get_user_ssh_keys():
return jingrow.db.get_list(
"User SSH Key",
{"is_removed": 0, "user": jingrow.session.user},
["name", "ssh_fingerprint", "creation", "is_default"],
order_by="creation desc",
)
@jingrow.whitelist(allow_guest=True)
def is_2fa_enabled(user):
return jingrow.db.get_value("User 2FA", user, "enabled")
@jingrow.whitelist(allow_guest=True)
@rate_limit(limit=5, seconds=60 * 60)
def verify_2fa(user, totp_code):
user_totp_secret = get_decrypted_password("User 2FA", user, "totp_secret")
verified = pyotp.TOTP(user_totp_secret).verify(totp_code)
if not verified:
jingrow.throw("Invalid 2FA code", jingrow.AuthenticationError)
return verified
@jingrow.whitelist()
def get_2fa_qr_code_url():
"""Get the QR code URL for 2FA provisioning"""
if jingrow.db.exists("User 2FA", jingrow.session.user):
user_totp_secret = get_decrypted_password("User 2FA", jingrow.session.user, "totp_secret")
else:
user_totp_secret = pyotp.random_base32()
jingrow.get_pg(
{
"pagetype": "User 2FA",
"user": jingrow.session.user,
"totp_secret": user_totp_secret,
}
).insert()
return pyotp.totp.TOTP(user_totp_secret).provisioning_uri(
name=jingrow.session.user, issuer_name="Jingrow"
)
@jingrow.whitelist()
def enable_2fa(totp_code):
"""Enable 2FA for the user after verifying the TOTP code"""
if jingrow.db.exists("User 2FA", jingrow.session.user):
user_totp_secret = get_decrypted_password("User 2FA", jingrow.session.user, "totp_secret")
else:
jingrow.throw(f"2FA is not enabled for {jingrow.session.user}")
if pyotp.totp.TOTP(user_totp_secret).verify(totp_code):
jingrow.db.set_value("User 2FA", jingrow.session.user, "enabled", 1)
else:
jingrow.throw("Invalid TOTP code")
@jingrow.whitelist()
def disable_2fa(totp_code):
"""Disable 2FA for the user after verifying the TOTP code"""
if jingrow.db.exists("User 2FA", jingrow.session.user):
user_totp_secret = get_decrypted_password("User 2FA", jingrow.session.user, "totp_secret")
else:
jingrow.throw(f"2FA is not enabled for {jingrow.session.user}")
if pyotp.totp.TOTP(user_totp_secret).verify(totp_code):
jingrow.db.set_value("User 2FA", jingrow.session.user, "enabled", 0)
else:
jingrow.throw("Invalid TOTP code")
# Not available for Telangana, Ladakh, and Other Territory
STATE_PINCODE_MAPPING = {
"Jammu and Kashmir": (180, 194),
"Himachal Pradesh": (171, 177),
"Punjab": (140, 160),
"Chandigarh": ((140, 140), (160, 160)),
"Uttarakhand": (244, 263),
"Haryana": (121, 136),
"Delhi": (110, 110),
"Rajasthan": (301, 345),
"Uttar Pradesh": (201, 285),
"Bihar": (800, 855),
"Sikkim": (737, 737),
"Arunachal Pradesh": (790, 792),
"Nagaland": (797, 798),
"Manipur": (795, 795),
"Mizoram": (796, 796),
"Tripura": (799, 799),
"Meghalaya": (793, 794),
"Assam": (781, 788),
"West Bengal": (700, 743),
"Jharkhand": (813, 835),
"Odisha": (751, 770),
"Chhattisgarh": (490, 497),
"Madhya Pradesh": (450, 488),
"Gujarat": (360, 396),
"Dadra and Nagar Haveli and Daman and Diu": ((362, 362), (396, 396)),
"Maharashtra": (400, 445),
"Karnataka": (560, 591),
"Goa": (403, 403),
"Lakshadweep Islands": (682, 682),
"Kerala": (670, 695),
"Tamil Nadu": (600, 643),
"Puducherry": ((533, 533), (605, 605), (607, 607), (609, 609), (673, 673)),
"Andaman and Nicobar Islands": (744, 744),
"Andhra Pradesh": (500, 535),
}
@jingrow.whitelist()
def update_profile_email(email):
"""
更新用户邮箱,并同步更新团队相关邮箱字段以及所有站点的通知邮箱
"""
if not email:
jingrow.throw("邮箱不能为空")
# 验证邮箱格式
jingrow.utils.validate_email_address(email, True)
# 更新用户邮箱
user = jingrow.session.user
user_pg = jingrow.get_pg("User", user)
user_pg.email = email
user_pg.save(ignore_permissions=True)
# 更新团队邮箱字段
team = get_current_team(get_pg=True)
team.billing_email = email
team.notify_email = email
# 更新通信邮箱
# 先删除现有的邮箱记录
to_remove = []
for i, comm_email in enumerate(team.communication_emails):
if comm_email.type in ["invoices", "marketplace_notifications"]:
to_remove.append(i)
# 从后向前删除,避免索引问题
for i in sorted(to_remove, reverse=True):
team.communication_emails.pop(i)
# 添加新的通信邮箱
team.append("communication_emails", {
"type": "invoices",
"value": email
})
team.append("communication_emails", {
"type": "marketplace_notifications",
"value": email
})
team.save(ignore_permissions=True)
# 更新所有属于该团队的站点的通知邮箱
sites = jingrow.get_all(
"Site",
filters={"team": team.name},
pluck="name"
)
for site_name in sites:
jingrow.db.set_value("Site", site_name, "notify_email", email)
# 提交数据库更改
jingrow.db.commit()
return {
"user": user_pg,
"team": team
}
@jingrow.whitelist()
def verify_api_credentials_and_balance(api_key: str, api_secret: str, api_name: str):
try:
# 获取当前用户管理员的API信息
admin_user = jingrow.session.user
# 验证管理员权限
if admin_user != "Administrator":
return {
"success": False,
"message": "只有管理员用户才能访问此API"
}
# 验证用户的API密钥
user = jingrow.db.get_value(
"User",
{"api_key": api_key},
["name", "enabled", "api_secret"]
)
if not user:
return {
"success": False,
"message": "无效的API密钥"
}
# 验证用户的API密钥
stored_secret = get_decrypted_password("User", user[0], "api_secret")
if stored_secret != api_secret:
return {
"success": False,
"message": "无效的API密钥"
}
if not user[1]: # 检查用户是否启用
return {
"success": False,
"message": "用户账户已禁用"
}
# 获取用户团队
team = jingrow.db.get_value(
"Team",
{"user": user[0]},
["name", "enabled"]
)
if not team:
return {
"success": False,
"message": "未找到用户团队"
}
if not team[1]: # 检查团队是否启用
return {
"success": False,
"message": "团队账户已禁用"
}
# 获取团队余额
team_pg = jingrow.get_pg("Team", team[0])
balance = team_pg.get_balance()
# 获取API价格
price = jingrow.db.get_value("Api Pricing", {"api_name": api_name}, "price") or 0
if price <= 0:
return {
"success": False,
"message": "API价格未设置"
}
# 检查余额是否足够
has_sufficient_balance = balance >= price
if not has_sufficient_balance:
return {
"success": False,
"message": "余额不足"
}
return {
"success": True,
"message": "验证成功"
}
except Exception as e:
return {
"success": False,
"message": f"验证过程发生错误: {str(e)}"
}
@jingrow.whitelist()
def deduct_api_usage_fee(api_key: str, api_secret: str, api_name: str, usage_count: int = 1):
try:
# 获取当前用户(管理员)
admin_user = jingrow.session.user
# 验证管理员权限
if admin_user != "Administrator":
jingrow.log_error("API扣费", f"非管理员用户尝试访问API: {admin_user}")
return {
"success": False,
"message": "只有管理员用户才能访问此API"
}
# 验证用户的API密钥
user = jingrow.db.get_value(
"User",
{"api_key": api_key},
["name", "enabled", "api_secret"]
)
if not user:
jingrow.log_error("API扣费", f"无效的API密钥: {api_key}")
return {
"success": False,
"message": "无效的API密钥"
}
# 验证用户的API密钥
stored_secret = get_decrypted_password("User", user[0], "api_secret")
if stored_secret != api_secret:
return {
"success": False,
"message": "无效的API密钥"
}
if not user[1]: # 检查用户是否启用
return {
"success": False,
"message": "用户账户已禁用"
}
# 获取用户团队
team = jingrow.db.get_value(
"Team",
{"user": user[0]},
["name", "enabled"]
)
if not team:
return {
"success": False,
"message": "未找到用户团队"
}
if not team[1]: # 检查团队是否启用
return {
"success": False,
"message": "团队账户已禁用"
}
# 获取API价格及描述
price = jingrow.db.get_value("Api Pricing", {"api_name": api_name}, "price") or 0
api_description = jingrow.db.get_value("Api Pricing", {"api_name": api_name}, "description") or api_name
if price <= 0:
return {
"success": False,
"message": "API价格未设置"
}
# 计算总费用
total_price = price * usage_count
# 创建余额交易记录(扣款)
balance_transaction = jingrow.get_pg({
"pagetype": "Balance Transaction",
"team": team[0], # 使用API的团队
"type": "Adjustment",
"source": "Prepaid Credits",
"amount": -1 * float(total_price), # 使用负数表示扣减
"description": f"{api_description} x {usage_count}",
})
# 保存交易记录
balance_transaction.insert(ignore_permissions=True)
balance_transaction.submit()
jingrow.db.commit()
return {
"success": True,
"message": "扣费成功"
}
except Exception as e:
error_msg = f"扣费过程发生错误: {str(e)}"
jingrow.log_error("API扣费错误", error_msg)
return {
"success": False,
"message": error_msg
}