fix dashboard

This commit is contained in:
femsci 2023-10-01 11:50:33 +02:00
parent fb712bbcab
commit cc7fb93439
Signed by: femsci
GPG key ID: 08F7911F0E650C67
9 changed files with 500 additions and 73 deletions

View 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();
}
}

View file

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

View file

@ -1,26 +1,58 @@
@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)
{
<h1>User not found @(":<")</h1>
}
else
{
if (_user != null)
{
<div class="user-profile">
<img class="background" src="images/background.jpg" alt="Background" /> <img class="background" src="images/background.jpg" alt="Background" />
<img class="profile-img" src="images/kuba.jpg" alt="Profile Picture" /> <img class="profile-img" src="images/kuba.jpg" alt="Profile Picture" />
<div class="profile-names"> <div class="profile-names">
<h1 class="profile-username">@($"@{Username}")</h1> <h1 class="profile-username">@($"@{_user.Username}")</h1>
<h3 class="profile-fullname">(@FullName)</h3> @if (_user.Name != null)
</div>
<p>@Bio</p>
<h2>Skills</h2>
<ul>
@foreach (var skill in Skills)
{ {
<li>@skill</li> <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> </ul>
</div> }
</div>
}
}
@code {
protected override async Task OnInitializedAsync()
{
var resp = await cli.GetAsync($"/api/profile/{Username}");
if (!resp.IsSuccessStatusCode)
{
_valid = false;
return;
}
_user = (await resp.Content.ReadFromJsonAsync<UserModel>())!;
this.StateHasChanged();
}
}

View file

@ -10,7 +10,7 @@ using System.Threading.Tasks;
namespace Interlinked.Shared.Model; namespace Interlinked.Shared.Model;
public class UserModel public record UserModel
{ {
[Key] [Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
@ -34,6 +34,8 @@ public class UserModel
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime CreatedTimestamp { get; set; } public DateTime CreatedTimestamp { get; set; }
public string? Bio { get; set; }
public static byte[] HashPassword(string password, byte[] salt) public static byte[] HashPassword(string password, byte[] salt)
{ {
return Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, 1000, HashAlgorithmName.SHA256, 64); return Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, 1000, HashAlgorithmName.SHA256, 64);

View file

@ -108,5 +108,43 @@ public class AuthController : ControllerBase
return Unauthorized(); 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();
}
} }

View 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! });
}
}

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

View 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");
}
}
}

View file

@ -82,6 +82,9 @@ namespace Interlinked.User.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("Bio")
.HasColumnType("TEXT");
b.Property<string>("City") b.Property<string>("City")
.HasColumnType("TEXT"); .HasColumnType("TEXT");