Merge commit

This commit is contained in:
2024-06-25 13:28:03 +02:00
38 changed files with 505 additions and 422 deletions

View File

@@ -6,6 +6,10 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\Model.Designer.vb" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="1.0.0" />
@@ -41,6 +45,24 @@
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Model.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Model.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Model.en.resx">
<CustomToolNamespace>My.Resources</CustomToolNamespace>
<LastGenOutput>Model.en.Designer.vb</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Model.resx">
<CustomToolNamespace>My.Resources</CustomToolNamespace>
<LastGenOutput>Model.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.de-DE.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>

View File

@@ -0,0 +1,3 @@
Public Class Model
End Class

View File

@@ -100,19 +100,6 @@ namespace EnvelopeGenerator.Domain.Entities
[NotMapped]
public bool IsAlreadySent => Status > (int) Constants.EnvelopeStatus.EnvelopeSaved;
[NotMapped]
public string? StatusTranslated => Model.ResourceManager.GetString(Status.ToString());
[NotMapped]
public string? ContractTypeTranslated
{
get
{
string? oContractType = ContractType.ToString();
return oContractType is null ? default : Model.ResourceManager.GetString(oContractType);
}
}
public IEnumerable<EnvelopeDocument>? Documents { get; set; }
public IEnumerable<EnvelopeHistory>? History { get; set; }

View File

@@ -179,6 +179,7 @@
</EmbeddedResource>
<EmbeddedResource Include="frmEnvelopeEditor.resx">
<DependentUpon>frmEnvelopeEditor.vb</DependentUpon>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="frmEnvelopeMainData.en.resx">
<DependentUpon>frmEnvelopeMainData.vb</DependentUpon>

View File

@@ -45,8 +45,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
"maximumWarning": "1.5mb",
"maximumError": "2mb"
},
{
"type": "anyComponentStyle",
@@ -114,4 +114,4 @@
}
}
}
}
}

View File

@@ -2,7 +2,7 @@ import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { NavbarComponent } from "./components/navbar/navbar.component";
import { LoginComponent } from "./components/login/login.component";
import { HomeComponent } from "./components/home/home.component";
import { HomeComponent } from "./pages/home/home.component";
@Component({
selector: 'app-root',

View File

@@ -6,7 +6,8 @@ import { provideAnimationsAsync } from '@angular/platform-browser/animations/asy
import { APP_BASE_HREF } from '@angular/common';
import { UrlService } from './services/url.service';
import { API_URL } from './tokens/index'
import { provideHttpClient, withFetch } from '@angular/common/http';
import { HTTP_INTERCEPTORS, provideHttpClient, withFetch } from '@angular/common/http';
import { HttpRequestInterceptor } from './http.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
@@ -23,6 +24,11 @@ export const appConfig: ApplicationConfig = {
provide: API_URL,
useFactory: (urlService: UrlService) => urlService.getApiUrl(),
deps: [UrlService]
},
{
provide: HTTP_INTERCEPTORS,
useClass: HttpRequestInterceptor,
multi: true
}
]
};

View File

@@ -1,10 +1,10 @@
import { Routes } from '@angular/router';
import { EnvelopeTableComponent } from '../app/components/envelope-table/envelope-table.component'
import { HomeComponent } from '../app/components/home/home.component'
import { HomeComponent } from '../app/pages/home/home.component'
import { authGuard } from './guards/auth.guard'
import { EnvelopeComponent } from './pages/envelope/envelope.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: HomeComponent },
{ path: 'envelope', component: EnvelopeTableComponent, canActivate: [authGuard] }
{ path: 'envelope', component: EnvelopeComponent, canActivate: [authGuard] }
];

View File

@@ -5,8 +5,9 @@
[paging]="paging"
[sorting]="sorting"
[searching]="searching"
[summaries]="summaries"
[infoPanel]="infoPanel"
[localization]="localization"
[theme]="theme"
[autoResizeWidth] = "true"
>
</gui-grid>

View File

