From 7a12be392a569858c1d9e9404486fa419c980119 Mon Sep 17 00:00:00 2001 From: SHM Date: Tue, 15 Jul 2025 10:16:26 +0900 Subject: [PATCH] =?UTF-8?q?[=EC=84=B1=ED=98=84=EB=AA=A8]=20AuthApi=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Config/WebApi.AuthApi.Config.json | 35 ++ Projects/Config/log4net.config | 85 +++++ Projects/DLL/SystemX.Core.dll | Bin 43008 -> 55296 bytes .../DBPatch/sqlScripts/dacpac/HubX.DB.dacpac | Bin 3022 -> 3028 bytes .../DB/HubX/Context/HubXContext.cs | 2 +- .../Controllers/UniqueKeyController.cs | 27 +- .../HubX.Server/Services/UniqueKeyService.cs | 28 ++ .../SystemX.Core/DBPatch/CreateAccountDB.bat | 18 + .../SystemX.Core/DBPatch/UpdateAccountDB.bat | 25 ++ ...ateSqlServerAccount_관리자권한으로실행.bat | 19 + .../sqlScripts/AdminAccount_Create.sql | 43 +++ .../SystemX.DB.AccountDB_Create.sql | 346 ++++++++++++++++++ .../DBPatch/sqlScripts/_CreateScript.bat | 18 + .../DBPatch/sqlScripts/_UpdateAccountDB.bat | 19 + .../sqlScripts/_UpdateScriptGenerate.bat | 12 + .../dacpac/SystemX.DB.AccountDB.dacpac | Bin 0 -> 3503 bytes Projects/SystemX.Core/SystemX.Core.sln | 10 +- .../SystemX.Core/Config/Model/Auth.cs | 33 ++ .../AccountDB/Context/AccountDbContext.cs | 84 +++++ .../AccountDB/Tables/TRefreshToken.cs | 11 + .../DB/DBContext/AccountDB/Tables/TRole.cs | 13 + .../DB/DBContext/AccountDB/Tables/TUser.cs | 19 + .../SystemX.Core/SystemX.Core/ERROR_CODE.cs | 32 ++ .../SystemX.Core/Log4net/Log4net.cs | 6 +- .../SystemX.Core/Model/Auth/LoginModel.cs | 34 ++ .../SystemX.Core/Model/Auth/LogoutModel.cs | 23 ++ .../SystemX.Core/Model/Auth/RegisterModel.cs | 35 ++ .../SystemX.Core/Model/Auth/UserModel.cs | 14 + .../SystemX.Core/Model/Auth}/UserRole.cs | 2 +- .../SystemX.Core/Model/Auth}/UserState.cs | 4 +- .../SystemX.Core/SystemX.Core.csproj | 14 +- .../SystemX.DB.AccountDB.sqlproj | 80 ++++ .../dbo/Scripts/scriptAfterBuild.sql | 29 ++ .../dbo/Tables/tRefreshToken.sql | 5 + .../SystemX.DB.AccountDB/dbo/Tables/tRole.sql | 6 + .../SystemX.DB.AccountDB/dbo/Tables/tUser.sql | 10 + Projects/Tools/Tools_Scaffold_AccountDB.bat | 5 + Projects/WebApi/AuthApi/AuthApi.csproj | 26 +- .../AuthApi/Controllers/AuthController.cs | 146 ++++++++ .../AuthApi/Controllers/CommonController.cs | 59 +++ .../Controllers/WeatherForecastController.cs | 33 -- Projects/WebApi/AuthApi/Program.cs | 97 ++++- .../WebApi/AuthApi/Services/AuthService.cs | 263 +++++++++++++ Projects/WebApi/AuthApi/WeatherForecast.cs | 13 - .../AccountDB/Context/AccountDbContext.cs | 84 +++++ .../AccountDB/Tables/TRefreshToken.cs | 11 + .../DB/DBContext/AccountDB/Tables/TRole.cs | 13 + .../DB/DBContext/AccountDB/Tables/TUser.cs | 19 + .../WebApi.Library.DBContext.csproj | 21 ++ .../WebApi.Library/Config/WebApiConfig.cs | 20 + .../WebApi.Library/WebApi.Library.csproj | 23 ++ Projects/WebApi/WebApi.sln | 17 + 52 files changed, 1931 insertions(+), 60 deletions(-) create mode 100644 Projects/Config/WebApi.AuthApi.Config.json create mode 100644 Projects/Config/log4net.config create mode 100644 Projects/SystemX.Core/DBPatch/CreateAccountDB.bat create mode 100644 Projects/SystemX.Core/DBPatch/UpdateAccountDB.bat create mode 100644 Projects/SystemX.Core/DBPatch/_CreateSqlServerAccount_관리자권한으로실행.bat create mode 100644 Projects/SystemX.Core/DBPatch/sqlScripts/AdminAccount_Create.sql create mode 100644 Projects/SystemX.Core/DBPatch/sqlScripts/SystemX.DB.AccountDB_Create.sql create mode 100644 Projects/SystemX.Core/DBPatch/sqlScripts/_CreateScript.bat create mode 100644 Projects/SystemX.Core/DBPatch/sqlScripts/_UpdateAccountDB.bat create mode 100644 Projects/SystemX.Core/DBPatch/sqlScripts/_UpdateScriptGenerate.bat create mode 100644 Projects/SystemX.Core/DBPatch/sqlScripts/dacpac/SystemX.DB.AccountDB.dacpac create mode 100644 Projects/SystemX.Core/SystemX.Core/Config/Model/Auth.cs create mode 100644 Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Context/AccountDbContext.cs create mode 100644 Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TRefreshToken.cs create mode 100644 Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TRole.cs create mode 100644 Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TUser.cs create mode 100644 Projects/SystemX.Core/SystemX.Core/ERROR_CODE.cs create mode 100644 Projects/SystemX.Core/SystemX.Core/Model/Auth/LoginModel.cs create mode 100644 Projects/SystemX.Core/SystemX.Core/Model/Auth/LogoutModel.cs create mode 100644 Projects/SystemX.Core/SystemX.Core/Model/Auth/RegisterModel.cs create mode 100644 Projects/SystemX.Core/SystemX.Core/Model/Auth/UserModel.cs rename Projects/{VPKI/VPKI/VPKI.Library/Enums => SystemX.Core/SystemX.Core/Model/Auth}/UserRole.cs (89%) rename Projects/{VPKI/VPKI/VPKI.Library/Enums => SystemX.Core/SystemX.Core/Model/Auth}/UserState.cs (74%) create mode 100644 Projects/SystemX.Core/SystemX.DB.AccountDB/SystemX.DB.AccountDB.sqlproj create mode 100644 Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Scripts/scriptAfterBuild.sql create mode 100644 Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tRefreshToken.sql create mode 100644 Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tRole.sql create mode 100644 Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tUser.sql create mode 100644 Projects/Tools/Tools_Scaffold_AccountDB.bat create mode 100644 Projects/WebApi/AuthApi/Controllers/AuthController.cs create mode 100644 Projects/WebApi/AuthApi/Controllers/CommonController.cs delete mode 100644 Projects/WebApi/AuthApi/Controllers/WeatherForecastController.cs create mode 100644 Projects/WebApi/AuthApi/Services/AuthService.cs delete mode 100644 Projects/WebApi/AuthApi/WeatherForecast.cs create mode 100644 Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Context/AccountDbContext.cs create mode 100644 Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TRefreshToken.cs create mode 100644 Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TRole.cs create mode 100644 Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TUser.cs create mode 100644 Projects/WebApi/WebApi.Library.DBContext/WebApi.Library.DBContext.csproj create mode 100644 Projects/WebApi/WebApi.Library/Config/WebApiConfig.cs create mode 100644 Projects/WebApi/WebApi.Library/WebApi.Library.csproj diff --git a/Projects/Config/WebApi.AuthApi.Config.json b/Projects/Config/WebApi.AuthApi.Config.json new file mode 100644 index 0000000..d1ab4d3 --- /dev/null +++ b/Projects/Config/WebApi.AuthApi.Config.json @@ -0,0 +1,35 @@ +{ + "Server": { + "Address": "https://*", + "Port": 11000, + "IIS": false + }, + "Auth": { + "issuer": "SystemX.WebApi.Auth", + "audience": "AuthApi", + "accessTokenSecret": "t6zdogyrT0U1bYw3gJvMm3JHmj2Iyawr7O2WKE2truX+MK0l/XNGmpU2ofagdUWBN4DxAUv7c8xSYVv/8abL6A==", + "accessTokenExpires": 1440, //minutes + "refreshTokenSecret": "1vVuoGqIqkStFI3QUXHMr0/yO1feLPnhqcfFGjZyk478+4WY7dhrUjCfVeWjmmSZYgb+rtP0X6ec+3iL35Yezw==", + "refreshTokenExpires": 1440 //minuts, 60*24 (1day) + }, + "DataBase": [ + { + "IP": "127.0.0.1", + "Port": 1433, + "DBName": "AccountDB", + "DBID": 1, + "DBContext": "AccountDB", + "UserID": "SystemX", + "Password": "X" + }, + { + "IP": "127.0.0.1", + "Port": 1433, + "DBName": "AccountDB_DEV", + "DBID": 2, + "DBContext": "AccountDB", + "UserID": "SystemX", + "Password": "X" + } + ] +} \ No newline at end of file diff --git a/Projects/Config/log4net.config b/Projects/Config/log4net.config new file mode 100644 index 0000000..b970851 --- /dev/null +++ b/Projects/Config/log4net.config @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/DLL/SystemX.Core.dll b/Projects/DLL/SystemX.Core.dll index 44c0858cd1766d99d64a1b71c8c261eaa55b92a0..5ab830f7e02832f71c645e3be9016ceb10042403 100644 GIT binary patch literal 55296 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 NM@;u)a{m_`_%HhbMqB^@ literal 43008 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 delta 1583 zcmY+EYd8}M7{|8}wviaNnu>L!Lz`iyZN$rt3_cp#S-S`h8a9HYC?tfID5M z);uQDTDw0s~qg+)o|q`@3aRr~Bcn7x(0;-fE7OkoFos5X1ZE?5a&hg$-*sPPKW z(oi$wU_^N%0{p)JrV4&vul-xS4`(!L2xiKU!93W6bD%MPvnzS1w54H?NEN6=BN9E^ zElp*Nifmot6Jofgnq#TK42GPR{*({z*~M?v@p%|a5zma9N-lSq#T*Z0RH~d*H=*@G zzo6wS>0>%@JBlRY%t5Eh3Kd+>=7do2ER6i1`C2+`}TYLQ&YdN>QU&$CALgRGz6- zY!uj(o|NUiyw5^5ha?^!hVng6V25TNQxL(H749Bx6gj6uDCT_D)wtaJL<(868XSsV z--x`BIm*H1_fu)UNKVtfcxxqIVUK}i%7j(-n#AbHz10pIo+FZTF6nn;>2aQPVXyjG z*)q8z{B-HV6fd+$&Wz@75z0_aBP`kW5#y@Ir9RWor4)6*6l2O2_FV2I^Ga zpQ)FczZujUay^Pe7`Ejw|+19u>JNXmbdo{ z!aj;~@aJ6@#n{FBks=meq{?Ehe9BpwiWJcq>;hT_Yc>2oIs9wHme$y*^vg}mR{J~F zp?u%g71dwDQuj_i0VubY8YV?yO66O6UdT`>yFdT{o*ATwru&{Q!D!G}^{eqQzV}(+ z?Vw5x_2A+rj}TsTci|93I%s~Zna@l4%Kn;EB>&8~90zOpGJby6a{usL_0|JSS;39D zHZYkTa%^joaNe%pU4OKD@ck*x&XuUS72l(nxQfZ^r)(5SE;9$b&U=FVzc5LKU}P`X zxUyFFxC&-~&Q%-XuG{fSdXUr`vQX;f0K(WIet+A{xl$l8X(ML-gRi$1RD8GAQ zW7}=1VTQ%v3|H;WJ9ko}?6#2}nU~-U4)Mh;1BxXRxP$U<|6g<>Hp8JQzqPS%>`DYq)>Pf2>oxG0g; zVnW9spgo;qCcqofs3nzOsABejN58}LL&eM*$nNi$jX{vWQRV^!>GKUX|5)R|vNdt@ z*IIS0f0n-j0ANQr=@LjK1Q4i%Ju&1{Zdf455b)FUKS*73J>=)*+T_d(hi?4-E(3}L Oa+x=wI~B6Oulx_vV%bgr diff --git a/Projects/HubX/HubX.Library.DB/DB/HubX/Context/HubXContext.cs b/Projects/HubX/HubX.Library.DB/DB/HubX/Context/HubXContext.cs index a125fdb..488d700 100644 --- a/Projects/HubX/HubX.Library.DB/DB/HubX/Context/HubXContext.cs +++ b/Projects/HubX/HubX.Library.DB/DB/HubX/Context/HubXContext.cs @@ -19,7 +19,7 @@ public partial class HubXContext : DbContext protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) #warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263. - => optionsBuilder.UseSqlServer("server=127.0.0.1; user id=VPKI; password=Kefico!@34; database=HubX; TrustServerCertificate=true;"); + => optionsBuilder.UseSqlServer("server=127.0.0.1; user id=alis; password=Kefico!@34; database=HubX; TrustServerCertificate=true;"); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/Projects/HubX/HubX.Server/Controllers/UniqueKeyController.cs b/Projects/HubX/HubX.Server/Controllers/UniqueKeyController.cs index 2168f24..6e8bf6e 100644 --- a/Projects/HubX/HubX.Server/Controllers/UniqueKeyController.cs +++ b/Projects/HubX/HubX.Server/Controllers/UniqueKeyController.cs @@ -1,4 +1,5 @@ -using HubX.Library.Http.Packet; +using Azure.Core; +using HubX.Library.Http.Packet; using HubX.Server.Services; using Microsoft.AspNetCore.Mvc; @@ -47,6 +48,30 @@ namespace HubX.Server.Controllers return Results.Ok(res); } + [HttpGet] + public async Task SelectUniqueKeyGet([FromQuery] string key) + { + var guid = Guid.NewGuid(); + // Log4net.WriteLine($"[Requeust]({guid}) UniqueKey/SelectUniqueKey::{request.ToJson()}", LogType.CONTROLLER); + + Response_SelectUniqueKy res = await _uniqueKeyService.Request_SelectUniqueKey(new Request_SelectUniqueKey { Identity = key } ); + // Log4net.WriteLine($"[Response]({guid}) UniqueKey/SelectUniqueKey::{res.ToJson()}", LogType.CONTROLLER); + + return Results.Ok(res); + } + + [HttpGet] + public async Task SelectUniqueKeyGetAll() + { + var guid = Guid.NewGuid(); + // Log4net.WriteLine($"[Requeust]({guid}) UniqueKey/SelectUniqueKey::{request.ToJson()}", LogType.CONTROLLER); + + var res = await _uniqueKeyService.Request_SelectUniqueKeyAll(); + // Log4net.WriteLine($"[Response]({guid}) UniqueKey/SelectUniqueKey::{res.ToJson()}", LogType.CONTROLLER); + + return Results.Ok(res); + } + [HttpPost] public async Task UpdateUniqueKey(Request_UpdateUniqueKey request) { diff --git a/Projects/HubX/HubX.Server/Services/UniqueKeyService.cs b/Projects/HubX/HubX.Server/Services/UniqueKeyService.cs index 7b05f6c..36b7d2b 100644 --- a/Projects/HubX/HubX.Server/Services/UniqueKeyService.cs +++ b/Projects/HubX/HubX.Server/Services/UniqueKeyService.cs @@ -4,6 +4,7 @@ using HubX.Library.Http.Packet; using Microsoft.EntityFrameworkCore; using Microsoft.Identity.Client.Extensions.Msal; using System; +using System.Collections.Generic; using System.Data; using System.Xml; using SystemX.Core.DB; @@ -116,6 +117,33 @@ namespace HubX.Server.Services return response; } + public async Task> Request_SelectUniqueKeyAll(string guid = "") + { + List result = new List(); + using (var scope = _scopeFactory.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + if (context != null) + { + try + { + using (var transaction = await context.CreateTransactionAsync(IsolationLevel.ReadUncommitted)) + { + result = await context.TStorages.AsNoTracking().ToListAsync(); + await context.CloseTransactionAsync(transaction); + } + } + catch (Exception e) + { + Log4net.WriteLine($"Select Unique Key Transaction Error::{guid}", LogType.Error); + Log4net.WriteLine(e); + } + } + } + + return result; + } + public async Task Request_UpdateUniqueKey(Request_UpdateUniqueKey request, string guid = "") { Response_UpdateUniqueKy response = new Response_UpdateUniqueKy(); diff --git a/Projects/SystemX.Core/DBPatch/CreateAccountDB.bat b/Projects/SystemX.Core/DBPatch/CreateAccountDB.bat new file mode 100644 index 0000000..d2c77f5 --- /dev/null +++ b/Projects/SystemX.Core/DBPatch/CreateAccountDB.bat @@ -0,0 +1,18 @@ +@echo off +::log +IF NOT EXIST .\logs mkdir logs + +::서버연결정보 +SET ServerIP=127.0.0.1 +SET ServerPort=1433 + +::DB 정보 +SET UserID=SystemX +SET Passwd=X +SET DBName=AccountDB + +::Default DB +@echo off +CD .\sqlScripts\ +CALL _CreateScript.bat %ServerIP% %ServerPort% %UserID% %Passwd% %DBName% +CALL _CreateScript.bat %ServerIP% %ServerPort% %UserID% %Passwd% %DBName%_DEV \ No newline at end of file diff --git a/Projects/SystemX.Core/DBPatch/UpdateAccountDB.bat b/Projects/SystemX.Core/DBPatch/UpdateAccountDB.bat new file mode 100644 index 0000000..0d0b8db --- /dev/null +++ b/Projects/SystemX.Core/DBPatch/UpdateAccountDB.bat @@ -0,0 +1,25 @@ +@echo off +::log +IF NOT EXIST .\logs mkdir logs + +::서버연결정보 +SET ServerIP=127.0.0.1 +SET ServerPort=1433 + +::DB 정보 +SET UserID=SystemX +SET Passwd=X +SET DBName=AccountDB + +::Update script 정보 +SET Dacpac=.\dacpac\SystemX.DB.AccountDB.dacpac +SET OUTPUT=SystemX.DB.AccountDB_Update.sql + +@echo off +::generate update script +CD .\sqlScripts\ +CALL _UpdateScriptGenerate.bat %ServerIP% %ServerPort% %UserID% %Passwd% %DBName% %Dacpac% %OUTPUT% + +::Default DB +CALL _UpdateAccountDB.bat %ServerIP% %ServerPort% %UserID% %Passwd% %DBName% +CALL _UpdateAccountDB.bat %ServerIP% %ServerPort% %UserID% %Passwd% %DBName%_DEV \ No newline at end of file diff --git a/Projects/SystemX.Core/DBPatch/_CreateSqlServerAccount_관리자권한으로실행.bat b/Projects/SystemX.Core/DBPatch/_CreateSqlServerAccount_관리자권한으로실행.bat new file mode 100644 index 0000000..c6c8430 --- /dev/null +++ b/Projects/SystemX.Core/DBPatch/_CreateSqlServerAccount_관리자권한으로실행.bat @@ -0,0 +1,19 @@ +@echo Create Admin Account Start +@echo off + +SET SqlCmdOption=-E -C +sqlcmd %SqlCmdOption% -i %~dp0\sqlScripts\AdminAccount_Create.sql +if errorlevel 1 goto errexit +goto end +:errexit +echo DB Patch Fail +goto end +:end +@echo on + +@echo Create Admin Account End + +net stop /y MSSQLSERVER +net start /y MSSQLSERVER + +pause \ No newline at end of file diff --git a/Projects/SystemX.Core/DBPatch/sqlScripts/AdminAccount_Create.sql b/Projects/SystemX.Core/DBPatch/sqlScripts/AdminAccount_Create.sql new file mode 100644 index 0000000..744268c --- /dev/null +++ b/Projects/SystemX.Core/DBPatch/sqlScripts/AdminAccount_Create.sql @@ -0,0 +1,43 @@ +USE [master] +GO + +CREATE LOGIN [SystemX] WITH PASSWORD=N'X', DEFAULT_DATABASE=[master], DEFAULT_LANGUAGE=[English], CHECK_POLICY=ON +GO + +ALTER LOGIN [SystemX] ENABLE +GO + +ALTER SERVER ROLE [sysadmin] ADD MEMBER [SystemX] +GO + +ALTER SERVER ROLE [securityadmin] ADD MEMBER [SystemX] +GO + +ALTER SERVER ROLE [serveradmin] ADD MEMBER [SystemX] +GO + +ALTER SERVER ROLE [setupadmin] ADD MEMBER [SystemX] +GO + + + +USE [master] +GO + +CREATE LOGIN [Alis] WITH PASSWORD=N'Kefico!@34', DEFAULT_DATABASE=[master], DEFAULT_LANGUAGE=[English], CHECK_POLICY=ON +GO + +ALTER LOGIN [Alis] ENABLE +GO + +ALTER SERVER ROLE [sysadmin] ADD MEMBER [Alis] +GO + +ALTER SERVER ROLE [securityadmin] ADD MEMBER [Alis] +GO + +ALTER SERVER ROLE [serveradmin] ADD MEMBER [Alis] +GO + +ALTER SERVER ROLE [setupadmin] ADD MEMBER [Alis] +GO \ No newline at end of file diff --git a/Projects/SystemX.Core/DBPatch/sqlScripts/SystemX.DB.AccountDB_Create.sql b/Projects/SystemX.Core/DBPatch/sqlScripts/SystemX.DB.AccountDB_Create.sql new file mode 100644 index 0000000..72848a5 --- /dev/null +++ b/Projects/SystemX.Core/DBPatch/sqlScripts/SystemX.DB.AccountDB_Create.sql @@ -0,0 +1,346 @@ +/* +SystemX.DB.AccountDB의 배포 스크립트 + +이 코드는 도구를 사용하여 생성되었습니다. +파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면 +변경 내용이 손실됩니다. +*/ + +GO +SET ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, ARITHABORT, CONCAT_NULL_YIELDS_NULL, QUOTED_IDENTIFIER ON; + +SET NUMERIC_ROUNDABORT OFF; + + +GO +/* +:setvar DatabaseName "SystemX.DB.AccountDB" +:setvar DefaultFilePrefix "SystemX.DB.AccountDB" +:setvar DefaultDataPath "" +:setvar DefaultLogPath "" +*/ + +GO +:on error exit +GO +/* +SQLCMD 모드가 지원되지 않으면 SQLCMD 모드를 검색하고 스크립트를 실행하지 않습니다. +SQLCMD 모드를 설정한 후에 이 스크립트를 다시 사용하려면 다음을 실행합니다. +SET NOEXEC OFF; +*/ +:setvar __IsSqlCmdEnabled "True" +GO +IF N'$(__IsSqlCmdEnabled)' NOT LIKE N'True' + BEGIN + PRINT N'이 스크립트를 실행하려면 SQLCMD 모드를 사용하도록 설정해야 합니다.'; + SET NOEXEC ON; + END + + +GO +USE [master]; + + +GO + +IF (DB_ID(N'$(DatabaseName)') IS NOT NULL) +BEGIN + ALTER DATABASE [$(DatabaseName)] + SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE [$(DatabaseName)]; +END + +GO +PRINT N'$(DatabaseName) 데이터베이스를 만드는 중...' +GO +CREATE DATABASE [$(DatabaseName)] COLLATE Korean_Wansung_CI_AS +GO +USE [$(DatabaseName)]; + + +GO +IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + ALTER DATABASE [$(DatabaseName)] + SET ANSI_NULLS ON, + ANSI_PADDING ON, + ANSI_WARNINGS ON, + ARITHABORT ON, + CONCAT_NULL_YIELDS_NULL ON, + NUMERIC_ROUNDABORT OFF, + QUOTED_IDENTIFIER ON, + ANSI_NULL_DEFAULT ON, + CURSOR_DEFAULT LOCAL, + RECOVERY FULL, + CURSOR_CLOSE_ON_COMMIT OFF, + AUTO_CREATE_STATISTICS ON, + AUTO_SHRINK OFF, + AUTO_UPDATE_STATISTICS ON, + RECURSIVE_TRIGGERS OFF + WITH ROLLBACK IMMEDIATE; + END + + +GO +IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + ALTER DATABASE [$(DatabaseName)] + SET ALLOW_SNAPSHOT_ISOLATION OFF; + END + + +GO +IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + ALTER DATABASE [$(DatabaseName)] + SET READ_COMMITTED_SNAPSHOT OFF + WITH ROLLBACK IMMEDIATE; + END + + +GO +IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + ALTER DATABASE [$(DatabaseName)] + SET AUTO_UPDATE_STATISTICS_ASYNC OFF, + PAGE_VERIFY NONE, + DATE_CORRELATION_OPTIMIZATION OFF, + DISABLE_BROKER, + PARAMETERIZATION SIMPLE, + SUPPLEMENTAL_LOGGING OFF + WITH ROLLBACK IMMEDIATE; + END + + +GO +IF IS_SRVROLEMEMBER(N'sysadmin') = 1 + BEGIN + IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + EXECUTE sp_executesql N'ALTER DATABASE [$(DatabaseName)] + SET TRUSTWORTHY OFF, + DB_CHAINING OFF + WITH ROLLBACK IMMEDIATE'; + END + END +ELSE + BEGIN + PRINT N'데이터베이스 설정을 수정할 수 없습니다. 이러한 설정을 적용하려면 SysAdmin이어야 합니다.'; + END + + +GO +IF IS_SRVROLEMEMBER(N'sysadmin') = 1 + BEGIN + IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + EXECUTE sp_executesql N'ALTER DATABASE [$(DatabaseName)] + SET HONOR_BROKER_PRIORITY OFF + WITH ROLLBACK IMMEDIATE'; + END + END +ELSE + BEGIN + PRINT N'데이터베이스 설정을 수정할 수 없습니다. 이러한 설정을 적용하려면 SysAdmin이어야 합니다.'; + END + + +GO +ALTER DATABASE [$(DatabaseName)] + SET TARGET_RECOVERY_TIME = 0 SECONDS + WITH ROLLBACK IMMEDIATE; + + +GO +IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + ALTER DATABASE [$(DatabaseName)] + SET FILESTREAM(NON_TRANSACTED_ACCESS = OFF), + CONTAINMENT = NONE + WITH ROLLBACK IMMEDIATE; + END + + +GO +IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + ALTER DATABASE [$(DatabaseName)] + SET AUTO_CREATE_STATISTICS ON(INCREMENTAL = OFF), + MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT = OFF, + DELAYED_DURABILITY = DISABLED + WITH ROLLBACK IMMEDIATE; + END + + +GO +IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + ALTER DATABASE [$(DatabaseName)] + SET QUERY_STORE (QUERY_CAPTURE_MODE = ALL, DATA_FLUSH_INTERVAL_SECONDS = 900, INTERVAL_LENGTH_MINUTES = 60, MAX_PLANS_PER_QUERY = 200, CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 367), MAX_STORAGE_SIZE_MB = 100) + WITH ROLLBACK IMMEDIATE; + END + + +GO +IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + ALTER DATABASE [$(DatabaseName)] + SET QUERY_STORE = OFF + WITH ROLLBACK IMMEDIATE; + END + + +GO +IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + ALTER DATABASE SCOPED CONFIGURATION SET MAXDOP = 0; + ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY SET MAXDOP = PRIMARY; + ALTER DATABASE SCOPED CONFIGURATION SET LEGACY_CARDINALITY_ESTIMATION = OFF; + ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY SET LEGACY_CARDINALITY_ESTIMATION = PRIMARY; + ALTER DATABASE SCOPED CONFIGURATION SET PARAMETER_SNIFFING = ON; + ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY SET PARAMETER_SNIFFING = PRIMARY; + ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES = OFF; + ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY SET QUERY_OPTIMIZER_HOTFIXES = PRIMARY; + END + + +GO +IF EXISTS (SELECT 1 + FROM [master].[dbo].[sysdatabases] + WHERE [name] = N'$(DatabaseName)') + BEGIN + ALTER DATABASE [$(DatabaseName)] + SET TEMPORAL_HISTORY_RETENTION ON + WITH ROLLBACK IMMEDIATE; + END + + +GO +IF fulltextserviceproperty(N'IsFulltextInstalled') = 1 + EXECUTE sp_fulltext_database 'enable'; + + +GO +PRINT N'테이블 [dbo].[tRefreshToken]을(를) 만드는 중...'; + + +GO +CREATE TABLE [dbo].[tRefreshToken] ( + [cAuid] NVARCHAR (250) NOT NULL, + [cRefreshToken] NVARCHAR (1000) NOT NULL, + PRIMARY KEY CLUSTERED ([cAuid] ASC) +); + + +GO +PRINT N'테이블 [dbo].[tRole]을(를) 만드는 중...'; + + +GO +CREATE TABLE [dbo].[tRole] ( + [cAuid] NVARCHAR (250) NOT NULL, + [cRoleID] TINYINT NOT NULL, + [cRoleName] NVARCHAR (20) NOT NULL, + PRIMARY KEY CLUSTERED ([cAuid] ASC) +); + + +GO +PRINT N'테이블 [dbo].[tUser]을(를) 만드는 중...'; + + +GO +CREATE TABLE [dbo].[tUser] ( + [cUserID] NVARCHAR (50) NOT NULL, + [cAuid] NVARCHAR (250) NOT NULL, + [cPasswordHashed] NVARCHAR (250) NOT NULL, + [cState] TINYINT NOT NULL, + [cCreateDateTime] DATETIME2 (7) NOT NULL, + [cLastLoginDateTime] DATETIME2 (7) NULL, + PRIMARY KEY CLUSTERED ([cUserID] ASC) +); + + +GO +/* +배포 후 스크립트 템플릿 +-------------------------------------------------------------------------------------- + 이 파일에는 빌드 스크립트에 추가될 SQL 문이 있습니다. + SQLCMD 구문을 사용하여 파일을 배포 후 스크립트에 포함합니다. + 예: :r .\myfile.sql + SQLCMD 구문을 사용하여 배포 후 스크립트의 변수를 참조합니다. + 예: :setvar TableName MyTable + SELECT * FROM [$(TableName)] +-------------------------------------------------------------------------------------- +*/ + +IF NOT EXISTS (SELECT 1 FROM tUser WHERE cUserID = 'Alis') +BEGIN + INSERT INTO tUser (cUserID, cAuid, cPasswordHashed, cState, cCreateDateTime, cLastLoginDateTime) + VALUES ('Alis', 'SuperUserAlis' ,'oKLQCdunc2kT5aAVfK+POKwd8R3p8OZvs/NATwpg4gM=' ,1 ,GETDATE(), GETDATE()); + + INSERT INTO tRole(cAuid, cRoleID, cRoleName) + VALUES ('SuperUserAlis','20','SuperUser'); +END + +IF NOT EXISTS (SELECT 1 FROM tUser WHERE cUserID = 'SystemX') +BEGIN + INSERT INTO tUser (cUserID, cAuid, cPasswordHashed, cState, cCreateDateTime, cLastLoginDateTime) + VALUES ('SystemX', 'SuperUserSystemX' ,'S2irOEf+2n1sYsH7y+6/o16rc1HtXnj03a3qXfZLgBU=' ,1 ,GETDATE(), GETDATE()); + + INSERT INTO tRole(cAuid, cRoleID, cRoleName) + VALUES ('SuperUserSystemX','20','SuperUser'); +END +GO + +GO +DECLARE @VarDecimalSupported AS BIT; + +SELECT @VarDecimalSupported = 0; + +IF ((ServerProperty(N'EngineEdition') = 3) + AND (((@@microsoftversion / power(2, 24) = 9) + AND (@@microsoftversion & 0xffff >= 3024)) + OR ((@@microsoftversion / power(2, 24) = 10) + AND (@@microsoftversion & 0xffff >= 1600)))) + SELECT @VarDecimalSupported = 1; + +IF (@VarDecimalSupported > 0) + BEGIN + EXECUTE sp_db_vardecimal_storage_format N'$(DatabaseName)', 'ON'; + END + + +GO +ALTER DATABASE [$(DatabaseName)] + SET MULTI_USER + WITH ROLLBACK IMMEDIATE; + + +GO +PRINT N'업데이트가 완료되었습니다.'; + + +GO diff --git a/Projects/SystemX.Core/DBPatch/sqlScripts/_CreateScript.bat b/Projects/SystemX.Core/DBPatch/sqlScripts/_CreateScript.bat new file mode 100644 index 0000000..1627dc9 --- /dev/null +++ b/Projects/SystemX.Core/DBPatch/sqlScripts/_CreateScript.bat @@ -0,0 +1,18 @@ +@echo off + +SET ServerIP=%1 +SET ServerPort=%2 +SET UserID=%3 +SET Passwd=%4 +SET DBName=%5 + +SET SqlCmdOption=-C -U %UserID% -P %Passwd% -S %ServerIP%,%ServerPort% -f 65001 -o ..\logs\%DBName%.log +SET DatabaseName=%DBName% +sqlcmd %SqlCmdOption% -i .\SystemX.DB.AccountDB_Create.sql +if errorlevel 1 goto errexit +goto end +:errexit +echo DB Patch Failed +goto end +:end +@echo on \ No newline at end of file diff --git a/Projects/SystemX.Core/DBPatch/sqlScripts/_UpdateAccountDB.bat b/Projects/SystemX.Core/DBPatch/sqlScripts/_UpdateAccountDB.bat new file mode 100644 index 0000000..d2df2be --- /dev/null +++ b/Projects/SystemX.Core/DBPatch/sqlScripts/_UpdateAccountDB.bat @@ -0,0 +1,19 @@ +@echo off + +SET ServerIP=%1 +SET ServerPort=%2 +SET UserID=%3 +SET Passwd=%4 +SET DBName=%5 + +SET SqlCmdOption=-U %UserID% -P %Passwd% -S %ServerIP%,%ServerPort% -d %DBName% -o ..\logs\%DBName%.log +SET DatabaseName=%DBName% +sqlcmd %SqlCmdOption% -i .\SystemX.DB.AccountDB_Update.sql + +if errorlevel 1 goto errexit +goto end +:errexit +echo DB Patch Fail +goto end +:end +@echo on \ No newline at end of file diff --git a/Projects/SystemX.Core/DBPatch/sqlScripts/_UpdateScriptGenerate.bat b/Projects/SystemX.Core/DBPatch/sqlScripts/_UpdateScriptGenerate.bat new file mode 100644 index 0000000..56db9d9 --- /dev/null +++ b/Projects/SystemX.Core/DBPatch/sqlScripts/_UpdateScriptGenerate.bat @@ -0,0 +1,12 @@ +@echo off + +SET ServerIP=%1 +SET ServerPort=%2 +SET UserID=%3 +SET Passwd=%4 +SET DBName=%5 +SET Dacpac=%6 +SET OUTPUT=%7 + +::create update sql file +sqlpackage /Action:Script /SourceFile:%Dacpac% /TargetConnectionString:"server=%ServerIP%,%ServerPort%; user id=%UserID%; password=%Passwd%; database=%DBName%; TrustServerCertificate=true" /OutputPath:".\%OUTPUT%" /p:CommentOutSetVarDeclarations=True \ No newline at end of file diff --git a/Projects/SystemX.Core/DBPatch/sqlScripts/dacpac/SystemX.DB.AccountDB.dacpac b/Projects/SystemX.Core/DBPatch/sqlScripts/dacpac/SystemX.DB.AccountDB.dacpac new file mode 100644 index 0000000000000000000000000000000000000000..67cb16e7592f468ffedd2894d8c1c69075e430b2 GIT binary patch literal 3503 zcmai%X*d*I8^=c&W8bq)St2__M3!VMgE7VyStc}=>^q54s3iMjFR~3`#@HKKvoB+d zvSkfLJXx|Yujk8qrFVJGxz2}kUHAXP|2p^myYFKNC#PTo001CB0%FZFN`#SDj}ic= z5(faN0bqc;r@e!l)IE1Mz8p?~$eX_d|8uQcy8DhRG9AClAf7bs9@s%`N;Ss$pH^1p z4WtT^UK(hR(iSEOnJ0eSeRzb6Zmt`BJh|YLJ*xqfaJH#D_>s%*y-Mj=7Ry;&;VDi& zsnnxqe<(->zSS{|fj-Zc;_+X|WYSL(W^ve|x)OTOFYf4ZgH8W(t+BNcZ{AHG3bbbK zcs1;SoOzJtYaYL73NJ1BdZ-C3LX@EQP8rWoQub+9e8TLx_Du%IhxY^sBaGH!GQeu2 z$^@k@b#ADT-l~g3qlxAXI$LQ8ny-)K+ z=z&%R*UJa}i*Tn1EyW=W^L=Ne-+0;(PjpCB1h8>|{Ij2q3}mm`Ecu8Q6AOYycRZ3U zLSs(OWfZSqwPkQMl|kn;;z3%&gD*5#Rr2{b3GbQ$tfC8HsX?h0{^)%EU26|;@=B(# z0De8Ae{?vBT?jZr%gtT7e~<*<`LHS@ai`|gY*h2^q{v= z0dH_j%fqFg4;lwmR32m~qdY1no-iO3>I1BvaM$vY7wOB_zPZBwE7vhwXGu7#1wLC| z^QZ@6bE}F0L!Y$(b<--z?<>q92WChiK?TQ{PI{rqzvTP`1t}-FuZA0TWKbJ~ueLV> z(nJ|Sn9iL7F2!f+ywuA)_bf@k3;fwxDY*@tAu9^DOffzMJ=tpS91>CosE)bRyFWZC za(*kpP@v=*&~8&Dh-Ga<0LcXBUO6ImCdbnX|-m{(xyXgT`9YXd|-t3mm*;MTqhMA?-wQjGOp|9c{o&n;RZ1;`)Hrpogg!Nq0_5CTWg#0}6>-8P+Qh{nP`XH%0mtyQS7>Kb^{A))0&d-3r znuL(6$o*uho;lhFv(iQu%G*-*e--SsOo?K93%+yKP~6-hXF&u-ljg|TeE9VeUli3c> zk~0S8=1_Nn_lYQuP%2U6)VasN<6qylw>R*x#l!pSCxq}g^8tt1i z{xBgWqXSqmtMcMwz|?tW_Sk15gP-g#^(Tl=u;pwoWj`4)7Q`5 z!OP7vNXpmy{}Q>)2<7_Gkh9@7-^pw}(@@+f9@i%`ruEs;-8tP2p|mpmVia3dVFMpZ znnDOeb}vONX6 zzTu_EI7PNa^dFp%-<}R5^o3>b;MBLUvD^jix4(=k`&h{0NH-)v90mI`pzU9#uu_BG z0ICb$&;oS*ULG=blWw7q31Lhy?uAi714X!{ag%?eVb9l%a`>0Ne#G%0Z>N({fU<4K zq}H$4^)DwNXRlb%;Jl94#Yc$qYR$8dMi~z21&4{z#9uOmyYk_~580I@ z zqR3nD^EHbP5 zEVC=70v>0N%t+U#NX0_MnYh2d{5FD$xJPD@=ooJ==fUzJ5ZaTI781YTxg6!rym}Xk z@=nbq`t1&}Gp}7@2)%TtIj;B~`B}%z7WJ;;r?rTp003xC9V305ot!=X$!xx9PizK+ z?Th)c8ejDV0s;~T;#^SAD@r%W%lo1{Drg39ax+2fyP~!*`?T`Z1K92ovC@#gRCmBx zt_PpvvbyG7Q5Mswrq*(AW32C5m55R-kgiu*p_CGiQtLxgBy`s$_A(Wc$>hYU7%j7~ zfWo;Zmz>FT4r37`ml`H{L+PYDLzR>-*=$<`h1pLGD*4GSYF>Pl>0WDTR(`oH;m4R| z6e}kEia&Km>*r#u+uR2aAzRSa-jlm>voYN^NT`c;iYBH+@Tme>ps%V&uFxg_X*b)0 z7AMqZmM+Qoeal!{piQ$CXFyRa(J^MtRHwnUD!#jKfM*F1NFg-SAlX$hhO!0^@V@1r z+kdr3C!+>!O=Hr#&?xej^`nRn4L91doJFG!-gN7Fx_Vxp$~QZzrwki4kj(vKKUwd2 zMH)z~S}Gvobtj$xTcEEGU;m;AB5(weW5rjP7r~X1eoKbia#Jt+ZU>5M=1-CqxQSLd zv5uR@s<-O-MQQGz@Rn#XT&kvt8r9#G*@%oo+nXDuNtLyQ-CM9pUF2Ub~hn66OlW?}yN7H0=unxrAF9nh_WI z$1N1YdqBA&w`TKspV3OgW+$`eRbaH>h|N73JYPELP$;^EFJf=VhP1)8y=bJkDKM>) z+z=?ekgOcET!^=%vQbemXnOr_PpD}ZVgD!k)#h~dGER$Y{`9b(rkj<9r-z?|ho6mU zke7om>Ywt?Jr>Rq2BCXeI@EPBObXnQO)&JFp;VGr(bf^CXUHTyfo;&7A1y!RwK+b# zK3(@}-=O|32#2PBFGsHB_N9KovPI;kPeggArzcXqO^k{@CQ^+bIAQ1sT~_hd7gN~M z&H=k@wO`jqyz4I~x9osQJnvO_(tc8I;Xx*8$2AeN;2V&xZK>9r~$uW;ZNP!7yVrqNelRI?`sIB1pXaHdAdc(PCe26eft*z)9Zx* literal 0 HcmV?d00001 diff --git a/Projects/SystemX.Core/SystemX.Core.sln b/Projects/SystemX.Core/SystemX.Core.sln index 61b9793..15eec93 100644 --- a/Projects/SystemX.Core/SystemX.Core.sln +++ b/Projects/SystemX.Core/SystemX.Core.sln @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemX.Core", "SystemX.Core\SystemX.Core.csproj", "{F057A1E8-F5FF-4241-BEEA-1A57E971F379}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemX.Core", "SystemX.Core\SystemX.Core.csproj", "{F057A1E8-F5FF-4241-BEEA-1A57E971F379}" +EndProject +Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "SystemX.DB.AccountDB", "SystemX.DB.AccountDB\SystemX.DB.AccountDB.sqlproj", "{B44C85FA-BD31-419F-8481-477E166A5753}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,12 @@ Global {F057A1E8-F5FF-4241-BEEA-1A57E971F379}.Debug|Any CPU.Build.0 = Debug|Any CPU {F057A1E8-F5FF-4241-BEEA-1A57E971F379}.Release|Any CPU.ActiveCfg = Release|Any CPU {F057A1E8-F5FF-4241-BEEA-1A57E971F379}.Release|Any CPU.Build.0 = Release|Any CPU + {B44C85FA-BD31-419F-8481-477E166A5753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B44C85FA-BD31-419F-8481-477E166A5753}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B44C85FA-BD31-419F-8481-477E166A5753}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {B44C85FA-BD31-419F-8481-477E166A5753}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B44C85FA-BD31-419F-8481-477E166A5753}.Release|Any CPU.Build.0 = Release|Any CPU + {B44C85FA-BD31-419F-8481-477E166A5753}.Release|Any CPU.Deploy.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Projects/SystemX.Core/SystemX.Core/Config/Model/Auth.cs b/Projects/SystemX.Core/SystemX.Core/Config/Model/Auth.cs new file mode 100644 index 0000000..bc5f7ab --- /dev/null +++ b/Projects/SystemX.Core/SystemX.Core/Config/Model/Auth.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace SystemX.Core.Config.Model +{ + public class Auth + { + //jwt common + [JsonPropertyName("issuer")] + public string? issuer { get; set; } + + [JsonPropertyName("audience")] + public string? audience { get; set; } + + //access token + [JsonPropertyName("accessTokenSecret")] + public string? accessTokenSecret { get; set; } + + [JsonPropertyName("accessTokenExpires")] + public uint? accessTokenExpires { get; set; } + + //refresh token + [JsonPropertyName("refreshTokenSecret")] + public string? refreshTokenSecret { get; set; } + + [JsonPropertyName("refreshTokenExpires")] + public uint? refreshTokenExpires { get; set; } + } +} diff --git a/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Context/AccountDbContext.cs b/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Context/AccountDbContext.cs new file mode 100644 index 0000000..f330eda --- /dev/null +++ b/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Context/AccountDbContext.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using SystemX.Core.DB.DBContext.AccountDB.Tables; + +namespace SystemX.Core.DB.DBContext.AccountDB.Context; + +public partial class AccountDbContext : DbContext +{ + public AccountDbContext() + { + } + + public AccountDbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet TRefreshTokens { get; set; } + + public virtual DbSet TRoles { get; set; } + + public virtual DbSet TUsers { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263. + => optionsBuilder.UseSqlServer("server=127.0.0.1; user id=SystemX; password=X; database=AccountDB; TrustServerCertificate=true;"); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.CAuid).HasName("PK__tRefresh__FBF0855465EB95AB"); + + entity.ToTable("tRefreshToken"); + + entity.Property(e => e.CAuid) + .HasMaxLength(250) + .HasColumnName("cAuid"); + entity.Property(e => e.CRefreshToken) + .HasMaxLength(1000) + .HasColumnName("cRefreshToken"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.CAuid).HasName("PK__tRole__FBF085540BB887D7"); + + entity.ToTable("tRole"); + + entity.Property(e => e.CAuid) + .HasMaxLength(250) + .HasColumnName("cAuid"); + entity.Property(e => e.CRoleId).HasColumnName("cRoleID"); + entity.Property(e => e.CRoleName) + .HasMaxLength(20) + .HasColumnName("cRoleName"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.CUserId).HasName("PK__tUser__A75DC19A721265FF"); + + entity.ToTable("tUser"); + + entity.Property(e => e.CUserId) + .HasMaxLength(50) + .HasColumnName("cUserID"); + entity.Property(e => e.CAuid) + .HasMaxLength(250) + .HasColumnName("cAuid"); + entity.Property(e => e.CCreateDateTime).HasColumnName("cCreateDateTime"); + entity.Property(e => e.CLastLoginDateTime).HasColumnName("cLastLoginDateTime"); + entity.Property(e => e.CPasswordHashed) + .HasMaxLength(250) + .HasColumnName("cPasswordHashed"); + entity.Property(e => e.CState).HasColumnName("cState"); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TRefreshToken.cs b/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TRefreshToken.cs new file mode 100644 index 0000000..c2578dc --- /dev/null +++ b/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TRefreshToken.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace SystemX.Core.DB.DBContext.AccountDB.Tables; + +public partial class TRefreshToken +{ + public string CAuid { get; set; } = null!; + + public string CRefreshToken { get; set; } = null!; +} diff --git a/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TRole.cs b/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TRole.cs new file mode 100644 index 0000000..5404196 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TRole.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace SystemX.Core.DB.DBContext.AccountDB.Tables; + +public partial class TRole +{ + public string CAuid { get; set; } = null!; + + public byte CRoleId { get; set; } + + public string CRoleName { get; set; } = null!; +} diff --git a/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TUser.cs b/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TUser.cs new file mode 100644 index 0000000..4a10d36 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.Core/DB/DBContext/AccountDB/Tables/TUser.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace SystemX.Core.DB.DBContext.AccountDB.Tables; + +public partial class TUser +{ + public string CUserId { get; set; } = null!; + + public string CAuid { get; set; } = null!; + + public string CPasswordHashed { get; set; } = null!; + + public byte CState { get; set; } + + public DateTime CCreateDateTime { get; set; } + + public DateTime? CLastLoginDateTime { get; set; } +} diff --git a/Projects/SystemX.Core/SystemX.Core/ERROR_CODE.cs b/Projects/SystemX.Core/SystemX.Core/ERROR_CODE.cs new file mode 100644 index 0000000..e58b1e6 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.Core/ERROR_CODE.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SystemX.Core +{ + public enum ERROR_CODE + { + //기본 에러 + EC_NONE = 0, + EC_OK = 1, + + //DB 관련 에러 + EC_DEFAULT_ERROR = 1000, + EC_DATABASE_ERROR = 1001, + + //유저 관련 에러 + EC_USER_REGISTER_FAILED = 2000, + EC_USER_REGISTER_EXIST = 2001, + EC_USER_REGISTER_CONFIRM_PASSWORD_ERROR = 2002, + + EC_USER_LOGIN_FAILED = 2100, + EC_USER_LOGIN_NOT_EXIST = 2101, + EC_USER_LOGIN_INVALID_PASSWORD = 2102, + EC_USER_LOGIN_INAVTIVE = 2103, + EC_USER_LOGIN_BLOCKED = 2104, + + EC_USER_LOGOUT_FAILED = 2200 + } +} diff --git a/Projects/SystemX.Core/SystemX.Core/Log4net/Log4net.cs b/Projects/SystemX.Core/SystemX.Core/Log4net/Log4net.cs index 30c0754..adef78f 100644 --- a/Projects/SystemX.Core/SystemX.Core/Log4net/Log4net.cs +++ b/Projects/SystemX.Core/SystemX.Core/Log4net/Log4net.cs @@ -57,6 +57,8 @@ public static class Log4net public static bool IsConfigLoad { get; set; } = false; + public static string Log4netConfigPath { get; } = @"../../Config/log4net.config"; + //로그 사용여부 public static bool IsDebugEnabled { get; set; } = true; public static bool IsDBEnabled { get; set; } = true; @@ -70,7 +72,7 @@ public static class Log4net static Log4net() { - string log4netConfigPath = @"../Config/log4net.config"; + string log4netConfigPath = Log4netConfigPath; if (File.Exists(log4netConfigPath) == true) { @@ -82,7 +84,7 @@ public static class Log4net IsConfigLoad = OpenConfig(log4netConfigPath); } - + private static bool OpenConfig(string path) { bool result = true; diff --git a/Projects/SystemX.Core/SystemX.Core/Model/Auth/LoginModel.cs b/Projects/SystemX.Core/SystemX.Core/Model/Auth/LoginModel.cs new file mode 100644 index 0000000..997084b --- /dev/null +++ b/Projects/SystemX.Core/SystemX.Core/Model/Auth/LoginModel.cs @@ -0,0 +1,34 @@ +using log4net.Core; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SystemX.Core.Model.Auth +{ + //로그인 요청 모델 + public class LoginModel + { + [Required] + public string? UserID { get; set; } + + [Required] + public string? Password { get; set; } + } + + //로그인 응답 모델 + public class LoginResponseModel + { + public string? UserID { get; set; } + public UserRole Role { get; set; } + public string? RoleName { get; set; } + + public string? AccessToken { get; set; } + public long AccessTokenExpired { get; set; } + public string? RefreshToken { get; set; } + + public ERROR_CODE? EC { get; set; } + } +} diff --git a/Projects/SystemX.Core/SystemX.Core/Model/Auth/LogoutModel.cs b/Projects/SystemX.Core/SystemX.Core/Model/Auth/LogoutModel.cs new file mode 100644 index 0000000..bf8f7a1 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.Core/Model/Auth/LogoutModel.cs @@ -0,0 +1,23 @@ +using log4net.Core; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SystemX.Core.Model.Auth +{ + public class LogoutModel + { + [Required] + public string? UserID { get; set; } + } + + public class LogoutResponseModel + { + public string? UserID { get; set; } + + public ERROR_CODE? EC { get; set; } + } +} diff --git a/Projects/SystemX.Core/SystemX.Core/Model/Auth/RegisterModel.cs b/Projects/SystemX.Core/SystemX.Core/Model/Auth/RegisterModel.cs new file mode 100644 index 0000000..67085f8 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.Core/Model/Auth/RegisterModel.cs @@ -0,0 +1,35 @@ +using log4net.Core; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SystemX.Core.Model.Auth +{ + //유저 등록 모델 + public class RegisterModel + { + [Required] + public string UserID { get; set; } = string.Empty; + + [Required] + public string Password { get; set; } = string.Empty; + + [Required] + public string PasswordConfirm { get; set; } = string.Empty; + + [Required] + public UserRole Role { get; set; } = UserRole.User; + } + + public class RegisterResponseModel + { + public string? UserID { get; set; } + public UserRole Role { get; set; } + public string? RoleName { get; set; } + + public ERROR_CODE? EC { get; set; } = ERROR_CODE.EC_USER_REGISTER_FAILED; + } +} diff --git a/Projects/SystemX.Core/SystemX.Core/Model/Auth/UserModel.cs b/Projects/SystemX.Core/SystemX.Core/Model/Auth/UserModel.cs new file mode 100644 index 0000000..660f847 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.Core/Model/Auth/UserModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SystemX.Core.Model.Auth +{ + public class UserModel + { + //public TUser? TUser { get; set; } + //public TRole? TRole { get; set; } + } +} diff --git a/Projects/VPKI/VPKI/VPKI.Library/Enums/UserRole.cs b/Projects/SystemX.Core/SystemX.Core/Model/Auth/UserRole.cs similarity index 89% rename from Projects/VPKI/VPKI/VPKI.Library/Enums/UserRole.cs rename to Projects/SystemX.Core/SystemX.Core/Model/Auth/UserRole.cs index b5ca981..07840ae 100644 --- a/Projects/VPKI/VPKI/VPKI.Library/Enums/UserRole.cs +++ b/Projects/SystemX.Core/SystemX.Core/Model/Auth/UserRole.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace VPKI.Library.Enums +namespace SystemX.Core.Model.Auth { //User 권한 public enum UserRole : byte diff --git a/Projects/VPKI/VPKI/VPKI.Library/Enums/UserState.cs b/Projects/SystemX.Core/SystemX.Core/Model/Auth/UserState.cs similarity index 74% rename from Projects/VPKI/VPKI/VPKI.Library/Enums/UserState.cs rename to Projects/SystemX.Core/SystemX.Core/Model/Auth/UserState.cs index 0d48b84..0b15639 100644 --- a/Projects/VPKI/VPKI/VPKI.Library/Enums/UserState.cs +++ b/Projects/SystemX.Core/SystemX.Core/Model/Auth/UserState.cs @@ -4,9 +4,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace VPKI.Library.Enums +namespace SystemX.Core.Model.Auth { - public enum UserState: byte + public enum UserState : byte { Inactive = 0, Active = 1, diff --git a/Projects/SystemX.Core/SystemX.Core/SystemX.Core.csproj b/Projects/SystemX.Core/SystemX.Core/SystemX.Core.csproj index 17016a0..3ffe46e 100644 --- a/Projects/SystemX.Core/SystemX.Core/SystemX.Core.csproj +++ b/Projects/SystemX.Core/SystemX.Core/SystemX.Core.csproj @@ -9,18 +9,26 @@ 9999 + False 9999 + False - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Projects/SystemX.Core/SystemX.DB.AccountDB/SystemX.DB.AccountDB.sqlproj b/Projects/SystemX.Core/SystemX.DB.AccountDB/SystemX.DB.AccountDB.sqlproj new file mode 100644 index 0000000..d94a9b7 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.DB.AccountDB/SystemX.DB.AccountDB.sqlproj @@ -0,0 +1,80 @@ + + + + Debug + AnyCPU + SystemX.DB.AccountDB + 2.0 + 4.1 + {b44c85fa-bd31-419f-8481-477e166a5753} + Microsoft.Data.Tools.Schema.Sql.Sql160DatabaseSchemaProvider + Database + + + SystemX.DB.AccountDB + SystemX.DB.AccountDB + 1042,CI + BySchemaAndSchemaType + True + v4.7.2 + CS + Properties + False + True + True + True + Korean_Wansung_CI_AS + + + bin\Release\ + $(MSBuildProjectName).sql + False + pdbonly + true + false + true + prompt + 4 + + + bin\Debug\ + $(MSBuildProjectName).sql + true + true + full + false + true + true + prompt + 4 + + + 11.0 + + True + 11.0 + + + + + + + + + + + + + + + + + + + + + xcopy /y $(ProjectDir)$(OutputPath)$(TargetName)_Create.sql $(SolutionDir)DBPatch\sqlScripts\ + +xcopy /y $(ProjectDir)$(OutputPath)$(TargetName).dacpac $(SolutionDir)DBPatch\sqlScripts\dacpac\ + + \ No newline at end of file diff --git a/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Scripts/scriptAfterBuild.sql b/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Scripts/scriptAfterBuild.sql new file mode 100644 index 0000000..b57fab3 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Scripts/scriptAfterBuild.sql @@ -0,0 +1,29 @@ +/* +배포 후 스크립트 템플릿 +-------------------------------------------------------------------------------------- + 이 파일에는 빌드 스크립트에 추가될 SQL 문이 있습니다. + SQLCMD 구문을 사용하여 파일을 배포 후 스크립트에 포함합니다. + 예: :r .\myfile.sql + SQLCMD 구문을 사용하여 배포 후 스크립트의 변수를 참조합니다. + 예: :setvar TableName MyTable + SELECT * FROM [$(TableName)] +-------------------------------------------------------------------------------------- +*/ + +IF NOT EXISTS (SELECT 1 FROM tUser WHERE cUserID = 'Alis') +BEGIN + INSERT INTO tUser (cUserID, cAuid, cPasswordHashed, cState, cCreateDateTime, cLastLoginDateTime) + VALUES ('Alis', 'SuperUserAlis' ,'oKLQCdunc2kT5aAVfK+POKwd8R3p8OZvs/NATwpg4gM=' ,1 ,GETDATE(), GETDATE()); + + INSERT INTO tRole(cAuid, cRoleID, cRoleName) + VALUES ('SuperUserAlis','20','SuperUser'); +END + +IF NOT EXISTS (SELECT 1 FROM tUser WHERE cUserID = 'SystemX') +BEGIN + INSERT INTO tUser (cUserID, cAuid, cPasswordHashed, cState, cCreateDateTime, cLastLoginDateTime) + VALUES ('SystemX', 'SuperUserSystemX' ,'S2irOEf+2n1sYsH7y+6/o16rc1HtXnj03a3qXfZLgBU=' ,1 ,GETDATE(), GETDATE()); + + INSERT INTO tRole(cAuid, cRoleID, cRoleName) + VALUES ('SuperUserSystemX','20','SuperUser'); +END \ No newline at end of file diff --git a/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tRefreshToken.sql b/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tRefreshToken.sql new file mode 100644 index 0000000..0ec9fd9 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tRefreshToken.sql @@ -0,0 +1,5 @@ +CREATE TABLE [dbo].[tRefreshToken] +( + [cAuid] NVARCHAR(250) NOT NULL PRIMARY KEY, + [cRefreshToken] NVARCHAR(1000) NOT NULL +) diff --git a/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tRole.sql b/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tRole.sql new file mode 100644 index 0000000..5a49a82 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tRole.sql @@ -0,0 +1,6 @@ +CREATE TABLE [dbo].[tRole] +( + [cAuid] NVARCHAR(250) NOT NULL PRIMARY KEY, + [cRoleID] TINYINT NOT NULL, + [cRoleName] NVARCHAR(20) NOT NULL +) diff --git a/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tUser.sql b/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tUser.sql new file mode 100644 index 0000000..a7e6a64 --- /dev/null +++ b/Projects/SystemX.Core/SystemX.DB.AccountDB/dbo/Tables/tUser.sql @@ -0,0 +1,10 @@ +CREATE TABLE [dbo].[tUser] +( + [cUserID] NVARCHAR(50) NOT NULL, + [cAuid] NVARCHAR(250) NOT NULL , + [cPasswordHashed] NVARCHAR(250) NOT NULL, + [cState] tinyint NOT NULL, + [cCreateDateTime] DATETIME2 NOT NULL, + [cLastLoginDateTime] DATETIME2 NULL, + PRIMARY KEY ([cUserID]) +) diff --git a/Projects/Tools/Tools_Scaffold_AccountDB.bat b/Projects/Tools/Tools_Scaffold_AccountDB.bat new file mode 100644 index 0000000..425d4f8 --- /dev/null +++ b/Projects/Tools/Tools_Scaffold_AccountDB.bat @@ -0,0 +1,5 @@ +::AccountDB +cd ../SystemX.Core/SystemX.Core + +::WebApi +dotnet ef dbcontext scaffold "server=127.0.0.1; user id=SystemX; password=X; database=AccountDB; TrustServerCertificate=true;" Microsoft.EntityFrameworkCore.SqlServer --namespace SystemX.Core.DBContext --context-dir DBContext\AccountDB\Context --output-dir DBContext\AccountDB\Tables -f \ No newline at end of file diff --git a/Projects/WebApi/AuthApi/AuthApi.csproj b/Projects/WebApi/AuthApi/AuthApi.csproj index 9daa180..e65ec2b 100644 --- a/Projects/WebApi/AuthApi/AuthApi.csproj +++ b/Projects/WebApi/AuthApi/AuthApi.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -6,8 +6,32 @@ enable + + True + + + + True + + + + + + + + + + + ..\..\DLL\SystemX.Core.dll + + + + + + + diff --git a/Projects/WebApi/AuthApi/Controllers/AuthController.cs b/Projects/WebApi/AuthApi/Controllers/AuthController.cs new file mode 100644 index 0000000..83c9889 --- /dev/null +++ b/Projects/WebApi/AuthApi/Controllers/AuthController.cs @@ -0,0 +1,146 @@ +using AuthApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using SystemX.Core; +using SystemX.Core.Model.Auth; + +namespace AuthApi.Controllers +{ + [Tags("Auth")] + [Route("api/auth")] + [ApiController] + [ApiExplorerSettings(IgnoreApi = true)] + public class AuthController : CommonController + { + private readonly AuthService _authService; + + public AuthController(IServiceProvider serviceProvider, IHttpContextAccessor httpContextAccessor, + AuthService authService) + : base(serviceProvider, httpContextAccessor) + { + _authService = authService; + } + + [HttpGet("/health")] + public async Task Health() + { + await Task.CompletedTask; + return Results.Ok("Healthy"); + } + + [HttpPost("regisger")] + public async Task Register([FromBody] RegisterModel request) + { + // Log4net.WriteLine(GetRequestLog(request).LogModelToString("Request Auth"), LogType.CONTROLLER); + + RegisterResponseModel response = new RegisterResponseModel(); + + if (request?.UserID != null && request?.Password != null) + { + response = await _authService.CreateUser(request); + } + + // Log4net.WriteLine(GetResponseLog(response).LogModelToString("Response Auth"), LogType.CONTROLLER); + + return Results.Ok(response); + } + + [HttpPost("login")] + public async Task Login([FromBody] LoginModel request) + { + // Log4net.WriteLine(GetRequestLog(request).LogModelToString("Request Auth"), LogType.CONTROLLER); + + LoginResponseModel response = new LoginResponseModel(); + response.UserID = request.UserID; + response.EC = ERROR_CODE.EC_USER_LOGIN_FAILED; + + if (request.UserID != null && request.Password != null) + { + response = await _authService.SelectUser(request); + + if (response.EC == ERROR_CODE.EC_OK) + { + double convertExpires = Convert.ToDouble(_configService?.GetConfig()?.Auth?.accessTokenExpires); + + response.AccessToken = GenerateJwtToken(response); + response.AccessTokenExpired = DateTime.UtcNow.AddMinutes(convertExpires).ToUnixTime(); + + response.RefreshToken = GenerateJwtToken(response, true); + } + + await _authService.UpdateLoginInfo(request, response.RefreshToken); + } + + // Log4net.WriteLine(GetResponseLog(response).LogModelToString("Response Auth"), LogType.CONTROLLER); + + return Results.Ok(response); + } + + [HttpPost("logout")] + public async Task Logout([FromBody] LogoutModel request) + { + // Log4net.WriteLine(GetRequestLog(request).LogModelToString("Request Auth"), LogType.CONTROLLER); + + var response = _authService.LogoutUser(request); + await Task.CompletedTask; + + // Log4net.WriteLine(GetResponseLog(response).LogModelToString("Response Auth"), LogType.CONTROLLER); + + return Results.Ok(response); + } + + [Authorize] + [HttpPost("validate")] + public ActionResult Validate([FromBody] string authToken) + { + return ""; + } + + private TokenValidationParameters GetValidationParameters() + { + return new TokenValidationParameters() + { + ValidateLifetime = true, + ValidateAudience = true, + ValidateIssuer = true, + ValidIssuer = $"{_configService?.GetConfig()?.Auth?.issuer}", + ValidAudience = $"{_configService?.GetConfig()?.Auth?.issuer}", + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes($"{_configService?.GetConfig()?.Auth?.accessTokenSecret}")) + }; + } + + private string GenerateJwtToken(LoginResponseModel loginResponseModel, bool isRefreshToken = false) + { + var claims = new[] + { + new Claim(ClaimTypes.Name, $"{loginResponseModel.UserID}"), + new Claim(ClaimTypes.Role, $"{loginResponseModel.RoleName}"), + }; + + string secret = $"{_configService?.GetConfig()?.Auth?.accessTokenSecret}"; + double convertExpires = Convert.ToDouble(_configService?.GetConfig()?.Auth?.accessTokenExpires); + if (isRefreshToken == true) + { + secret = $"{_configService?.GetConfig()?.Auth?.refreshTokenSecret}"; + convertExpires = Convert.ToDouble(_configService?.GetConfig()?.Auth?.refreshTokenExpires); + } + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: $"{_configService?.GetConfig()?.Auth?.issuer}", + audience: $"{_configService?.GetConfig()?.Auth?.audience}", + claims: claims, + expires: DateTime.UtcNow.AddMinutes(convertExpires), + signingCredentials: creds + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + } +} diff --git a/Projects/WebApi/AuthApi/Controllers/CommonController.cs b/Projects/WebApi/AuthApi/Controllers/CommonController.cs new file mode 100644 index 0000000..ef12193 --- /dev/null +++ b/Projects/WebApi/AuthApi/Controllers/CommonController.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Mvc; +using System.Runtime.CompilerServices; +using SystemX.Core.Services; +using WebApi.Library.Config; + +namespace AuthApi.Controllers +{ + public class CommonController : ControllerBase + { + public readonly IServiceProvider _serviceProvider; + public readonly IHttpContextAccessor _httpContextAccessor; + + public readonly ConfigService? _configService; + + protected static Guid guid { get; private set; } = Guid.NewGuid(); + + public CommonController(IServiceProvider serviceProvider, IHttpContextAccessor httpContextAccessor) + { + //provider + _serviceProvider = serviceProvider; + _httpContextAccessor = httpContextAccessor; + + //service + _configService = _serviceProvider.GetService>(); + } + + /// + /// Request 클라이언트 IP + /// + protected virtual string? GetClientIP() + { + return _httpContextAccessor?.HttpContext?.Connection?.RemoteIpAddress?.ToString(); + } + + /// + /// Request 클라이언트 Url + /// + protected virtual string? GetRequestUrl() + { + return _httpContextAccessor?.HttpContext?.Request?.Path; + } + + /// + /// Request 클라이언트 method: [GET] or [POST] + /// + protected virtual string? GetRequestMethod() + { + return _httpContextAccessor?.HttpContext?.Request?.Method; + } + + /// + /// 현재 Action(함수) 이름 가져오기 + /// + protected virtual string GetMethodName([CallerMemberName] string callerMemberName = "") + { + return callerMemberName; + } + } +} diff --git a/Projects/WebApi/AuthApi/Controllers/WeatherForecastController.cs b/Projects/WebApi/AuthApi/Controllers/WeatherForecastController.cs deleted file mode 100644 index a18781e..0000000 --- a/Projects/WebApi/AuthApi/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace AuthApi.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/Projects/WebApi/AuthApi/Program.cs b/Projects/WebApi/AuthApi/Program.cs index 48863a6..5135652 100644 --- a/Projects/WebApi/AuthApi/Program.cs +++ b/Projects/WebApi/AuthApi/Program.cs @@ -1,25 +1,118 @@ +using AuthApi.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.ComponentModel; +using System.Text; +using SystemX.Core.Services; +using WebApi.Library.Config; + +string configDir = @"../../Config"; +string configFileName = "WebApi.AuthApi.Config.json"; + +//raed log4net configs +if (Log4net.IsConfigLoad == true) +{ + Log4net.WriteLine("Log4net Init Success"); + Log4net.AutoRemoveLog(); +} +else +{ + Console.WriteLine("Log4net Init Failed"); + return; +} + var builder = WebApplication.CreateBuilder(args); // Add services to the container. +//singleton +builder.Services.AddSingleton>(); +builder.Services.AddScoped(); + +//scoped builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +//config preload, auth set +ConfigService preloadConfig = new ConfigService(); +if (preloadConfig.OpenConfig($@"{configDir}/{configFileName}") == true) +{ + var config = preloadConfig.GetConfig(); + + //auth + builder.Services + .AddAuthentication(option => + { + option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ClockSkew = TimeSpan.Zero, + ValidIssuer = $"{config?.Auth?.issuer}", + ValidAudience = $"{config?.Auth?.audience}", + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes($"{config?.Auth?.accessTokenSecret}")) + }; + }); +} +else +{ + Log4net.WriteLine("Config Preload Load Error.", LogType.Error); + return; +} + var app = builder.Build(); +//read api config and set +string serverUrl = string.Empty; +var configService = app.Services.GetService>(); +bool isIIS = false; + +if (configService?.OpenConfig($@"{configDir}/{configFileName}") == true) +{ + Log4net.WriteLine("WebApi Config Success."); + var apiConfig = ConfigService.Config; + if (apiConfig != null) + { + serverUrl = $"{apiConfig?.Server?.Address}:{apiConfig?.Server?.Port}"; + isIIS = apiConfig!.Server.IIS; + } +} +else +{ + Log4net.WriteLine("WebApi Config Error."); + return; +} + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { + Log4net.WriteLine($"IsDevelopment:{app.Environment.IsDevelopment()}"); + Log4net.WriteLine($"Swagger Url: {serverUrl}/swagger"); app.UseSwagger(); app.UseSwaggerUI(); } +app.UseAuthentication(); app.UseHttpsRedirection(); - app.UseAuthorization(); app.MapControllers(); -app.Run(); +if (isIIS == true) +{ + app.Run(); +} +else +{ + Log4net.WriteLine($"Operation Url: {serverUrl}"); + app.Run($"{serverUrl}"); +} diff --git a/Projects/WebApi/AuthApi/Services/AuthService.cs b/Projects/WebApi/AuthApi/Services/AuthService.cs new file mode 100644 index 0000000..acced8d --- /dev/null +++ b/Projects/WebApi/AuthApi/Services/AuthService.cs @@ -0,0 +1,263 @@ +using SystemX.Core.Model.Auth; +using SystemX.Core.Services; +using SystemX.Core; +using WebApi.Library.Config; +using SystemX.Core.Config.Model; +using System.Data; +using SystemX.Core.DB; +using Microsoft.EntityFrameworkCore; +using SystemX.Core.DB.DBContext.AccountDB.Context; +using SystemX.Core.DB.DBContext.AccountDB.Tables; + +namespace AuthApi.Services +{ + public class AuthService + { + private readonly IServiceProvider _serviceProvider; + private readonly IServiceScopeFactory _scopeFactory; + private readonly ConfigService? _configService; + + private readonly DataBase? _accountDB; + + private static List Session = new List(); + + public AuthService(IServiceProvider serviceProvider, IServiceScopeFactory scopeFactory, ConfigService configSerice) + { + _serviceProvider = serviceProvider; + _configService = configSerice; + _scopeFactory = scopeFactory; + _accountDB = _configService?.GetConfig()?.DataBase?.Find(x => x.DBContext == "VpkiAccountDbContext"); + } + + /// + /// create new user + /// + public async Task CreateUser(RegisterModel registerModel) + { + //response + RegisterResponseModel response = new RegisterResponseModel(); + response.EC = ERROR_CODE.EC_USER_REGISTER_FAILED; + response.UserID = registerModel.UserID; + response.Role = registerModel.Role; + response.RoleName = registerModel.Role.ToString(); + + //context + using (var scope = _scopeFactory.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + if (context is not null) + { + var user = await context.TUsers.AsNoTracking().Where(x => x.CUserId.ToLower() == registerModel.UserID.ToLower()).ToListAsync(); + if (user?.Count <= 0) + { + string auid = Guid.NewGuid().ToString(); + //user + TUser newUser = new TUser + { + CAuid = auid, + CUserId = registerModel.UserID, + CPasswordHashed = registerModel.Password, + CCreateDateTime = DateTime.Now, + CLastLoginDateTime = new DateTime() + }; + //role + TRole newUserRole = new TRole + { + CAuid = auid, + CRoleId = Convert.ToByte(registerModel.Role), + CRoleName = registerModel.Role.ToString() + }; + + using (var transaction = await context.CreateTransactionAsync()) + { + await context.AddAsync(newUser); + await context.AddAsync(newUserRole); + + var result = await context.CloseTransactionAsync(transaction); + if (result == true) + { + response.EC = ERROR_CODE.EC_OK; + } + } + } + } + } + + return response; + } + + /// + /// select user(login) + /// + public async Task SelectUser(LoginModel loginModel) + { + //response + LoginResponseModel response = new LoginResponseModel(); + response.EC = ERROR_CODE.EC_USER_LOGIN_FAILED; + response.UserID = loginModel.UserID; + + //var session = Session.Find(x => x.UserID?.ToLower() == loginModel.UserID?.ToLower()); + //if (session?.AccessTokenExpired < DateTime.Now.ToUnixTime()) + //{ + // Session.Remove(session); + //} + + //기존 로그인 체크 + // if (Session.Exists(x => x.UserID == $"{loginModel.UserID?.ToLower()}") == false) + { + if (loginModel != null) + { + //context + using (var scope = _scopeFactory.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + if (context is not null) + { + try + { + using (var transaction = await context.CreateTransactionAsync(IsolationLevel.ReadUncommitted)) + { + //select user + var selectUser = await context.TUsers.AsNoTracking().FirstOrDefaultAsync(x => x.CUserId.ToLower() == loginModel!.UserID!.ToLower()); + if (selectUser is not null) + { + if (selectUser.CPasswordHashed == loginModel?.Password) + { + //select role + var selectRole = await context.TRoles.FindAsync(selectUser.CAuid); + if (selectRole != null) + { + response.Role = (UserRole)Enum.Parse(typeof(UserRole), selectRole.CRoleId.ToString()); + response.RoleName = selectRole.CRoleName; + } + + // Session.Add(response); + + if (selectUser.CState == (byte)UserState.Active) + { + response.EC = ERROR_CODE.EC_OK; + } + else if (selectUser.CState == (byte)UserState.Inactive) + { + response.EC = ERROR_CODE.EC_USER_LOGIN_INAVTIVE; + } + else if (selectUser.CState == (byte)UserState.Block) + { + response.EC = ERROR_CODE.EC_USER_LOGIN_BLOCKED; + } + } + else + { + response.EC = ERROR_CODE.EC_USER_LOGIN_INVALID_PASSWORD; + } + } + else + { + response.EC = ERROR_CODE.EC_USER_LOGIN_NOT_EXIST; + Log4net.WriteLine($"{response.EC}", LogType.Error); + } + await context.CloseTransactionAsync(transaction); + } + } + catch (Exception e) + { + Log4net.WriteLine($"Select User Transaction Error", LogType.Exception); + Log4net.WriteLine(e); + } + } + } + } + } + + return response; + } + + public async Task UpdateLoginInfo(LoginModel loginModel, string? RefreshToken = "") + { + bool result = false; + bool transactionResult = true; + + using (var scope = _scopeFactory.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + if (context is not null) + { + var selectUser = await context.TUsers.AsNoTracking().FirstOrDefaultAsync(x => x.CUserId.ToLower() == loginModel!.UserID!.ToLower()); + if (selectUser is not null) + { + using (var transaction = await context.CreateTransactionAsync()) + { + try + { + //user info + selectUser.CLastLoginDateTime = DateTime.Now; + context.Update(selectUser); + + //refresh token + var findRefreshToken = await context.TRefreshTokens.AsNoTracking().FirstOrDefaultAsync(x => x.CAuid == selectUser.CAuid); + //null이면(없으면) add + if (findRefreshToken == null) + { + await context.AddAsync(new TRefreshToken + { + CAuid = selectUser.CAuid, + CRefreshToken = $"{RefreshToken}" + }); + } + //있으면 update + else + { + findRefreshToken.CRefreshToken = $"{RefreshToken}"; + context.Update(findRefreshToken); + } + + //commit + Log4net.WriteLine(findRefreshToken?.ToJson(), LogType.Debug); + + result = true; + } + catch (Exception ex) + { + Log4net.WriteLine(ex); + } + + transactionResult = await context.CloseTransactionAsync(transaction); + } + } + else + { + Log4net.WriteLine($"Not Exist User {loginModel.UserID}", LogType.Error); + } + + //db error + if (transactionResult == false) + { + Log4net.WriteLine($"Transaction Error", LogType.Error); + } + else + { + Log4net.WriteLine($"Transaction Success", LogType.DB); + } + } + } + + return result; + } + + public LogoutResponseModel LogoutUser(LogoutModel logoutModel) + { + LogoutResponseModel response = new LogoutResponseModel(); + response.UserID = logoutModel.UserID; + response.EC = ERROR_CODE.EC_USER_LOGOUT_FAILED; + + var session = Session.Find(x => x.UserID?.ToLower() == logoutModel?.UserID?.ToLower()); + if (session != null) + { + Session.Remove(session); + response.EC = ERROR_CODE.EC_OK; + } + + return response; + } + } +} diff --git a/Projects/WebApi/AuthApi/WeatherForecast.cs b/Projects/WebApi/AuthApi/WeatherForecast.cs deleted file mode 100644 index 417734b..0000000 --- a/Projects/WebApi/AuthApi/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace AuthApi -{ - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -} diff --git a/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Context/AccountDbContext.cs b/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Context/AccountDbContext.cs new file mode 100644 index 0000000..a053ff5 --- /dev/null +++ b/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Context/AccountDbContext.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using WebApi.Library.DBContext.DB.DBContext.AccountDB.Tables; + +namespace WebApi.Library.DBContext.DB.DBContext.AccountDB.Context; + +public partial class AccountDbContext : DbContext +{ + public AccountDbContext() + { + } + + public AccountDbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet TRefreshTokens { get; set; } + + public virtual DbSet TRoles { get; set; } + + public virtual DbSet TUsers { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263. + => optionsBuilder.UseSqlServer("server=127.0.0.1; user id=SystemX; password=X; database=AccountDB; TrustServerCertificate=true;"); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.CAuid).HasName("PK__tRefresh__FBF0855465EB95AB"); + + entity.ToTable("tRefreshToken"); + + entity.Property(e => e.CAuid) + .HasMaxLength(250) + .HasColumnName("cAuid"); + entity.Property(e => e.CRefreshToken) + .HasMaxLength(1000) + .HasColumnName("cRefreshToken"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.CAuid).HasName("PK__tRole__FBF085540BB887D7"); + + entity.ToTable("tRole"); + + entity.Property(e => e.CAuid) + .HasMaxLength(250) + .HasColumnName("cAuid"); + entity.Property(e => e.CRoleId).HasColumnName("cRoleID"); + entity.Property(e => e.CRoleName) + .HasMaxLength(20) + .HasColumnName("cRoleName"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.CUserId).HasName("PK__tUser__A75DC19A721265FF"); + + entity.ToTable("tUser"); + + entity.Property(e => e.CUserId) + .HasMaxLength(50) + .HasColumnName("cUserID"); + entity.Property(e => e.CAuid) + .HasMaxLength(250) + .HasColumnName("cAuid"); + entity.Property(e => e.CCreateDateTime).HasColumnName("cCreateDateTime"); + entity.Property(e => e.CLastLoginDateTime).HasColumnName("cLastLoginDateTime"); + entity.Property(e => e.CPasswordHashed) + .HasMaxLength(250) + .HasColumnName("cPasswordHashed"); + entity.Property(e => e.CState).HasColumnName("cState"); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TRefreshToken.cs b/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TRefreshToken.cs new file mode 100644 index 0000000..d807df1 --- /dev/null +++ b/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TRefreshToken.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace WebApi.Library.DBContext.DB.DBContext.AccountDB.Tables; + +public partial class TRefreshToken +{ + public string CAuid { get; set; } = null!; + + public string CRefreshToken { get; set; } = null!; +} diff --git a/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TRole.cs b/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TRole.cs new file mode 100644 index 0000000..bc6fa15 --- /dev/null +++ b/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TRole.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace WebApi.Library.DBContext.DB.DBContext.AccountDB.Tables; + +public partial class TRole +{ + public string CAuid { get; set; } = null!; + + public byte CRoleId { get; set; } + + public string CRoleName { get; set; } = null!; +} diff --git a/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TUser.cs b/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TUser.cs new file mode 100644 index 0000000..4c4ec32 --- /dev/null +++ b/Projects/WebApi/WebApi.Library.DBContext/DB/DBContext/AccountDB/Tables/TUser.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace WebApi.Library.DBContext.DB.DBContext.AccountDB.Tables; + +public partial class TUser +{ + public string CUserId { get; set; } = null!; + + public string CAuid { get; set; } = null!; + + public string CPasswordHashed { get; set; } = null!; + + public byte CState { get; set; } + + public DateTime CCreateDateTime { get; set; } + + public DateTime? CLastLoginDateTime { get; set; } +} diff --git a/Projects/WebApi/WebApi.Library.DBContext/WebApi.Library.DBContext.csproj b/Projects/WebApi/WebApi.Library.DBContext/WebApi.Library.DBContext.csproj new file mode 100644 index 0000000..085266c --- /dev/null +++ b/Projects/WebApi/WebApi.Library.DBContext/WebApi.Library.DBContext.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Projects/WebApi/WebApi.Library/Config/WebApiConfig.cs b/Projects/WebApi/WebApi.Library/Config/WebApiConfig.cs new file mode 100644 index 0000000..51fb9fe --- /dev/null +++ b/Projects/WebApi/WebApi.Library/Config/WebApiConfig.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using SystemX.Core.Config; +using SystemX.Core.Config.Model; + +namespace WebApi.Library.Config +{ + public class WebApiConfig : WebCommonConfig + { + [JsonPropertyName("Auth")] + public Auth? Auth { get; set; } + + [JsonPropertyName("DataBase")] + public List? DataBase { get; set; } + } +} diff --git a/Projects/WebApi/WebApi.Library/WebApi.Library.csproj b/Projects/WebApi/WebApi.Library/WebApi.Library.csproj new file mode 100644 index 0000000..8f25caa --- /dev/null +++ b/Projects/WebApi/WebApi.Library/WebApi.Library.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + True + + + + True + + + + + ..\..\DLL\SystemX.Core.dll + + + + diff --git a/Projects/WebApi/WebApi.sln b/Projects/WebApi/WebApi.sln index e587684..9b91b76 100644 --- a/Projects/WebApi/WebApi.sln +++ b/Projects/WebApi/WebApi.sln @@ -5,6 +5,15 @@ VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthApi", "AuthApi\AuthApi.csproj", "{321DD194-9455-48F7-A0BE-EF6E95881714}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.Library", "WebApi.Library\WebApi.Library.csproj", "{1B109CFE-B860-4125-8F2B-06D95DE85E91}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.Library.DBContext", "WebApi.Library.DBContext\WebApi.Library.DBContext.csproj", "{92599205-8D5B-4630-B669-AA390193BC9E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{C8D5274F-AC00-46C7-1F8D-E88E81087A52}" + ProjectSection(SolutionItems) = preProject + ..\Config\WebApi.AuthApi.Config.json = ..\Config\WebApi.AuthApi.Config.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +24,14 @@ Global {321DD194-9455-48F7-A0BE-EF6E95881714}.Debug|Any CPU.Build.0 = Debug|Any CPU {321DD194-9455-48F7-A0BE-EF6E95881714}.Release|Any CPU.ActiveCfg = Release|Any CPU {321DD194-9455-48F7-A0BE-EF6E95881714}.Release|Any CPU.Build.0 = Release|Any CPU + {1B109CFE-B860-4125-8F2B-06D95DE85E91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B109CFE-B860-4125-8F2B-06D95DE85E91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B109CFE-B860-4125-8F2B-06D95DE85E91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B109CFE-B860-4125-8F2B-06D95DE85E91}.Release|Any CPU.Build.0 = Release|Any CPU + {92599205-8D5B-4630-B669-AA390193BC9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92599205-8D5B-4630-B669-AA390193BC9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92599205-8D5B-4630-B669-AA390193BC9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92599205-8D5B-4630-B669-AA390193BC9E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE