add registration page with nickname/phone fields and fix lateinit crashes
This commit is contained in:
@@ -90,37 +90,47 @@ class UserApi internal constructor(
|
|||||||
fun register(
|
fun register(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
nickname: String? = null,
|
||||||
|
phone: String? = null,
|
||||||
onComplete: (Result<AuthSession>) -> Unit,
|
onComplete: (Result<AuthSession>) -> Unit,
|
||||||
): CancellableRequest = submit(onComplete) { requestBlocking("auth/register", username, password, auth = false) }
|
): CancellableRequest = submit(onComplete) {
|
||||||
|
requestRegisterBlocking(
|
||||||
|
"api/v1/auth/register",
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
nickname = nickname,
|
||||||
|
phone = phone,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun login(
|
fun login(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
onComplete: (Result<AuthSession>) -> Unit,
|
onComplete: (Result<AuthSession>) -> Unit,
|
||||||
): CancellableRequest = submit(onComplete) { requestBlocking("auth/login", username, password, auth = false) }
|
): CancellableRequest = submit(onComplete) { requestBlocking("api/v1/auth/login", username, password, auth = false) }
|
||||||
|
|
||||||
fun courses(
|
fun courses(
|
||||||
token: String,
|
token: String,
|
||||||
onComplete: (Result<List<Course>>) -> Unit,
|
onComplete: (Result<List<Course>>) -> Unit,
|
||||||
): CancellableRequest = submitObject(onComplete, token) { parseCourses(getBlocking(it, "courses")) }
|
): CancellableRequest = submitObject(onComplete, token) { parseCourses(getBlocking(it, "api/v1/courses")) }
|
||||||
|
|
||||||
fun enroll(
|
fun enroll(
|
||||||
token: String,
|
token: String,
|
||||||
videoHash: String,
|
videoHash: String,
|
||||||
onComplete: (Result<Unit>) -> Unit,
|
onComplete: (Result<Unit>) -> Unit,
|
||||||
): CancellableRequest = submitUnit(onComplete, token) {
|
): CancellableRequest = submitUnit(onComplete, token) {
|
||||||
request("courses/$videoHash/enroll", token, method = "POST")
|
request("api/v1/courses/$videoHash/enroll", token, method = "POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun results(
|
fun results(
|
||||||
token: String,
|
token: String,
|
||||||
onComplete: (Result<List<UserResult>>) -> Unit,
|
onComplete: (Result<List<UserResult>>) -> Unit,
|
||||||
): CancellableRequest = submitObject(onComplete, token) { parseResults(getBlocking(it, "me/results")) }
|
): CancellableRequest = submitObject(onComplete, token) { parseResults(getBlocking(it, "api/v1/me/results")) }
|
||||||
|
|
||||||
fun dubShares(
|
fun dubShares(
|
||||||
token: String,
|
token: String,
|
||||||
onComplete: (Result<List<UserDubShare>>) -> Unit,
|
onComplete: (Result<List<UserDubShare>>) -> Unit,
|
||||||
): CancellableRequest = submitObject(onComplete, token) { parseDubShares(getBlocking(it, "me/dub-shares")) }
|
): CancellableRequest = submitObject(onComplete, token) { parseDubShares(getBlocking(it, "api/v1/me/dub-shares")) }
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
executor.shutdownNow()
|
executor.shutdownNow()
|
||||||
@@ -163,6 +173,22 @@ class UserApi internal constructor(
|
|||||||
return parseSession(JSONObject(response))
|
return parseSession(JSONObject(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun requestRegisterBlocking(
|
||||||
|
path: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
nickname: String?,
|
||||||
|
phone: String?,
|
||||||
|
): AuthSession {
|
||||||
|
val root = JSONObject()
|
||||||
|
.put("username", username)
|
||||||
|
.put("password", password)
|
||||||
|
nickname?.takeIf { it.isNotBlank() }?.let { root.put("nickname", it) }
|
||||||
|
phone?.takeIf { it.isNotBlank() }?.let { root.put("phone", it) }
|
||||||
|
val response = request(path, body = root.toString(), method = "POST")
|
||||||
|
return parseSession(JSONObject(response))
|
||||||
|
}
|
||||||
|
|
||||||
private fun getBlocking(token: String, path: String): JSONArray {
|
private fun getBlocking(token: String, path: String): JSONArray {
|
||||||
val body = request(path, token = token, method = "GET")
|
val body = request(path, token = token, method = "GET")
|
||||||
return JSONArray(body)
|
return JSONArray(body)
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ class MainActivity : Activity() {
|
|||||||
private var authUsernameInput: EditText? = null
|
private var authUsernameInput: EditText? = null
|
||||||
private var authPasswordInput: EditText? = null
|
private var authPasswordInput: EditText? = null
|
||||||
private var authStatusText: TextView? = null
|
private var authStatusText: TextView? = null
|
||||||
|
private var regUsernameInput: EditText? = null
|
||||||
|
private var regPasswordInput: EditText? = null
|
||||||
|
private var regNicknameInput: EditText? = null
|
||||||
|
private var regPhoneInput: EditText? = null
|
||||||
|
private var regStatusText: TextView? = null
|
||||||
|
private var showingRegisterPage = false
|
||||||
private var myContentList: LinearLayout? = null
|
private var myContentList: LinearLayout? = null
|
||||||
private var sentenceBoundaryRequestGeneration = 0
|
private var sentenceBoundaryRequestGeneration = 0
|
||||||
private var catalogVideos: List<TrainingVideoSummary> = emptyList()
|
private var catalogVideos: List<TrainingVideoSummary> = emptyList()
|
||||||
@@ -320,7 +326,13 @@ class MainActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setRootView() {
|
private fun setRootView() {
|
||||||
setContentView(if (isLoggedIn()) createContentView() else createAuthView())
|
setContentView(
|
||||||
|
when {
|
||||||
|
isLoggedIn() -> createContentView()
|
||||||
|
showingRegisterPage -> createRegisterView()
|
||||||
|
else -> createAuthView()
|
||||||
|
}
|
||||||
|
)
|
||||||
if (isLoggedIn()) {
|
if (isLoggedIn()) {
|
||||||
renderCatalog()
|
renderCatalog()
|
||||||
refreshCurrentUi()
|
refreshCurrentUi()
|
||||||
@@ -375,13 +387,137 @@ class MainActivity : Activity() {
|
|||||||
})
|
})
|
||||||
addView(Button(this@MainActivity).apply {
|
addView(Button(this@MainActivity).apply {
|
||||||
text = "注册"
|
text = "注册"
|
||||||
setOnClickListener { authenticate(register = true) }
|
setOnClickListener {
|
||||||
|
showingRegisterPage = true
|
||||||
|
setRootView()
|
||||||
|
}
|
||||||
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 44.dp).withMargins(0, 0, 0, 0)
|
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 44.dp).withMargins(0, 0, 0, 0)
|
||||||
})
|
})
|
||||||
addView(authStatusText)
|
addView(authStatusText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createRegisterView(): View {
|
||||||
|
return ScrollView(this).apply {
|
||||||
|
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||||
|
setBackgroundColor(COLOR_BACKGROUND)
|
||||||
|
addView(LinearLayout(this@MainActivity).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
gravity = Gravity.CENTER_HORIZONTAL
|
||||||
|
setPadding(28.dp, 60.dp, 28.dp, 28.dp)
|
||||||
|
|
||||||
|
addView(TextView(this@MainActivity).apply {
|
||||||
|
text = "注册账号"
|
||||||
|
textSize = 28f
|
||||||
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
|
setTextColor(Color.WHITE)
|
||||||
|
})
|
||||||
|
addView(TextView(this@MainActivity).apply {
|
||||||
|
text = "填写以下信息完成注册"
|
||||||
|
textSize = 14f
|
||||||
|
setTextColor(COLOR_TEXT_MUTED)
|
||||||
|
setPadding(0, 6.dp, 0, 24.dp)
|
||||||
|
})
|
||||||
|
|
||||||
|
addView(fieldLabel("用户名"))
|
||||||
|
regUsernameInput = EditText(this@MainActivity).apply {
|
||||||
|
hint = "3-50 位字母、数字或下划线"
|
||||||
|
setSingleLine(true)
|
||||||
|
}
|
||||||
|
addView(regUsernameInput)
|
||||||
|
|
||||||
|
addView(fieldLabel("密码"))
|
||||||
|
regPasswordInput = EditText(this@MainActivity).apply {
|
||||||
|
hint = "至少 8 位"
|
||||||
|
inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
setSingleLine(true)
|
||||||
|
}
|
||||||
|
addView(regPasswordInput)
|
||||||
|
|
||||||
|
addView(fieldLabel("姓名"))
|
||||||
|
regNicknameInput = EditText(this@MainActivity).apply {
|
||||||
|
hint = "你的真实姓名或昵称"
|
||||||
|
setSingleLine(true)
|
||||||
|
}
|
||||||
|
addView(regNicknameInput)
|
||||||
|
|
||||||
|
addView(fieldLabel("手机号"))
|
||||||
|
regPhoneInput = EditText(this@MainActivity).apply {
|
||||||
|
hint = "选填"
|
||||||
|
inputType = android.text.InputType.TYPE_CLASS_PHONE
|
||||||
|
setSingleLine(true)
|
||||||
|
}
|
||||||
|
addView(regPhoneInput)
|
||||||
|
|
||||||
|
regStatusText = TextView(this@MainActivity).apply {
|
||||||
|
setTextColor(COLOR_ACCENT_LIGHT)
|
||||||
|
textSize = 13f
|
||||||
|
setPadding(0, 14.dp, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
addView(Button(this@MainActivity).apply {
|
||||||
|
text = "注册"
|
||||||
|
background = rounded(COLOR_ACCENT, 8f)
|
||||||
|
setTextColor(Color.WHITE)
|
||||||
|
setOnClickListener { submitRegistration() }
|
||||||
|
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 48.dp).withMargins(0, 20.dp, 0, 8.dp)
|
||||||
|
})
|
||||||
|
addView(Button(this@MainActivity).apply {
|
||||||
|
text = "返回登录"
|
||||||
|
setOnClickListener {
|
||||||
|
showingRegisterPage = false
|
||||||
|
regStatusText = null
|
||||||
|
setRootView()
|
||||||
|
}
|
||||||
|
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 44.dp).withMargins(0, 0, 0, 0)
|
||||||
|
})
|
||||||
|
addView(regStatusText)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fieldLabel(label: String): TextView {
|
||||||
|
return TextView(this).apply {
|
||||||
|
text = label
|
||||||
|
textSize = 13f
|
||||||
|
setTextColor(COLOR_TEXT_MUTED)
|
||||||
|
setPadding(0, 12.dp, 0, 4.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submitRegistration() {
|
||||||
|
val username = regUsernameInput?.text?.toString()?.trim().orEmpty()
|
||||||
|
val password = regPasswordInput?.text?.toString().orEmpty()
|
||||||
|
val nickname = regNicknameInput?.text?.toString()?.trim().orEmpty()
|
||||||
|
val phone = regPhoneInput?.text?.toString()?.trim().orEmpty()
|
||||||
|
|
||||||
|
if (username.length < 3) {
|
||||||
|
regStatusText?.text = "用户名至少 3 个字符"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (password.length < 8) {
|
||||||
|
regStatusText?.text = "密码至少 8 位"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (nickname.isBlank()) {
|
||||||
|
regStatusText?.text = "请填写姓名"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
regStatusText?.text = "注册中..."
|
||||||
|
sdk.userApi.register(username, password, nickname, phone) { result ->
|
||||||
|
result.fold(
|
||||||
|
onSuccess = { session ->
|
||||||
|
showingRegisterPage = false
|
||||||
|
saveAuthSession(session)
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
regStatusText?.text = "注册失败:${error.message}"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun authenticate(register: Boolean) {
|
private fun authenticate(register: Boolean) {
|
||||||
val username = authUsernameInput?.text?.toString()?.trim().orEmpty()
|
val username = authUsernameInput?.text?.toString()?.trim().orEmpty()
|
||||||
val password = authPasswordInput?.text?.toString().orEmpty()
|
val password = authPasswordInput?.text?.toString().orEmpty()
|
||||||
@@ -392,7 +528,7 @@ class MainActivity : Activity() {
|
|||||||
onFailure = { error -> authStatusText?.text = "失败:${error.message}" },
|
onFailure = { error -> authStatusText?.text = "失败:${error.message}" },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (register) sdk.userApi.register(username, password, callback)
|
if (register) sdk.userApi.register(username, password, onComplete = callback)
|
||||||
else sdk.userApi.login(username, password, callback)
|
else sdk.userApi.login(username, password, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1473,6 +1609,7 @@ class MainActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaChanged(item: TrainingMediaItem?) {
|
override fun onMediaChanged(item: TrainingMediaItem?) {
|
||||||
|
if (!::lessonTitleText.isInitialized) return
|
||||||
lessonTitleText.text = item?.title ?: "未选择课程"
|
lessonTitleText.text = item?.title ?: "未选择课程"
|
||||||
currentSentenceCount = item?.sentences?.size ?: 0
|
currentSentenceCount = item?.sentences?.size ?: 0
|
||||||
}
|
}
|
||||||
@@ -1482,6 +1619,7 @@ class MainActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onGesture(event: GestureEvent) {
|
override fun onGesture(event: GestureEvent) {
|
||||||
|
if (!::statusText.isInitialized) return
|
||||||
statusText.text = when (event.kind) {
|
statusText.text = when (event.kind) {
|
||||||
GestureKind.SINGLE_TAP -> "播放状态已切换"
|
GestureKind.SINGLE_TAP -> "播放状态已切换"
|
||||||
GestureKind.SWIPE_LEFT -> "已跳到上一句"
|
GestureKind.SWIPE_LEFT -> "已跳到上一句"
|
||||||
@@ -1491,12 +1629,18 @@ class MainActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayerError(error: Throwable) {
|
override fun onPlayerError(error: Throwable) {
|
||||||
|
if (!::statusText.isInitialized) return
|
||||||
statusText.text = "播放失败:${error.message.orEmpty()}"
|
statusText.text = "播放失败:${error.message.orEmpty()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyPlaybackSnapshot(snapshot: PlaybackSnapshot) {
|
private fun applyPlaybackSnapshot(snapshot: PlaybackSnapshot) {
|
||||||
|
if (!::timeText.isInitialized || !::progressBar.isInitialized ||
|
||||||
|
!::speedText.isInitialized || !::statusText.isInitialized
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!progressBarDragging) {
|
if (!progressBarDragging) {
|
||||||
timeText.text = "${formatTime(snapshot.positionMs)} / ${formatTime(snapshot.durationMs)}"
|
timeText.text = "${formatTime(snapshot.positionMs)} / ${formatTime(snapshot.durationMs)}"
|
||||||
progressBar.progress = playbackProgress(snapshot)
|
progressBar.progress = playbackProgress(snapshot)
|
||||||
@@ -1514,6 +1658,11 @@ class MainActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun applySentence(sentence: SentenceBoundary?) {
|
private fun applySentence(sentence: SentenceBoundary?) {
|
||||||
|
if (!::sentenceMetaText.isInitialized || !::sentenceText.isInitialized ||
|
||||||
|
!::lessonTitleText.isInitialized
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (sentence == null) {
|
if (sentence == null) {
|
||||||
sentenceMetaText.text = "暂无句子边界"
|
sentenceMetaText.text = "暂无句子边界"
|
||||||
sentenceText.text = lessonTitleText.text
|
sentenceText.text = lessonTitleText.text
|
||||||
@@ -1606,6 +1755,11 @@ class MainActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createCourseSection(): View {
|
private fun createCourseSection(): View {
|
||||||
|
if (!::catalogList.isInitialized) {
|
||||||
|
catalogList = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
}
|
||||||
|
}
|
||||||
return ScrollView(this).apply {
|
return ScrollView(this).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)
|
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)
|
||||||
addView(LinearLayout(this@MainActivity).apply {
|
addView(LinearLayout(this@MainActivity).apply {
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ def create_app(
|
|||||||
user = video_repository.create_user(
|
user = video_repository.create_user(
|
||||||
username=payload.username,
|
username=payload.username,
|
||||||
password=payload.password,
|
password=payload.password,
|
||||||
nickname=payload.username,
|
nickname=payload.nickname or payload.username,
|
||||||
|
phone=payload.phone,
|
||||||
)
|
)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=409, detail="Username is already taken.")
|
raise HTTPException(status_code=409, detail="Username is already taken.")
|
||||||
|
|||||||
@@ -138,12 +138,15 @@ class AuthRequest(BaseModel):
|
|||||||
|
|
||||||
username: str = Field(min_length=3, max_length=50, pattern=r"^[A-Za-z0-9_.-]+$")
|
username: str = Field(min_length=3, max_length=50, pattern=r"^[A-Za-z0-9_.-]+$")
|
||||||
password: str = Field(min_length=8, max_length=128)
|
password: str = Field(min_length=8, max_length=128)
|
||||||
|
nickname: Optional[str] = Field(default=None, max_length=80)
|
||||||
|
phone: Optional[str] = Field(default=None, max_length=20)
|
||||||
|
|
||||||
|
|
||||||
class UserPublic(BaseModel):
|
class UserPublic(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
username: str
|
username: str
|
||||||
nickname: str
|
nickname: str
|
||||||
|
phone: Optional[str] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class VideoRepository:
|
|||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||||
nickname TEXT NOT NULL,
|
nickname TEXT NOT NULL,
|
||||||
|
phone TEXT,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL
|
created_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
@@ -147,6 +148,14 @@ class VideoRepository:
|
|||||||
}
|
}
|
||||||
if "user_id" not in attempt_columns:
|
if "user_id" not in attempt_columns:
|
||||||
connection.execute("ALTER TABLE attempts ADD COLUMN user_id TEXT NOT NULL DEFAULT ''")
|
connection.execute("ALTER TABLE attempts ADD COLUMN user_id TEXT NOT NULL DEFAULT ''")
|
||||||
|
|
||||||
|
with self._connect() as connection:
|
||||||
|
user_columns = {
|
||||||
|
row["name"]
|
||||||
|
for row in connection.execute("PRAGMA table_info(users)")
|
||||||
|
}
|
||||||
|
if "phone" not in user_columns:
|
||||||
|
connection.execute("ALTER TABLE users ADD COLUMN phone TEXT")
|
||||||
connection.execute("CREATE INDEX IF NOT EXISTS idx_attempts_user ON attempts(user_id)")
|
connection.execute("CREATE INDEX IF NOT EXISTS idx_attempts_user ON attempts(user_id)")
|
||||||
|
|
||||||
with self._connect() as connection:
|
with self._connect() as connection:
|
||||||
@@ -750,20 +759,28 @@ class VideoRepository:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_user(self, *, username: str, password: str, nickname: Optional[str]) -> Optional[Dict[str, Any]]:
|
def create_user(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
nickname: Optional[str] = None,
|
||||||
|
phone: Optional[str] = None,
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
user_id = uuid.uuid4().hex
|
user_id = uuid.uuid4().hex
|
||||||
now = utc_now()
|
now = utc_now()
|
||||||
try:
|
try:
|
||||||
with self._connect() as connection:
|
with self._connect() as connection:
|
||||||
row = connection.execute(
|
row = connection.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO users (id, username, nickname, password_hash, created_at)
|
INSERT INTO users (id, username, nickname, phone, password_hash, created_at)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
user_id,
|
user_id,
|
||||||
username.strip().lower(),
|
username.strip().lower(),
|
||||||
(nickname or username).strip()[:80],
|
(nickname or username).strip()[:80],
|
||||||
|
(phone or "").strip()[:20] or None,
|
||||||
hash_password(password),
|
hash_password(password),
|
||||||
now,
|
now,
|
||||||
),
|
),
|
||||||
@@ -783,7 +800,7 @@ class VideoRepository:
|
|||||||
def get_user_by_id(self, user_id: str) -> Optional[Dict[str, Any]]:
|
def get_user_by_id(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||||
with self._connect() as connection:
|
with self._connect() as connection:
|
||||||
row = connection.execute(
|
row = connection.execute(
|
||||||
"SELECT id, username, nickname, created_at FROM users WHERE id = ?",
|
"SELECT id, username, nickname, phone, created_at FROM users WHERE id = ?",
|
||||||
(user_id,),
|
(user_id,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
|
|||||||
Reference in New Issue
Block a user