Compare commits
9 Commits
feat/annot
...
16bdc7820d
| Author | SHA1 | Date | |
|---|---|---|---|
| 16bdc7820d | |||
| 06e32b99ea | |||
| c7c78f96a6 | |||
| 5c232e61f2 | |||
| 24c9321c0f | |||
| c75c2b1dd5 | |||
| 8445757f34 | |||
| b088eb089f | |||
| 1f745ae79c |
49
EnvelopeGenerator.Web/EnvelopeCookieManager.cs
Normal file
49
EnvelopeGenerator.Web/EnvelopeCookieManager.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Web;
|
||||||
|
|
||||||
|
public class EnvelopeCookieManager : ICookieManager
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<string> _envelopeKeyBasedCookieNames;
|
||||||
|
|
||||||
|
private readonly ChunkingCookieManager _inner = new();
|
||||||
|
|
||||||
|
public EnvelopeCookieManager(params string[] envelopeKeyBasedCookieNames)
|
||||||
|
{
|
||||||
|
_envelopeKeyBasedCookieNames = envelopeKeyBasedCookieNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCookieName(HttpContext context, string key)
|
||||||
|
{
|
||||||
|
if (!_envelopeKeyBasedCookieNames.Contains(key))
|
||||||
|
return key;
|
||||||
|
|
||||||
|
var envId = context.GetRouteValue("envelopeReceiverId")?.ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(envId) && context.Request.Query.TryGetValue("envKey", out var envKeyValue))
|
||||||
|
envId = envKeyValue;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(envId))
|
||||||
|
return key;
|
||||||
|
|
||||||
|
return $"{key}-{envId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetRequestCookie(HttpContext context, string key)
|
||||||
|
{
|
||||||
|
var cookieName = GetCookieName(context, key);
|
||||||
|
return _inner.GetRequestCookie(context, cookieName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendResponseCookie(HttpContext context, string key, string? value, CookieOptions options)
|
||||||
|
{
|
||||||
|
var cookieName = GetCookieName(context, key);
|
||||||
|
_inner.AppendResponseCookie(context, cookieName, value, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
|
||||||
|
{
|
||||||
|
var cookieName = GetCookieName(context, key);
|
||||||
|
_inner.DeleteCookie(context, cookieName, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
<PackageTags>digital data envelope generator web</PackageTags>
|
<PackageTags>digital data envelope generator web</PackageTags>
|
||||||
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
|
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
|
||||||
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
|
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
|
||||||
<Version>3.7.0</Version>
|
<Version>3.8.0</Version>
|
||||||
<AssemblyVersion>3.7.0</AssemblyVersion>
|
<AssemblyVersion>3.8.0</AssemblyVersion>
|
||||||
<FileVersion>3.7.0</FileVersion>
|
<FileVersion>3.8.0</FileVersion>
|
||||||
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
|
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using EnvelopeGenerator.Web.Models.Annotation;
|
|||||||
using DigitalData.UserManager.DependencyInjection;
|
using DigitalData.UserManager.DependencyInjection;
|
||||||
using EnvelopeGenerator.Web.Middleware;
|
using EnvelopeGenerator.Web.Middleware;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Web;
|
||||||
|
|
||||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||||
logger.Info("Logging initialized!");
|
logger.Info("Logging initialized!");
|
||||||
@@ -134,35 +135,16 @@ try
|
|||||||
options.ConsentCookie.Name = "cookie-consent-settings";
|
options.ConsentCookie.Name = "cookie-consent-settings";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var authCookieName = "env_auth";
|
||||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
.AddCookie(options =>
|
.AddCookie(options =>
|
||||||
{
|
{
|
||||||
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
|
options.Cookie.Name = authCookieName;
|
||||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
|
options.CookieManager = new EnvelopeCookieManager(authCookieName);
|
||||||
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
|
options.Cookie.HttpOnly = true;
|
||||||
|
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||||
|
options.Cookie.SameSite = SameSiteMode.Strict;
|
||||||
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
|
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
|
||||||
|
|
||||||
options.Events = new CookieAuthenticationEvents
|
|
||||||
{
|
|
||||||
OnRedirectToLogin = context =>
|
|
||||||
{
|
|
||||||
// Dynamically calculate the redirection path, for example:
|
|
||||||
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
|
|
||||||
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
|
|
||||||
|
|
||||||
context.Response.Redirect(context.RedirectUri);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
},
|
|
||||||
OnRedirectToLogout = context =>
|
|
||||||
{
|
|
||||||
// Apply a similar redirection logic for logout
|
|
||||||
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
|
|
||||||
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
|
|
||||||
|
|
||||||
context.Response.Redirect(context.RedirectUri);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddSingleton(config.GetSection("ContactLink").Get<ContactLink>() ?? new());
|
builder.Services.AddSingleton(config.GetSection("ContactLink").Get<ContactLink>() ?? new());
|
||||||
@@ -249,7 +231,7 @@ try
|
|||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
var cultures = app.Services.GetRequiredService<Cultures>();
|
var cultures = app.Services.GetRequiredService<Cultures>();
|
||||||
if(!cultures.Any())
|
if (!cultures.Any())
|
||||||
throw new InvalidOperationException(@"Languages section is missing in the appsettings. Please configure like following.
|
throw new InvalidOperationException(@"Languages section is missing in the appsettings. Please configure like following.
|
||||||
Language is both a name of the culture and the name of the resx file such as Resource.de-DE.resx
|
Language is both a name of the culture and the name of the resx file such as Resource.de-DE.resx
|
||||||
FIClass is the css class (in wwwroot/lib/flag-icons-main) for the flag of country.
|
FIClass is the css class (in wwwroot/lib/flag-icons-main) for the flag of country.
|
||||||
@@ -264,7 +246,7 @@ try
|
|||||||
}
|
}
|
||||||
]");
|
]");
|
||||||
|
|
||||||
if(!config.GetValue<bool>("DisableMultiLanguage"))
|
if (!config.GetValue<bool>("DisableMultiLanguage"))
|
||||||
app.UseCookieBasedLocalizer(cultures.Languages.ToArray());
|
app.UseCookieBasedLocalizer(cultures.Languages.ToArray());
|
||||||
|
|
||||||
app.UseCors("SameOriginPolicy");
|
app.UseCors("SameOriginPolicy");
|
||||||
@@ -273,7 +255,7 @@ try
|
|||||||
app.MapFallbackToController("Error404", "Home");
|
app.MapFallbackToController("Error404", "Home");
|
||||||
app.Run();
|
app.Run();
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.Error(ex, "Stopped program because of exception");
|
logger.Error(ex, "Stopped program because of exception");
|
||||||
throw;
|
throw;
|
||||||
|
|||||||
@@ -1,106 +1,111 @@
|
|||||||
//#region parameters
|
//#region parameters
|
||||||
const env = Object.freeze({
|
const env = Object.freeze({
|
||||||
__lazyXsrfToken: new Lazy(() => document.getElementsByName('__RequestVerificationToken')[0].value),
|
__lazyXsrfToken: new Lazy(() => document.getElementsByName('__RequestVerificationToken')[0].value),
|
||||||
get xsrfToken() {
|
get xsrfToken() {
|
||||||
return this.__lazyXsrfToken.value;
|
return this.__lazyXsrfToken.value;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const url = Object.freeze({
|
const url = Object.freeze({
|
||||||
reject: `/api/annotation/reject`,
|
reject: `/api/annotation/reject`,
|
||||||
share: `/api/readonly`
|
share: `/api/readonly`
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region request helper methods
|
//#region request helper methods
|
||||||
function sendRequest(method, url, body = undefined) {
|
function sendRequest(method, url, body = undefined) {
|
||||||
const options = {
|
const urlObj = new URL(url, window.location.origin);
|
||||||
credentials: 'include',
|
if (!urlObj.searchParams.has("envKey")) {
|
||||||
method: method,
|
urlObj.searchParams.set("envKey", ENV_KEY);
|
||||||
headers: {
|
|
||||||
'X-XSRF-TOKEN': env.xsrfToken
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (body !== undefined) {
|
const options = {
|
||||||
options.body = JSON.stringify(body);
|
credentials: 'include',
|
||||||
options.headers['Content-Type'] = 'application/json';
|
method: method,
|
||||||
}
|
headers: {
|
||||||
|
'X-XSRF-TOKEN': env.xsrfToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(url, options);
|
if (body !== undefined) {
|
||||||
|
options.body = JSON.stringify(body);
|
||||||
|
options.headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(urlObj, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRequest(url) {
|
function getRequest(url) {
|
||||||
return sendRequest('GET', url);
|
return sendRequest('GET', url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getJson(url) {
|
function getJson(url) {
|
||||||
return sendRequest('GET', url).then(res => {
|
return sendRequest('GET', url).then(res => {
|
||||||
if (res.ok)
|
if (res.ok)
|
||||||
return res.json();
|
return res.json();
|
||||||
throw new Error(`Request failed with status ${res.status}`);
|
throw new Error(`Request failed with status ${res.status}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function postRequest(url, body = undefined) {
|
function postRequest(url, body = undefined) {
|
||||||
return sendRequest('POST', url, body);
|
return sendRequest('POST', url, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirect(url) {
|
function redirect(url) {
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region envelope
|
//#region envelope
|
||||||
function signEnvelope(annotations) {
|
function signEnvelope(annotations) {
|
||||||
return postRequest(`/api/annotation`, annotations)
|
return postRequest(`/api/annotation`, annotations)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAnnotationParams(leftInInch = 0, topInInch = 0, inchToPointFactor = 72) {
|
async function getAnnotationParams(leftInInch = 0, topInInch = 0, inchToPointFactor = 72) {
|
||||||
const annotParams = await getJson("/api/Config/Annotations");
|
const annotParams = await getJson("/api/Config/Annotations");
|
||||||
|
|
||||||
for (var key in annotParams) {
|
for (var key in annotParams) {
|
||||||
var annot = annotParams[key];
|
var annot = annotParams[key];
|
||||||
annot.width *= inchToPointFactor;
|
annot.width *= inchToPointFactor;
|
||||||
annot.height *= inchToPointFactor;
|
annot.height *= inchToPointFactor;
|
||||||
annot.left += leftInInch - 0.7;
|
annot.left += leftInInch - 0.7;
|
||||||
annot.left *= inchToPointFactor;
|
annot.left *= inchToPointFactor;
|
||||||
annot.top += topInInch - 0.5;
|
annot.top += topInInch - 0.5;
|
||||||
annot.top *= inchToPointFactor;
|
annot.top *= inchToPointFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return annotParams;
|
return annotParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rejectEnvelope(reason) {
|
function rejectEnvelope(reason) {
|
||||||
return postRequest(url.reject, reason);
|
return postRequest(url.reject, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shareEnvelope(receiverMail, dateValid) {
|
function shareEnvelope(receiverMail, dateValid) {
|
||||||
return postRequest(url.share, { receiverMail: receiverMail, dateValid: dateValid });
|
return postRequest(url.share, { receiverMail: receiverMail, dateValid: dateValid });
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
async function setLanguage(language) {
|
async function setLanguage(language) {
|
||||||
const hasLang = await getJson('/api/localization/lang')
|
const hasLang = await getJson('/api/localization/lang')
|
||||||
.then(langs => langs.includes(language));
|
.then(langs => langs.includes(language));
|
||||||
|
|
||||||
if (hasLang)
|
if (hasLang)
|
||||||
postRequest(`/api/localization/lang/${language}`)
|
postRequest(`/api/localization/lang/${language}`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.redirected)
|
if (response.redirected)
|
||||||
redirect(response.url);
|
redirect(response.url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
return postRequest(`/auth/logout`)
|
return postRequest(`/auth/logout`)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.ok)
|
if (res.ok)
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
function sendRequest(n,t,i=undefined){const r={credentials:"include",method:n,headers:{"X-XSRF-TOKEN":env.xsrfToken}};return i!==undefined&&(r.body=JSON.stringify(i),r.headers["Content-Type"]="application/json"),fetch(t,r)}function getRequest(n){return sendRequest("GET",n)}function getJson(n){return sendRequest("GET",n).then(n=>{if(n.ok)return n.json();throw new Error(`Request failed with status ${n.status}`);})}function postRequest(n,t=undefined){return sendRequest("POST",n,t)}function reload(){window.location.reload()}function redirect(n){window.location.href=n}function signEnvelope(n){return postRequest(`/api/annotation`,n)}async function getAnnotationParams(n=0,t=0,i=72){var f,r;const u=await getJson("/api/Config/Annotations");for(f in u)r=u[f],r.width*=i,r.height*=i,r.left+=n-.7,r.left*=i,r.top+=t-.5,r.top*=i;return u}function rejectEnvelope(n){return postRequest(url.reject,n)}function shareEnvelope(n,t){return postRequest(url.share,{receiverMail:n,dateValid:t})}async function setLanguage(n){const t=await getJson("/api/localization/lang").then(t=>t.includes(n));t&&postRequest(`/api/localization/lang/${n}`).then(n=>{n.redirected&&redirect(n.url)})}function logout(){return postRequest(`/auth/logout`).then(n=>{n.ok&&(window.location.href="/")})}const env=Object.freeze({__lazyXsrfToken:new Lazy(()=>document.getElementsByName("__RequestVerificationToken")[0].value),get xsrfToken(){return this.__lazyXsrfToken.value}}),url=Object.freeze({reject:`/api/annotation/reject`,share:`/api/readonly`});
|
function sendRequest(n,t,i=undefined){const r=new URL(t,window.location.origin);r.searchParams.has("envKey")||r.searchParams.set("envKey",ENV_KEY);const u={credentials:"include",method:n,headers:{"X-XSRF-TOKEN":env.xsrfToken}};return i!==undefined&&(u.body=JSON.stringify(i),u.headers["Content-Type"]="application/json"),fetch(r,u)}function getRequest(n){return sendRequest("GET",n)}function getJson(n){return sendRequest("GET",n).then(n=>{if(n.ok)return n.json();throw new Error(`Request failed with status ${n.status}`);})}function postRequest(n,t=undefined){return sendRequest("POST",n,t)}function reload(){window.location.reload()}function redirect(n){window.location.href=n}function signEnvelope(n){return postRequest(`/api/annotation`,n)}async function getAnnotationParams(n=0,t=0,i=72){var f,r;const u=await getJson("/api/Config/Annotations");for(f in u)r=u[f],r.width*=i,r.height*=i,r.left+=n-.7,r.left*=i,r.top+=t-.5,r.top*=i;return u}function rejectEnvelope(n){return postRequest(url.reject,n)}function shareEnvelope(n,t){return postRequest(url.share,{receiverMail:n,dateValid:t})}async function setLanguage(n){const t=await getJson("/api/localization/lang").then(t=>t.includes(n));t&&postRequest(`/api/localization/lang/${n}`).then(n=>{n.redirected&&redirect(n.url)})}function logout(){return postRequest(`/auth/logout`).then(n=>{n.ok&&(window.location.href="/")})}const env=Object.freeze({__lazyXsrfToken:new Lazy(()=>document.getElementsByName("__RequestVerificationToken")[0].value),get xsrfToken(){return this.__lazyXsrfToken.value}}),url=Object.freeze({reject:`/api/annotation/reject`,share:`/api/readonly`});
|
||||||
@@ -4,14 +4,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Datenschutzinformation für das Fernsignatursystem signFLOW</title>
|
<title>Datenschutzinformation für das Fernsignatursystem: signFLOW</title>
|
||||||
<link rel="stylesheet" href="css/privacy-policy.min.css">
|
<link rel="stylesheet" href="css/privacy-policy.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>Datenschutzinformation für das Fernsignatursystem signFLOW</h1>
|
<h1>Datenschutzinformation für das Fernsignatursystem signFLOW</h1>
|
||||||
<p><strong>Stand:</strong> 19.09.2024</p>
|
<p><strong>Stand:</strong> 18.11.2025</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<h2>3. Datenerhebung</h2>
|
<h2>3. Datenerhebung</h2>
|
||||||
<h3>3.1 Die folgenden Kategorien personenbezogener Daten werden verarbeitet</h3>
|
<h3>3.1 Die folgenden Kategorien personenbezogener Daten werden verarbeitet</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Namen: Vor- und Zunamen sowie Ihre digitale Unterschrift</li>
|
<li>Namen: Benutzername, Vor- und Zunamen sowie Ihre digitale Unterschrift</li>
|
||||||
<li>Kontaktdaten: Telefonnummer, Mobilfunknummer und E-Mail-Adresse</li>
|
<li>Kontaktdaten: Telefonnummer, Mobilfunknummer und E-Mail-Adresse</li>
|
||||||
<li>Technische Daten: IP-Adresse, Zeitpunkt des Zugriffs oder Zugriffsversuchs</li>
|
<li>Technische Daten: IP-Adresse, Zeitpunkt des Zugriffs oder Zugriffsversuchs</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>Data Protection Information for the Remote Signature System signFLOW</h1>
|
<h1>Data Protection Information for the Remote Signature System: signFLOW</h1>
|
||||||
<p><strong>As of:</strong> 19.09.2024</p>
|
<p><strong>As of:</strong> 18.11.2025</p>
|
||||||
</header>
|
</header>
|
||||||
<section>
|
<section>
|
||||||
<h2>1. General Information</h2>
|
<h2>1. General Information</h2>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<h2>3. Data Collection</h2>
|
<h2>3. Data Collection</h2>
|
||||||
<h3>3.1 The following categories of personal data are processed</h3>
|
<h3>3.1 The following categories of personal data are processed</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Names: First and last names as well as your digital signature</li>
|
<li>Names: Username, first and last names as well as your digital signature</li>
|
||||||
<li>Contact details: Phone number, mobile phone number, and email address</li>
|
<li>Contact details: Phone number, mobile phone number, and email address</li>
|
||||||
<li>Technical data: IP address, time of access, or access attempts</li>
|
<li>Technical data: IP address, time of access, or access attempts</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user