From b4bb563782d86a4f8c2498d9efe30af857a31deb Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Fri, 11 Jul 2025 16:36:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E4=BA=86=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/Controllers/DataController.cs | 130 +++++++-- server/src/Database.cs | 81 +++++- src/APIClient.ts | 327 +++++++++++++++++++---- src/components/LoginCard.vue | 135 +++++++++- src/components/Navbar.vue | 128 +++++++-- src/utils/AuthManager.ts | 93 +++++++ src/views/LoginView.vue | 10 +- src/views/VideoStreamView.vue | 15 +- 8 files changed, 790 insertions(+), 129 deletions(-) create mode 100644 src/utils/AuthManager.ts diff --git a/server/src/Controllers/DataController.cs b/server/src/Controllers/DataController.cs index 735744d..89fd1b5 100644 --- a/server/src/Controllers/DataController.cs +++ b/server/src/Controllers/DataController.cs @@ -2,6 +2,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; @@ -16,20 +17,47 @@ public class DataController : ControllerBase { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + public class GetUserInfoResponse + { + /// + /// 用户的唯一标识符 + /// + public Guid ID { get; set; } + + /// + /// 用户的名称 + /// + public required string Name { get; set; } + + /// + /// 用户的电子邮箱 + /// + public required string EMail { get; set; } + + /// + /// 用户关联的板卡ID + /// + public Guid BoardID { get; set; } + } + /// - /// [TODO:description] + /// 用户登录,获取 JWT 令牌 /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] - [HttpPost("login")] + /// 用户名 + /// 用户密码 + /// JWT 令牌字符串 + [HttpPost("Login")] + [EnableCors("Users")] + [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public IActionResult Login(string name, string password) { // 验证用户密码 using var db = new Database.AppDataConnection(); var ret = db.CheckUserPassword(name, password); - if (!ret.IsSuccessful) return StatusCode(StatusCodes.Status500InternalServerError); - if (!ret.Value.HasValue) return BadRequest($"TODO"); + if (!ret.IsSuccessful) return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败"); + if (!ret.Value.HasValue) return BadRequest("用户名或密码错误"); var user = ret.Value.Value; // 生成 JWT @@ -55,32 +83,94 @@ public class DataController : ControllerBase } /// - /// [TODO:description] + /// 测试用户认证,需携带有效 JWT /// - /// [TODO:return] - [HttpGet("TestAuth")] + /// 认证成功信息 [Authorize] + [HttpGet("TestAuth")] + [EnableCors("Users")] + [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public IActionResult TestAuth() { - return Ok("Authenticated!"); + return Ok("认证成功!"); + } + + /// + /// 获取当前用户信息 + /// + /// 用户信息,包括ID、用户名、邮箱和板卡ID + [Authorize] + [HttpGet("GetUserInfo")] + [EnableCors("Users")] + [ProducesResponseType(typeof(GetUserInfoResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public IActionResult GetUserInfo() + { + // Get User Name + var userName = User.Identity?.Name; + if (string.IsNullOrEmpty(userName)) + return Unauthorized("未找到用户名信息"); + + // Get User Info + using var db = new Database.AppDataConnection(); + var ret = db.GetUserByName(userName); + if (!ret.IsSuccessful) + return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败"); + + if (!ret.Value.HasValue) + return BadRequest("用户不存在"); + + var user = ret.Value.Value; + return Ok(new GetUserInfoResponse + { + ID = user.ID, + Name = user.Name, + EMail = user.EMail, + BoardID = user.BoardID, + }); } /// /// 注册新用户 /// - /// 用户名 - /// [TODO:parameter] - /// [TODO:parameter] - /// 操作结果 + /// 用户名(不超过255个字符) + /// 邮箱地址 + /// 用户密码 + /// 操作结果,成功返回 true,失败返回错误信息 [HttpPost("SignUpUser")] + [EnableCors("Users")] + [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public IActionResult SignUpUser(string name, string email, string password) { - if (name.Length > 255) - return BadRequest("Name Couln't over 255 characters"); + // 验证输入参数 + if (string.IsNullOrWhiteSpace(name)) + return BadRequest("用户名不能为空"); - using var db = new Database.AppDataConnection(); - var ret = db.AddUser(name, email, password); - return Ok(ret); + if (name.Length > 255) + return BadRequest("用户名不能超过255个字符"); + + if (string.IsNullOrWhiteSpace(email)) + return BadRequest("邮箱不能为空"); + + if (string.IsNullOrWhiteSpace(password)) + return BadRequest("密码不能为空"); + + try + { + using var db = new Database.AppDataConnection(); + var ret = db.AddUser(name, email, password); + return Ok(ret); + } + catch (Exception ex) + { + logger.Error(ex, "注册用户时发生异常"); + return StatusCode(StatusCodes.Status500InternalServerError, "注册失败,请稍后重试"); + } } } diff --git a/server/src/Database.cs b/server/src/Database.cs index 6d2fd97..b1f95da 100644 --- a/server/src/Database.cs +++ b/server/src/Database.cs @@ -122,14 +122,30 @@ public class AppDataConnection : DataConnection { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + static readonly string DATABASE_FILEPATH = $"{Environment.CurrentDirectory}/Database.sqlite"; + static readonly LinqToDB.DataOptions options = - new LinqToDB.DataOptions() - .UseSQLite($"Data Source={Environment.CurrentDirectory}/Database.sqlite"); + new LinqToDB.DataOptions().UseSQLite($"Data Source={DATABASE_FILEPATH}"); /// /// 初始化应用程序数据连接 /// - public AppDataConnection() : base(options) { } + public AppDataConnection() : base(options) + { + if (!Path.Exists(DATABASE_FILEPATH)) + { + LinqToDB.DataProvider.SQLite.SQLiteTools.CreateDatabase(DATABASE_FILEPATH); + this.CreateAllTables(); + var user = new User() + { + Name = "Admin", + EMail = "selfconfusion@gmail.com", + Password = "12345678", + Permission = Database.User.UserPermission.Admin, + }; + this.Insert(user); + } + } /// @@ -173,20 +189,69 @@ public class AppDataConnection : DataConnection /// [TODO:description] /// /// [TODO:parameter] - /// [TODO:parameter] /// [TODO:return] - public Result> CheckUserPassword(string name, string password) + public Result> GetUserByName(string name) { var user = this.User.Where((user) => user.Name == name).ToArray(); if (user.Length > 1) { logger.Error($"TODO"); - return new(new Exception($"")); + return new(new Exception($"TODO")); } - if (user[0].Password == password) return new(user[0]); - else return new(Optional.Null()); + if (user.Length == 0) + { + logger.Info($"TODO"); + return new(Optional.None); + } + + return new(user[0]); + } + + /// + /// [TODO:description] + /// + /// [TODO:parameter] + /// [TODO:return] + public Result> GetUserByEMail(string email) + { + var user = this.User.Where((user) => user.EMail == email).ToArray(); + + if (user.Length > 1) + { + logger.Error($"TODO"); + return new(new Exception($"TODO")); + } + + if (user.Length == 0) + { + logger.Info($"TODO"); + return new(Optional.None); + } + + return new(user[0]); + } + + /// + /// [TODO:description] + /// + /// [TODO:parameter] + /// [TODO:parameter] + /// [TODO:return] + public Result> CheckUserPassword(string name, string password) + { + var ret = this.GetUserByName(name); + if (!ret.IsSuccessful) + return new(ret.Error); + + if (!ret.Value.HasValue) + return new(Optional.None); + + var user = ret.Value.Value; + + if (user.Password == password) return new(user); + else return new(Optional.None); } /// diff --git a/src/APIClient.ts b/src/APIClient.ts index d696954..19ac4a5 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -377,13 +377,13 @@ export class DataClient { } /** - * [TODO:description] - * @param name (optional) [TODO:parameter] - * @param password (optional) [TODO:parameter] - * @return [TODO:return] + * 用户登录,获取 JWT 令牌 + * @param name (optional) 用户名 + * @param password (optional) 用户密码 + * @return JWT 令牌字符串 */ - login(name: string | undefined, password: string | undefined): Promise { - let url_ = this.baseUrl + "/api/Data/login?"; + login(name: string | undefined, password: string | undefined): Promise { + let url_ = this.baseUrl + "/api/Data/Login?"; if (name === null) throw new Error("The parameter 'name' cannot be null."); else if (name !== undefined) @@ -397,7 +397,7 @@ export class DataClient { let options_: RequestInit = { method: "POST", headers: { - "Accept": "application/octet-stream" + "Accept": "application/json" } }; @@ -406,40 +406,48 @@ export class DataClient { }); } - protected processLogin(response: Response): Promise { + protected processLogin(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; - if (status === 200 || status === 206) { - const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined; - let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; - let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; - if (fileName) { - fileName = decodeURIComponent(fileName); - } else { - fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; - fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; - } - return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; }); + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = resultData200 !== undefined ? resultData200 : null; + + return result200; + }); + } else if (status === 400) { + return response.text().then((_responseText) => { + let result400: any = null; + let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result400 = ProblemDetails.fromJS(resultData400); + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + return throwException("A server side error occurred.", status, _responseText, _headers); + }); } else if (status !== 200 && status !== 204) { return response.text().then((_responseText) => { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } /** - * [TODO:description] - * @return [TODO:return] + * 测试用户认证,需携带有效 JWT + * @return 认证成功信息 */ - testAuth(): Promise { + testAuth(): Promise { let url_ = this.baseUrl + "/api/Data/TestAuth"; url_ = url_.replace(/[?&]$/, ""); let options_: RequestInit = { method: "GET", headers: { - "Accept": "application/octet-stream" + "Accept": "application/json" } }; @@ -448,36 +456,96 @@ export class DataClient { }); } - protected processTestAuth(response: Response): Promise { + protected processTestAuth(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; - if (status === 200 || status === 206) { - const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined; - let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; - let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; - if (fileName) { - fileName = decodeURIComponent(fileName); - } else { - fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; - fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; - } - return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; }); + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = resultData200 !== undefined ? resultData200 : null; + + return result200; + }); + } else if (status === 401) { + return response.text().then((_responseText) => { + let result401: any = null; + let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + }); } else if (status !== 200 && status !== 204) { return response.text().then((_responseText) => { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); + } + + /** + * 获取当前用户信息 + * @return 用户信息,包括ID、用户名、邮箱和板卡ID + */ + getUserInfo(): Promise { + let url_ = this.baseUrl + "/api/Data/GetUserInfo"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetUserInfo(_response); + }); + } + + protected processGetUserInfo(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = GetUserInfoResponse.fromJS(resultData200); + return result200; + }); + } else if (status === 400) { + return response.text().then((_responseText) => { + let result400: any = null; + let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result400 = ProblemDetails.fromJS(resultData400); + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + return throwException("A server side error occurred.", status, _responseText, _headers); + }); + } else if (status === 401) { + return response.text().then((_responseText) => { + let result401: any = null; + let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); } /** * 注册新用户 - * @param name (optional) 用户名 - * @param email (optional) [TODO:parameter] - * @param password (optional) [TODO:parameter] - * @return 操作结果 + * @param name (optional) 用户名(不超过255个字符) + * @param email (optional) 邮箱地址 + * @param password (optional) 用户密码 + * @return 操作结果,成功返回 true,失败返回错误信息 */ - signUpUser(name: string | undefined, email: string | undefined, password: string | undefined): Promise { + signUpUser(name: string | undefined, email: string | undefined, password: string | undefined): Promise { let url_ = this.baseUrl + "/api/Data/SignUpUser?"; if (name === null) throw new Error("The parameter 'name' cannot be null."); @@ -496,7 +564,7 @@ export class DataClient { let options_: RequestInit = { method: "POST", headers: { - "Accept": "application/octet-stream" + "Accept": "application/json" } }; @@ -505,26 +573,34 @@ export class DataClient { }); } - protected processSignUpUser(response: Response): Promise { + protected processSignUpUser(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; - if (status === 200 || status === 206) { - const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined; - let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; - let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; - if (fileName) { - fileName = decodeURIComponent(fileName); - } else { - fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; - fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; - } - return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; }); + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = resultData200 !== undefined ? resultData200 : null; + + return result200; + }); + } else if (status === 400) { + return response.text().then((_responseText) => { + let result400: any = null; + let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result400 = ProblemDetails.fromJS(resultData400); + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + return throwException("A server side error occurred.", status, _responseText, _headers); + }); } else if (status !== 200 && status !== 204) { return response.text().then((_responseText) => { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } } @@ -2286,6 +2362,145 @@ export interface ICameraConfigRequest { port: number; } +export class ProblemDetails implements IProblemDetails { + type?: string | undefined; + title?: string | undefined; + status?: number | undefined; + detail?: string | undefined; + instance?: string | undefined; + extensions!: { [key: string]: any; }; + + [key: string]: any; + + constructor(data?: IProblemDetails) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + if (!data) { + this.extensions = {}; + } + } + + init(_data?: any) { + if (_data) { + for (var property in _data) { + if (_data.hasOwnProperty(property)) + this[property] = _data[property]; + } + this.type = _data["type"]; + this.title = _data["title"]; + this.status = _data["status"]; + this.detail = _data["detail"]; + this.instance = _data["instance"]; + if (_data["extensions"]) { + this.extensions = {} as any; + for (let key in _data["extensions"]) { + if (_data["extensions"].hasOwnProperty(key)) + (this.extensions)![key] = _data["extensions"][key]; + } + } + } + } + + static fromJS(data: any): ProblemDetails { + data = typeof data === 'object' ? data : {}; + let result = new ProblemDetails(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + for (var property in this) { + if (this.hasOwnProperty(property)) + data[property] = this[property]; + } + data["type"] = this.type; + data["title"] = this.title; + data["status"] = this.status; + data["detail"] = this.detail; + data["instance"] = this.instance; + if (this.extensions) { + data["extensions"] = {}; + for (let key in this.extensions) { + if (this.extensions.hasOwnProperty(key)) + (data["extensions"])[key] = (this.extensions)[key]; + } + } + return data; + } +} + +export interface IProblemDetails { + type?: string | undefined; + title?: string | undefined; + status?: number | undefined; + detail?: string | undefined; + instance?: string | undefined; + extensions: { [key: string]: any; }; + + [key: string]: any; +} + +export class GetUserInfoResponse implements IGetUserInfoResponse { + /** 用户的唯一标识符 */ + id!: string; + /** 用户的名称 */ + name!: string; + /** 用户的电子邮箱 */ + eMail!: string; + /** 用户关联的板卡ID */ + boardID!: string; + + constructor(data?: IGetUserInfoResponse) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.id = _data["id"]; + this.name = _data["name"]; + this.eMail = _data["eMail"]; + this.boardID = _data["boardID"]; + } + } + + static fromJS(data: any): GetUserInfoResponse { + data = typeof data === 'object' ? data : {}; + let result = new GetUserInfoResponse(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["name"] = this.name; + data["eMail"] = this.eMail; + data["boardID"] = this.boardID; + return data; + } +} + +export interface IGetUserInfoResponse { + /** 用户的唯一标识符 */ + id: string; + /** 用户的名称 */ + name: string; + /** 用户的电子邮箱 */ + eMail: string; + /** 用户关联的板卡ID */ + boardID: string; +} + export class SystemException extends Exception implements ISystemException { constructor(data?: ISystemException) { diff --git a/src/components/LoginCard.vue b/src/components/LoginCard.vue index 3746e5b..17097da 100644 --- a/src/components/LoginCard.vue +++ b/src/components/LoginCard.vue @@ -1,29 +1,144 @@ - +