@@ -1,7 +1,6 @@
import { Component } from '@angular/core';
import { EnvelopeReceiverService } from '../../services/envelope-receiver.service';
import { NgModule } from '@angular/core';
import { GuiColumn, GuiColumnMenu, GuiGridModule, GuiInfoPanel, GuiLocalization, GuiPaging, GuiPagingDisplay, GuiSearching, GuiSorting, GuiSummaries } from '@generic-ui/ngx-grid';
import { GuiColumn, GuiColumnMenu, GuiGridModule, GuiInfoPanel, GuiLocalization, GuiPaging, GuiPagingDisplay, GuiSearching, GuiSorting, GuiSummaries, GuiTheme } from '@generic-ui/ngx-grid';
@Component({
selector: 'app-envelope-table',
@@ -23,48 +22,43 @@ export class EnvelopeTableComponent {
multiSorting: true
};
paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [10, 25, 50],
pagerTop: true,
pagerBottom: true,
display: GuiPagingDisplay.ADVANCED
};
paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [10, 25, 50],
pagerTop: true,
pagerBottom: true,
display: GuiPagingDisplay.ADVANCED
};
searching: GuiSearching = {
enabled: true
};
searching: GuiSearching = {
enabled: true
};
summaries: GuiSummaries = {
enabled: true
};
infoPanel: GuiInfoPanel = {
enabled:true,
infoDialog:false,
columnsManager:false,
schemaManager: true
};
infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true
};
localization: GuiLocalization = {
translationResolver: (key: string, value: string) => {
return '[de-DE]';
}
};
localization: GuiLocalization = {
translationResolver: (key: string, value: string) => EnvelopeTableComponent.Translation[key] ?? value
};
theme: GuiTheme = GuiTheme.FABRIC;
source: Array<any> = []
constructor(private erService: EnvelopeReceiverService) {
}
constructor(private erService: EnvelopeReceiverService) { }
ngOnInit() {
this.erService.getEnvelopeReceiver().subscribe({
next: res => this.source = res,
error: console.error
});
async ngOnInit() {
this.source = await this.erService.getEnvelopeReceiver();
}
columns: Array<GuiColumn> = [
@@ -88,4 +82,51 @@ export class EnvelopeTableComponent {
header: 'AddedWhen',
field: 'addedWhen'
}];
static readonly Translation: { [key: string]: string } = {
"sourceEmpty": "There are no items to show.",
"pagingItemsPerPage": "Items per page:",
"pagingOf": "of",
"pagingNextPage": "Next",
"pagingPrevPage": "Prev",
"pagingNoItems": "There is no items.",
"infoPanelShowing": "Showing",
"infoPanelItems": "items",
"infoPanelOutOf": "out of",
"infoPanelThemeMangerTooltipText": "Theme manager",
"infoPanelColumnManagerTooltipText": "Column manager",
"infoPanelInfoTooltipText": "info",
"themeManagerModalTitle": "Theme manager",
"themeManagerModalTheme": "Theme:",
"themeManagerModalRowColoring": "Row coloring:",
"themeManagerModalVerticalGrid": "Vertical grid",
"themeManagerModalHorizontalGrid": "HorizontalGrid",
"columnManagerModalTitle": "Manage columns",
"headerMenuMainTab": "Menu",
"headerMenuMainTabColumnSort": "Column sort",
"headerMenuMainTabHideColumn": "Hide column",
"headerMenuMainTabHighlightColumn": "Highlight",
"headerMenuMainTabMoveLeft": "Move left",
"headerMenuMainTabMoveRight": "Move right",
"headerMenuMainTabColumnSortAscending": "Ascending",
"headerMenuMainTabColumnSortDescending": "Descending",
"headerMenuMainTabColumnSortNone": "None",
"headerMenuFilterTab": "Filter",
"headerMenuColumnsTab": "Columns",
"summariesCount": "Count",
"summariesDist": "Dist",
"summariesSum": "Sum",
"summariesAvg": "Avg",
"summariesMin": "Min",
"summariesMax": "Max",
"summariesMed": "Med",
"summariesTruthy": "Truthy",
"summariesFalsy": "Falsy",
"summariesDistinctValuesTooltip": "Distinct values",
"summariesAverageTooltip": "Average",
"summariesMinTooltip": "Min",
"summariesMaxTooltip": "Max",
"summariesMedTooltip": "Median",
"summariesCountTooltip": "Number of items in the grid"
}
}

