Modernize and Dockerize server

This commit is contained in:
2026-03-19 13:35:53 +01:00
parent 76a2bf0e88
commit 766ed86999
21 changed files with 364 additions and 161 deletions

3
.gitignore vendored
View File

@@ -4,7 +4,8 @@
client/node_modules
client/dist
.vs
.vs/
.user
# Build results
[Dd]ebug/

30
server/.dockerignore Normal file
View File

@@ -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/**

View File

@@ -1,13 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>a7b40068-a2f6-4c63-bbd9-0fd346908fb0</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.72" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
</ItemGroup>

View File

@@ -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<IEnumerable<string>> GetUsers()
{
await using var con = new SqlConnection(config["Db:ConnectionStringZc"]);
await con.OpenAsync();
return await con.QueryAsync<string>("""
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<object> 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<IEnumerable<string>>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetUsers()
{
return Ok(await zeitConsens.GetUsersAsync());
}
[HttpGet]
[Route("{user}")]
[ProducesResponseType<Availability>(StatusCodes.Status200OK)]
public async Task<IActionResult> 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 });
}
}

View File

@@ -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<TapiContact>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> CallerIdAsync([FromRoute] string number)
{
[HttpGet, Route("{number}")]
public async Task<TapiContact> 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<TapiContact>("""
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();
}
}

View File

@@ -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<ContactController> logger, ITapiDirectoryRepository tapiDirectory) : ControllerBase
{
[ApiController]
[Route("[controller]")]
public class ContactController(ILogger<ContactController> logger, IConfiguration config) : ControllerBase
private readonly ILogger<ContactController> _logger = logger;
[HttpGet]
[Produces("application/json")]
[ProducesResponseType<IEnumerable<TapiContact>>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetAsync()
{
private readonly ILogger<ContactController> _logger = logger;
[HttpGet]
public async Task<IEnumerable<TapiContact>> GetAsync()
{
await using var con = new SqlConnection(config["Db:ConnectionString"]);
await con.OpenAsync();
var contacts = await con.QueryAsync<TapiContact>("""
SELECT
TD_ID,
TD_NAME,
TD_NUMBER,
TD_NUMBER_TAPI,
TD_MEDIUM
FROM dbo.CP_TAPI_DIRECTORY
""");
return contacts;
}
return Ok(await tapiDirectory.GetAllAsync());
}
}

View File

@@ -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<IEnumerable<TapiContact>>(StatusCodes.Status200OK)]
public async Task<IActionResult> SearchAsync([FromQuery] string query)
{
[HttpGet]
public async Task<IEnumerable<TapiContact>> SearchAsync([FromQuery] string query)
if (query == null)
{
if (query == null)
{
return new List<TapiContact>();
}
var args = Regex.Split(query, "\\s").Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()).ToArray();
if (args.Length == 0)
{
return new List<TapiContact>();
}
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<TapiContact>(sql.ToString(), dp);
return Ok(new List<TapiContact>());
}
var args = Regex.Split(query, "\\s").Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()).ToArray();
return Ok(await tapiDirectory.SearchAsync(args));
}
}

View File

@@ -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"]

View File

@@ -0,0 +1,5 @@
namespace CPATapi.Server.Interfaces;
public interface IRepository
{
}

View File

@@ -0,0 +1,10 @@
using CPATapi.Server.Models;
namespace CPATapi.Server.Interfaces;
public interface ITapiDirectoryRepository : IRepository
{
Task<IEnumerable<TapiContact>> SearchAsync(string[] args);
Task<TapiContact?> SearchByNumberAsync(string number);
Task<IEnumerable<TapiContact>> GetAllAsync();
}

View File

@@ -0,0 +1,9 @@
using CPATapi.Server.Models;
namespace CPATapi.Server.Interfaces;
public interface IZeitConsensRepository : IRepository
{
Task<IEnumerable<string>> GetUsersAsync();
Task<IEnumerable<Stamp>> GetStampsAsync(string user, DateTime from, DateTime to);
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,7 @@
namespace CPATapi.Server.Models;
public class Stamp
{
public int MA_NR { get; set; }
public DateTime BU_BU { get; set; }
}

View File

@@ -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; }
}

View File

@@ -1,5 +1,11 @@
using CPATapi.Server.Interfaces;
using CPATapi.Server.Repository;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<ITapiDirectoryRepository, TapiDirectoryRepository>();
builder.Services.AddTransient<IZeitConsensRepository, ZeitConsensRepository>();
builder.Services.AddControllers();
var app = builder.Build();
@@ -13,4 +19,4 @@ app.UseAuthorization();
app.MapControllers();
app.Run();
await app.RunAsync();

View File

@@ -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
}
}
}

View File

@@ -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<DbConnection> OpenAsync(string name)
{
var db = new SqlConnection(_config.GetConnectionString(name));
await db.OpenAsync();
return db;
}
}

View File

@@ -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<IEnumerable<TapiContact>> 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<TapiContact>(sql.ToString(), dp);
}
public async Task<TapiContact?> SearchByNumberAsync(string number)
{
await using var con = await OpenAsync("Tapi");
var result = await con.QueryFirstOrDefaultAsync<TapiContact>("""
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<IEnumerable<TapiContact>> GetAllAsync()
{
await using var con = await OpenAsync("Tapi");
return await con.QueryAsync<TapiContact>("""
SELECT
TD_ID
,TD_NAME
,TD_NUMBER
,TD_NUMBER_TAPI
,TD_MEDIUM
FROM dbo.CP_TAPI_DIRECTORY
""");
}
}

View File

@@ -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<IEnumerable<string>> GetUsersAsync()
{
await using var con = await OpenAsync("ZeitConsens");
return await con.QueryAsync<string>("""
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<IEnumerable<Stamp>> GetStampsAsync(string user, DateTime from, DateTime to)
{
await using var con = await OpenAsync("ZeitConsens");
return await con.QueryAsync<Stamp>("""
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 });
}
}

View File

@@ -6,9 +6,9 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Db": {
"ConnectionString": "",
"ConnectionStringZc": ""
"ConnectionStrings": {
"Tapi": "",
"ZeitConsens": ""
},
"AllowedHosts": "*"
}

View File

@@ -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