fix dashboard
This commit is contained in:
parent
fb712bbcab
commit
cc7fb93439
9 changed files with 500 additions and 73 deletions
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,26 +1,58 @@
|
|||
@page "/user/{username}"
|
||||
@page "/profile/{username}"
|
||||
@inject HttpClient cli
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Username { get; set; } = "johndoeUwU";
|
||||
private string FullName = "meow uwu";
|
||||
private string Bio = "Software Developer";
|
||||
private List<string> Skills = new List<string> { "C#", "ASP.NET Core", "Blazor" };
|
||||
private UserModel _user = null!;
|
||||
private bool _valid = true;
|
||||
}
|
||||
|
||||
<div class="user-profile">
|
||||
@if (!_valid)
|
||||
{
|
||||
<h1>User not found @(":<")</h1>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_user != null)
|
||||
{
|
||||
<div class="user-profile">
|
||||
<img class="background" src="images/background.jpg" alt="Background" />
|
||||
<img class="profile-img" src="images/kuba.jpg" alt="Profile Picture" />
|
||||
<div class="profile-names">
|
||||
<h1 class="profile-username">@($"@{Username}")</h1>
|
||||
<h3 class="profile-fullname">(@FullName)</h3>
|
||||
</div>
|
||||
<p>@Bio</p>
|
||||
<h2>Skills</h2>
|
||||
<ul>
|
||||
@foreach (var skill in Skills)
|
||||
<h1 class="profile-username">@($"@{_user.Username}")</h1>
|
||||
@if (_user.Name != null)
|
||||
{
|
||||
<h3 class="profile-fullname">(@_user.Name)</h3>
|
||||
}
|
||||
</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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace Interlinked.Shared.Model;
|
||||
|
||||
public class UserModel
|
||||
public record UserModel
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
|
@ -34,6 +34,8 @@ public class UserModel
|
|||
[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);
|
||||
|
|
|
@ -108,5 +108,43 @@ public class AuthController : ControllerBase
|
|||
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! });
|
||||
}
|
||||
}
|
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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -82,6 +82,9 @@ namespace Interlinked.User.Migrations
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("City")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
Loading…
Reference in a new issue