View File

@@ -3,15 +3,15 @@ import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.isAuthenticated().pipe(
map(isAuthenticated => {
if (!isAuthenticated) {
router.navigate(['/']);
router.navigate(['/login']);
}
return isAuthenticated;
})

View File

@@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { HttpInterceptorFn } from '@angular/common/http';
import { httpInterceptor } from './http.interceptor';
describe('httpInterceptor', () => {
const interceptor: HttpInterceptorFn = (req, next) =>
TestBed.runInInjectionContext(() => httpInterceptor(req, next));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(interceptor).toBeTruthy();
});
});

View File

@@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class HttpRequestInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const secureReq = req.clone({
withCredentials: true,
setHeaders: {
'X-Insecure-Request': 'true',
'Content-Type': 'application/json',
...req.headers.keys().reduce((headers, key) => {
headers[key] = req.headers.get(key) || '';
return headers;
}, {} as { [name: string]: string })
}
});
return next.handle(secureReq);
}
}

View File

@@ -1,6 +0,0 @@
export interface EnvelopeReceiver {
name: string | null
privateMessage: string | null
addedWhen: Date
changedWhen: Date | null
}

View File

@@ -0,0 +1,10 @@
<div id="table">
<mat-tab-group>
<mat-tab label="Offene Umschläge">
<app-envelope-table></app-envelope-table>
</mat-tab>
<mat-tab label="Abgeschlossene Umschläge">
<app-envelope-table></app-envelope-table>
</mat-tab>
</mat-tab-group>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EnvelopeComponent } from './envelope.component';
describe('EnvelopeComponent', () => {
let component: EnvelopeComponent;
let fixture: ComponentFixture<EnvelopeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EnvelopeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(EnvelopeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import { EnvelopeTableComponent } from "../../components/envelope-table/envelope-table.component";
import { MatTabsModule } from '@angular/material/tabs';
import { LocalizationService } from '../../services/localization.service';
import { firstValueFrom } from 'rxjs/internal/firstValueFrom';
@Component({
selector: 'app-envelope',
standalone: true,
templateUrl: './envelope.component.html',
styleUrl: './envelope.component.scss',
imports: [EnvelopeTableComponent, MatTabsModule]
})
export class EnvelopeComponent {
private localizer: any = {};
constructor(private localizationService: LocalizationService) {
}
async ngOnInit() {
this.localizer = await this.localizationService.getLocalizer()
}
}

View File

@@ -1,5 +1,5 @@
import { Component } from '@angular/core';
import { LoginComponent } from "../login/login.component";
import { LoginComponent } from "../../components/login/login.component";
@Component({
selector: 'app-home',

View File

@@ -1,6 +1,6 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, catchError } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Observable, firstValueFrom } from 'rxjs';
import { API_URL } from '../tokens/index';
@Injectable({
@@ -9,16 +9,12 @@ import { API_URL } from '../tokens/index';
export class EnvelopeReceiverService {
private url: string;
constructor(private http: HttpClient) {
constructor(private http: HttpClient) {
const api_url = inject(API_URL);
this.url = `${api_url}/envelopereceiver`;
}
getEnvelopeReceiver(): Observable<any> {
const headers = new HttpHeaders({
'Content-Type': 'application/json',
});
return this.http.get<any>(this.url, { withCredentials: true , headers });
getEnvelopeReceiver(): Promise<any> {
return firstValueFrom(this.http.get<any>(this.url));
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { LocalizationService } from './localization.service';
describe('LocalizationService', () => {
let service: LocalizationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(LocalizationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,21 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, firstValueFrom } from 'rxjs';
import { API_URL } from '../tokens/index';
@Injectable({
providedIn: 'root'
})
export class LocalizationService {
private url: string;
constructor(private http: HttpClient) {
const api_url = inject(API_URL);
this.url = `${api_url}/localization`;
}
getLocalizer(): Promise<any> {
return firstValueFrom(this.http.get<any>(this.url));
}
}

View File

@@ -64,6 +64,7 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers
{
IsPersistent = true,
AllowRefresh = true,
ExpiresUtc = DateTime.Now.AddMinutes(180)
};
// Sign in
@@ -72,8 +73,6 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers
new ClaimsPrincipal(claimsIdentity),
authProperties);
_dirSearchService.SetSearchRootCache(user.Username, login.Password);
return Ok();
}
catch(Exception ex)

View File

@@ -0,0 +1,91 @@
using DigitalData.Core.API;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Common;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Localization;
namespace EnvelopeGenerator.GeneratorAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LocalizationController : ControllerBase
{
private static readonly Guid L_KEY = Guid.NewGuid();
private readonly ILogger<LocalizationController> _logger;
private readonly IStringLocalizer<Model> _mLocalizer;
private readonly IStringLocalizer<Resource> _localizer;
private readonly IMemoryCache _cache;
public LocalizationController(
ILogger<LocalizationController> logger,
IStringLocalizer<Resource> localizer,
IMemoryCache memoryCache,
IStringLocalizer<Model> _modelLocalizer)
{
_logger = logger;
_localizer = localizer;
_cache = memoryCache;
_mLocalizer = _modelLocalizer;
}
[HttpGet]
public IActionResult GetAll() => Ok(_cache.GetOrCreate(Language ?? string.Empty + L_KEY, _ => _mLocalizer.ToDictionary()));
[HttpGet("lang")]
public IActionResult GetLanguage() => Language is null ? NotFound() : Ok(Language);
[HttpPost("lang")]
public IActionResult SetLanguage([FromQuery] string language)
{
if (string.IsNullOrEmpty(language))
return BadRequest();
Language = language;
return Ok();
}
[HttpDelete("lang")]
public IActionResult DeleteLanguage()
{
Language = null;
return Ok();
}
private string? Language
{
get
{
var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
if (string.IsNullOrEmpty(cookieValue))
return null;
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
return culture?.Value ?? null;
}
set
{
if (value is null)
Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName);
else
{
var cookieOptions = new CookieOptions()
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
Secure = false,
SameSite = SameSiteMode.Strict,
HttpOnly = true
};
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)),
cookieOptions);
}
}
}
}
}

View File

@@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="DigitalData.Core.Abstractions" Version="1.0.0" />
<PackageReference Include="DigitalData.Core.API" Version="1.0.0" />
<PackageReference Include="DigitalData.Core.API" Version="1.0.1" />
<PackageReference Include="DigitalData.Core.Application" Version="1.0.0" />
<PackageReference Include="DigitalData.Core.DTO" Version="1.0.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="1.0.0" />
@@ -25,6 +25,7 @@
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Common\EnvelopeGenerator.Common.vbproj" />
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
</ItemGroup>

View File

@@ -4,7 +4,9 @@ using DigitalData.UserManager.Application;
using DigitalData.UserManager.Infrastructure.Repositories;
using EnvelopeGenerator.Application;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
@@ -44,7 +46,6 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
options.LoginPath = "/api/auth/login";
options.LogoutPath = "/api/auth/logout";
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.SlidingExpiration = true;
});
@@ -74,7 +75,17 @@ if (app.Environment.IsDevelopment())
app.UseCors("AllowSpecificOriginsPolicy");
// Localizer
app.UseCookieBasedLocalizer("de-DE", "en-US");
string[] supportedCultureNames = { "de-DE", "en-US" };
IList<CultureInfo> list = supportedCultureNames.Select((string cn) => new CultureInfo(cn)).ToList();
CultureInfo cultureInfo = list.FirstOrDefault() ?? throw new ArgumentNullException("supportedCultureNames", "Supported cultures cannot be empty.");
RequestLocalizationOptions requestLocalizationOptions = new RequestLocalizationOptions
{
SupportedCultures = list,
SupportedUICultures = list
};
requestLocalizationOptions.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
app.UseRequestLocalization(requestLocalizationOptions);
app.UseHttpsRedirection();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long