1736 lines
47 KiB
Python
1736 lines
47 KiB
Python
# 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
|
||
}
|
||
|
||
|