diff --git a/.gitignore b/.gitignore index d687fc7..39b6c75 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,8 @@ client/node_modules client/dist -.vs +.vs/ +.user # Build results [Dd]ebug/ diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/server/src/CPATapi.Server/CPATapi.Server.csproj b/server/src/CPATapi.Server/CPATapi.Server.csproj index 380696f..ca74a31 100644 --- a/server/src/CPATapi.Server/CPATapi.Server.csproj +++ b/server/src/CPATapi.Server/CPATapi.Server.csproj @@ -1,13 +1,18 @@ - + net10.0 enable + enable + a7b40068-a2f6-4c63-bbd9-0fd346908fb0 + Linux + ..\.. + diff --git a/server/src/CPATapi.Server/Controllers/AvailabilityController.cs b/server/src/CPATapi.Server/Controllers/AvailabilityController.cs index d32ae0f..0c9b2a9 100644 --- a/server/src/CPATapi.Server/Controllers/AvailabilityController.cs +++ b/server/src/CPATapi.Server/Controllers/AvailabilityController.cs @@ -1,40 +1,29 @@ +using CPATapi.Server.Interfaces; +using CPATapi.Server.Models; using Microsoft.AspNetCore.Mvc; using Dapper; using Microsoft.Data.SqlClient; -namespace CPATapi.Server.Controllers -{ - [Route("[controller]")] - [ApiController] - public class AvailabilityController(IConfiguration config) : ControllerBase - { - [HttpGet, Route("users")] - public async Task> GetUsers() - { - await using var con = new SqlConnection(config["Db:ConnectionStringZc"]); - await con.OpenAsync(); - return await con.QueryAsync(""" - SELECT DISTINCT MA_USER_NAME - FROM dbo.MA_DATEN - WHERE MA_USER_NAME IS NOT NULL AND MA_USER_AKTIV = 1 - ORDER BY MA_USER_NAME - """); - } +namespace CPATapi.Server.Controllers; - [HttpGet, Route("{user}")] - public async Task GetAvailability(string user) - { - await using var con = new SqlConnection(config["Db:ConnectionStringZc"]); - await con.OpenAsync(); - var stampCount = (await con.QueryAsync(""" - SELECT * - FROM dbo.BU - INNER JOIN dbo.MA_DATEN on MA_NR = BU_MA_NR - WHERE - MA_USER_NAME = @user AND - BU_BU >= @date AND BU_BU < @tomorrow - """, new { user, date = DateTime.Now.Date, tomorrow = DateTime.Now.AddDays(1).Date })).Count(); - return new { user, loggedIn = stampCount % 2 != 0 }; - } +[Route("[controller]")] +[ApiController] +public class AvailabilityController(IZeitConsensRepository zeitConsens) : ControllerBase +{ + [HttpGet] + [Route("users")] + [ProducesResponseType>(StatusCodes.Status200OK)] + public async Task GetUsers() + { + return Ok(await zeitConsens.GetUsersAsync()); + } + + [HttpGet] + [Route("{user}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetAvailability(string user) + { + var stampCount = (await zeitConsens.GetStampsAsync(user, DateTime.Now.Date, DateTime.Now.AddDays(1).Date)).Count(); + return Ok(new Availability { User = user, LoggedIn = stampCount % 2 != 0 }); } } diff --git a/server/src/CPATapi.Server/Controllers/CallerIdController.cs b/server/src/CPATapi.Server/Controllers/CallerIdController.cs index 24ad04e..9fefc20 100644 --- a/server/src/CPATapi.Server/Controllers/CallerIdController.cs +++ b/server/src/CPATapi.Server/Controllers/CallerIdController.cs @@ -1,37 +1,30 @@ +using CPATapi.Server.Interfaces; using CPATapi.Server.Models; -using Dapper; using Microsoft.AspNetCore.Mvc; -using Microsoft.Data.SqlClient; -namespace CPATapi.Server.Controllers +namespace CPATapi.Server.Controllers; + +[Route("[controller]")] +[ApiController] +public class CallerIdController(ITapiDirectoryRepository tapiDirectory) : ControllerBase { - [Route("[controller]")] - [ApiController] - public class CallerIdController(IConfiguration config) : ControllerBase + [HttpGet] + [Route("{number}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task CallerIdAsync([FromRoute] string number) { - [HttpGet, Route("{number}")] - public async Task CallerIdAsync([FromRoute] string number) + if (number.Length >= 5) { - if (number.Length >= 5) - { - var minMatch = number[^5..]; - number = "%" + minMatch; - } - - await using var con = new SqlConnection(config["Db:ConnectionString"]); - await con.OpenAsync(); - - var result = await con.QueryFirstOrDefaultAsync(""" - SELECT - TD_ID, - TD_NAME, - TD_NUMBER, - TD_NUMBER_TAPI, - TD_MEDIUM - FROM dbo.CP_TAPI_DIRECTORY - WHERE TD_NUMBER_TAPI LIKE @number - """, new {number}); - return result; + var minMatch = number[^5..]; + number = "%" + minMatch; } + + var result = await tapiDirectory.SearchByNumberAsync(number); + if (result != null) + { + return Ok(result); + } + return NotFound(); } } diff --git a/server/src/CPATapi.Server/Controllers/ContactController.cs b/server/src/CPATapi.Server/Controllers/ContactController.cs index 79012f3..7bb9ae2 100644 --- a/server/src/CPATapi.Server/Controllers/ContactController.cs +++ b/server/src/CPATapi.Server/Controllers/ContactController.cs @@ -1,33 +1,21 @@ +using CPATapi.Server.Interfaces; using CPATapi.Server.Models; -using Dapper; using Microsoft.AspNetCore.Mvc; -using Microsoft.Data.SqlClient; -namespace CPATapi.Server.Controllers +namespace CPATapi.Server.Controllers; + +[ApiController] +[Route("[controller]")] +public class ContactController(ILogger logger, ITapiDirectoryRepository tapiDirectory) : ControllerBase { - [ApiController] - [Route("[controller]")] - public class ContactController(ILogger logger, IConfiguration config) : ControllerBase + + private readonly ILogger _logger = logger; + + [HttpGet] + [Produces("application/json")] + [ProducesResponseType>(StatusCodes.Status200OK)] + public async Task GetAsync() { - - private readonly ILogger _logger = logger; - - [HttpGet] - public async Task> GetAsync() - { - await using var con = new SqlConnection(config["Db:ConnectionString"]); - await con.OpenAsync(); - - var contacts = await con.QueryAsync(""" - SELECT - TD_ID, - TD_NAME, - TD_NUMBER, - TD_NUMBER_TAPI, - TD_MEDIUM - FROM dbo.CP_TAPI_DIRECTORY - """); - return contacts; - } + return Ok(await tapiDirectory.GetAllAsync()); } } diff --git a/server/src/CPATapi.Server/Controllers/SearchController.cs b/server/src/CPATapi.Server/Controllers/SearchController.cs index 9dcd629..cd1c6d6 100644 --- a/server/src/CPATapi.Server/Controllers/SearchController.cs +++ b/server/src/CPATapi.Server/Controllers/SearchController.cs @@ -1,58 +1,25 @@ -using System.Text; using System.Text.RegularExpressions; +using CPATapi.Server.Interfaces; using CPATapi.Server.Models; -using Dapper; using Microsoft.AspNetCore.Mvc; -using Microsoft.Data.SqlClient; -namespace CPATapi.Server.Controllers +namespace CPATapi.Server.Controllers; + +[Route("[controller]")] +[ApiController] +public class SearchController(ITapiDirectoryRepository tapiDirectory) : ControllerBase { - [Route("[controller]")] - [ApiController] - public class SearchController(IConfiguration config) : ControllerBase + [HttpGet] + [ProducesResponseType>(StatusCodes.Status200OK)] + public async Task SearchAsync([FromQuery] string query) { - [HttpGet] - public async Task> SearchAsync([FromQuery] string query) + if (query == null) { - if (query == null) - { - return new List(); - } - - var args = Regex.Split(query, "\\s").Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()).ToArray(); - - if (args.Length == 0) - { - return new List(); - } - - await using var con = new SqlConnection(config["Db:ConnectionString"]); - await con.OpenAsync(); - var sql = new StringBuilder(""" - SELECT TOP 10 - TD_ID, - TD_NAME, - TD_NUMBER, - TD_NUMBER_TAPI, - TD_MEDIUM - FROM dbo.CP_TAPI_DIRECTORY - WHERE - """); - var first = true; - var dp = new DynamicParameters(); - for (var i = 1; i <= args.Length; i++) - { - if (!first) - { - sql.AppendLine("AND"); - } - first = false; - sql.AppendLine($"(TD_NAME LIKE @s{i} OR"); - sql.AppendLine($" TD_NUMBER_TAPI LIKE @s{i})"); - dp.Add($"s{i}", $"%{args[i-1]}%"); - } - - return await con.QueryAsync(sql.ToString(), dp); + return Ok(new List()); } + + var args = Regex.Split(query, "\\s").Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()).ToArray(); + + return Ok(await tapiDirectory.SearchAsync(args)); } } diff --git a/server/src/CPATapi.Server/Dockerfile b/server/src/CPATapi.Server/Dockerfile new file mode 100644 index 0000000..f6ba016 --- /dev/null +++ b/server/src/CPATapi.Server/Dockerfile @@ -0,0 +1,28 @@ +# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +# This stage is used when running from VS in fast mode (Default for Debug configuration) +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 + +# This stage is used to build the service project +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["CPATapi.Server/CPATapi.Server.csproj", "CPATapi.Server/"] +RUN dotnet restore "CPATapi.Server/CPATapi.Server.csproj" +COPY . . +WORKDIR "/src/CPATapi.Server" +RUN dotnet build "./CPATapi.Server.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./CPATapi.Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "CPATapi.Server.dll"] diff --git a/server/src/CPATapi.Server/Interfaces/IRepository.cs b/server/src/CPATapi.Server/Interfaces/IRepository.cs new file mode 100644 index 0000000..3a5f10d --- /dev/null +++ b/server/src/CPATapi.Server/Interfaces/IRepository.cs @@ -0,0 +1,5 @@ +namespace CPATapi.Server.Interfaces; + +public interface IRepository +{ +} diff --git a/server/src/CPATapi.Server/Interfaces/ITapiDirectoryRepository.cs b/server/src/CPATapi.Server/Interfaces/ITapiDirectoryRepository.cs new file mode 100644 index 0000000..b7ccdc2 --- /dev/null +++ b/server/src/CPATapi.Server/Interfaces/ITapiDirectoryRepository.cs @@ -0,0 +1,10 @@ +using CPATapi.Server.Models; + +namespace CPATapi.Server.Interfaces; + +public interface ITapiDirectoryRepository : IRepository +{ + Task> SearchAsync(string[] args); + Task SearchByNumberAsync(string number); + Task> GetAllAsync(); +} diff --git a/server/src/CPATapi.Server/Interfaces/IZeitConsensRepository.cs b/server/src/CPATapi.Server/Interfaces/IZeitConsensRepository.cs new file mode 100644 index 0000000..3a0024c --- /dev/null +++ b/server/src/CPATapi.Server/Interfaces/IZeitConsensRepository.cs @@ -0,0 +1,9 @@ +using CPATapi.Server.Models; + +namespace CPATapi.Server.Interfaces; + +public interface IZeitConsensRepository : IRepository +{ + Task> GetUsersAsync(); + Task> GetStampsAsync(string user, DateTime from, DateTime to); +} diff --git a/server/src/CPATapi.Server/Models/Availability.cs b/server/src/CPATapi.Server/Models/Availability.cs new file mode 100644 index 0000000..24d2e19 --- /dev/null +++ b/server/src/CPATapi.Server/Models/Availability.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace CPATapi.Server.Models; + +public class Availability +{ + [JsonPropertyName("user")] + public string User { get; set; } + [JsonPropertyName("loggedIn")] + public bool LoggedIn { get; set; } +} diff --git a/server/src/CPATapi.Server/Models/Stamp.cs b/server/src/CPATapi.Server/Models/Stamp.cs new file mode 100644 index 0000000..5ad9a15 --- /dev/null +++ b/server/src/CPATapi.Server/Models/Stamp.cs @@ -0,0 +1,7 @@ +namespace CPATapi.Server.Models; + +public class Stamp +{ + public int MA_NR { get; set; } + public DateTime BU_BU { get; set; } +} diff --git a/server/src/CPATapi.Server/Models/TapiContact.cs b/server/src/CPATapi.Server/Models/TapiContact.cs index 94bb0cc..50dd0c3 100644 --- a/server/src/CPATapi.Server/Models/TapiContact.cs +++ b/server/src/CPATapi.Server/Models/TapiContact.cs @@ -1,11 +1,10 @@ -namespace CPATapi.Server.Models +namespace CPATapi.Server.Models; + +public class TapiContact { - public class TapiContact - { - public string TD_ID { get; set; } - public string TD_NAME { get; set; } - public string TD_NUMBER { get; set; } - public string TD_NUMBER_TAPI { get; set; } - public string TD_MEDIUM { get; set; } - } + public string TD_ID { get; set; } + public string TD_NAME { get; set; } + public string TD_NUMBER { get; set; } + public string TD_NUMBER_TAPI { get; set; } + public string TD_MEDIUM { get; set; } } \ No newline at end of file diff --git a/server/src/CPATapi.Server/Program.cs b/server/src/CPATapi.Server/Program.cs index ceb09d2..16a1f63 100644 --- a/server/src/CPATapi.Server/Program.cs +++ b/server/src/CPATapi.Server/Program.cs @@ -1,5 +1,11 @@ +using CPATapi.Server.Interfaces; +using CPATapi.Server.Repository; + var builder = WebApplication.CreateBuilder(args); +builder.Services.AddTransient(); +builder.Services.AddTransient(); + builder.Services.AddControllers(); var app = builder.Build(); @@ -13,4 +19,4 @@ app.UseAuthorization(); app.MapControllers(); -app.Run(); +await app.RunAsync(); diff --git a/server/src/CPATapi.Server/Properties/launchSettings.json b/server/src/CPATapi.Server/Properties/launchSettings.json index 37cfe29..3c16350 100644 --- a/server/src/CPATapi.Server/Properties/launchSettings.json +++ b/server/src/CPATapi.Server/Properties/launchSettings.json @@ -1,13 +1,4 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:62406", - "sslPort": 0 - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "IIS Express": { "commandName": "IISExpress", @@ -20,11 +11,30 @@ "CPATapiServer": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "weatherforecast", + "launchUrl": "availability/users", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" + "applicationUrl": "https://3cxtapi.cp-austria.at:5001;https://localhost:5001;http://localhost:5000" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/contact", + "environmentVariables": { + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": false + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:62406", + "sslPort": 0 } } } \ No newline at end of file diff --git a/server/src/CPATapi.Server/Repository/Repository.cs b/server/src/CPATapi.Server/Repository/Repository.cs new file mode 100644 index 0000000..130d816 --- /dev/null +++ b/server/src/CPATapi.Server/Repository/Repository.cs @@ -0,0 +1,21 @@ +using System.Data.Common; +using CPATapi.Server.Interfaces; +using Microsoft.Data.SqlClient; + +namespace CPATapi.Server.Repository; + +public class Repository: IRepository +{ + private readonly IConfiguration _config; + + protected Repository(IConfiguration config) + { + _config = config; + } + protected async Task OpenAsync(string name) + { + var db = new SqlConnection(_config.GetConnectionString(name)); + await db.OpenAsync(); + return db; + } +} diff --git a/server/src/CPATapi.Server/Repository/TapiDirectoryRepository.cs b/server/src/CPATapi.Server/Repository/TapiDirectoryRepository.cs new file mode 100644 index 0000000..dc67a7e --- /dev/null +++ b/server/src/CPATapi.Server/Repository/TapiDirectoryRepository.cs @@ -0,0 +1,71 @@ +using System.Text; +using CPATapi.Server.Interfaces; +using CPATapi.Server.Models; +using Dapper; + +namespace CPATapi.Server.Repository; + +internal class TapiDirectoryRepository(IConfiguration config) : Repository(config), ITapiDirectoryRepository +{ + public async Task> SearchAsync(string[] args) + { + await using var con = await OpenAsync("Tapi"); + var sql = new StringBuilder(""" + SELECT TOP 10 + TD_ID, + TD_NAME, + TD_NUMBER, + TD_NUMBER_TAPI, + TD_MEDIUM + FROM dbo.CP_TAPI_DIRECTORY + WHERE + """); + var first = true; + var dp = new DynamicParameters(); + for (var i = 1; i <= args.Length; i++) + { + if (!first) + { + sql.AppendLine("AND"); + } + first = false; + sql.AppendLine($"(TD_NAME LIKE @s{i} OR"); + sql.AppendLine($" TD_NUMBER_TAPI LIKE @s{i})"); + dp.Add($"s{i}", $"%{args[i-1]}%"); + } + + return await con.QueryAsync(sql.ToString(), dp); + } + + public async Task SearchByNumberAsync(string number) + { + await using var con = await OpenAsync("Tapi"); + + var result = await con.QueryFirstOrDefaultAsync(""" + SELECT + TD_ID + ,TD_NAME + ,TD_NUMBER + ,TD_NUMBER_TAPI + ,TD_MEDIUM + FROM dbo.CP_TAPI_DIRECTORY + WHERE TD_NUMBER_TAPI LIKE @number + """, new {number}); + return result; + } + + public async Task> GetAllAsync() + { + await using var con = await OpenAsync("Tapi"); + + return await con.QueryAsync(""" + SELECT + TD_ID + ,TD_NAME + ,TD_NUMBER + ,TD_NUMBER_TAPI + ,TD_MEDIUM + FROM dbo.CP_TAPI_DIRECTORY + """); + } +} diff --git a/server/src/CPATapi.Server/Repository/ZeitConsensRepository.cs b/server/src/CPATapi.Server/Repository/ZeitConsensRepository.cs new file mode 100644 index 0000000..28344a4 --- /dev/null +++ b/server/src/CPATapi.Server/Repository/ZeitConsensRepository.cs @@ -0,0 +1,34 @@ +using CPATapi.Server.Interfaces; +using CPATapi.Server.Models; +using Dapper; + +namespace CPATapi.Server.Repository; + +internal class ZeitConsensRepository(IConfiguration config) : Repository(config), IZeitConsensRepository +{ + public async Task> GetUsersAsync() + { + await using var con = await OpenAsync("ZeitConsens"); + return await con.QueryAsync(""" + SELECT DISTINCT MA_USER_NAME + FROM dbo.MA_DATEN + WHERE MA_USER_NAME IS NOT NULL AND MA_USER_AKTIV = 1 + ORDER BY MA_USER_NAME + """); + } + + public async Task> GetStampsAsync(string user, DateTime from, DateTime to) + { + await using var con = await OpenAsync("ZeitConsens"); + return await con.QueryAsync(""" + SELECT + MA_NR + ,BU_BU + FROM dbo.BU + INNER JOIN dbo.MA_DATEN on MA_NR = BU_MA_NR + WHERE + MA_USER_NAME = @user AND + BU_BU >= @from AND BU_BU < @to + """, new { user, from, to }); + } +} diff --git a/server/src/CPATapi.Server/appsettings.json b/server/src/CPATapi.Server/appsettings.json index 7caaae0..e82ee5a 100644 --- a/server/src/CPATapi.Server/appsettings.json +++ b/server/src/CPATapi.Server/appsettings.json @@ -6,9 +6,9 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "Db": { - "ConnectionString": "", - "ConnectionStringZc": "" + "ConnectionStrings": { + "Tapi": "", + "ZeitConsens": "" }, "AllowedHosts": "*" } diff --git a/server/src/CPATapi.Server/publish.bat b/server/src/CPATapi.Server/publish.bat new file mode 100644 index 0000000..4d70f36 --- /dev/null +++ b/server/src/CPATapi.Server/publish.bat @@ -0,0 +1,19 @@ +@echo off +setlocal +cd /d %~dp0 + +docker build -t source.cp-austria.at/cpatrd/3cx_tapi:latest -f Dockerfile .. +if errorlevel 1 ( + echo. + echo ERROR: Docker build failed! + exit /b 1 +) + + +SET /P AREYOUSURE=Publish to source.cp-austria.at? (Y/[N]) +IF /I "%AREYOUSURE%" NEQ "Y" GOTO END + +docker push source.cp-austria.at/cpatrd/3cx_tapi:latest + +:END +endlocal