Compare commits

...

1 Commits

Author SHA1 Message Date
Schedel Pascal
32a8a3ec38 holy! the server app is finished & model is nice now 2024-08-23 11:58:12 +02:00
16 changed files with 561 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
namespace OwnChar.Api.Endpoint;
internal class CharactersApi(IOwnCharApiServer server)
{
private IResult GetCharacter(long characterId)
{
}
private IResult CreateGroupCharacter(string name, long groupId)
{
}
private IResult CreateUserCharacter(string name, long userId)
{
}
private IResult UpdateCharacter(long characterId)
{
}
private IResult DeleteCharacter(long characterId)
{
}
}

View File

@@ -0,0 +1,184 @@
using OwnChar.Api.Packets.General;
using OwnChar.Api.Packets.Groups;
using OwnChar.Api.Updates;
using OwnChar.Data;
using OwnChar.Data.Model.Client;
using OwnChar.Data.Model.Server;
using OwnChar.Server.Extensions;
using Pilz.Net.Api;
namespace OwnChar.Api.Endpoint;
internal class GroupsApi(IOwnCharApiServer server)
{
[MessageHandler("/group/get/byid")]
public ApiResult GetById(GetSinlgeObjectRequest request, ApiRequestInfo info)
{
if (!server.CheckLogin(info, UserType.Guest, out UserAccountDb? user))
return ApiResult.Unauthorized();
if (server.Data?.Set<GroupDb>().FirstOrDefault(n => n.Id == request.ObjectId && n.Members.Any(m => m.User != null && m.User.Id == user.Id)) is not GroupDb group)
return ApiResult.NotFound();
return ApiResult.Ok(new GetSingleObjectResponse<Group>(group.ToClient()));
}
[MessageHandler("/group/get", RequiesAuth = true)]
public ApiResult Get(GetGroupsRequest request, ApiRequestInfo info)
{
if (!server.CheckLogin(info, UserType.Guest, out UserAccountDb? user))
return ApiResult.Unauthorized();
IQueryable<GroupDb> groups;
if (request.UseProfileId && server.Data!.Set<UserProfileDb>().FirstOrDefault(p => p.Id == request.ProfileId) is UserProfileDb profile)
groups = server.Data!.Set<GroupDb>().Where(group => group.Members.Any(m => m.User != null && m.User.Id == profile.Id));
else if (request.IncludeNonPublic && user.Is(UserType.Admin))
groups = server.Data!.Set<GroupDb>();
else
groups = Array.Empty<GroupDb>().AsQueryable(); // Currently not supported.
return ApiResult.Ok(new GetGroupsResponse([.. groups.Select(g => g.ToClient())]));
}
[MessageHandler("/group/create", RequiesAuth = true)]
public ApiResult Create(CreateGroupRequest request, ApiRequestInfo info)
{
if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user))
return ApiResult.Unauthorized();
var group = new GroupDb();
if (!string.IsNullOrWhiteSpace(request.Name))
group.Name = request.Name;
group.Members.Add(new()
{
User = user.Profile,
Level = MemberLevel.Owner,
});
server.Data!.Update(group);
server.Data.SaveChanges();
return ApiResult.Ok(new CreateGroupResponse(group.ToClient()));
}
[MessageHandler("/group/update", RequiesAuth = true)]
public ApiResult Update(UpdateRequest request, ApiRequestInfo info)
{
if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user)
|| server.Data?.Set<GroupDb>().FirstOrDefault(n => n.Id == request.Update.Id) is not GroupDb group
|| !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Admin
|| user.IsNot(UserType.Admin)))
return ApiResult.Unauthorized();
if (request.Update is not GroupUpdate update)
return ApiResult.NotFound();
group.Name = update.Name;
group.Fandom = update.Fandom;
server.Data.Update(group);
server.Data.SaveChanges();
return ApiResult.Ok();
}
[MessageHandler("/group/delete", RequiesAuth = true)]
public ApiResult Delete(DeleteObjectRequest request, ApiRequestInfo info)
{
if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user)
|| server.Data?.Set<GroupDb>().FirstOrDefault(n => n.Id == request.ObjectId) is not GroupDb group
|| !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Owner)
|| user.IsNot(UserType.Admin))
return ApiResult.Unauthorized();
server.Data.Remove(group);
server.Data.SaveChanges();
return ApiResult.Ok();
}
[MessageHandler("/group/members/get", RequiesAuth = true)]
public ApiResult GetMembers(GetGroupMembersRequest request, ApiRequestInfo info)
{
if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user)
|| server.Data?.Set<GroupDb>().FirstOrDefault(n => n.Id == request.GroupId) is not GroupDb group
|| !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Member)
|| user.IsNot(UserType.Admin))
return ApiResult.Unauthorized();
var members = group.Members.Select(n => n.ToClient());
return ApiResult.Ok(new GetGroupMembersResponse(members.ToList()));
}
[MessageHandler("/group/members/add", RequiesAuth = true)]
public ApiResult AddMembers(GroupMemberAddRequest request, ApiRequestInfo info)
{
if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user)
|| server.Data?.Set<GroupDb>().FirstOrDefault(n => n.Id == request.GroupId) is not GroupDb group
|| !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Admin)
|| user.IsNot(UserType.Admin))
return ApiResult.Unauthorized();
var addedMembers = new List<MemberEntryDb>();
foreach (var kvp in request.Members)
{
if (group.Members.FirstOrDefault(m => m.User != null && m.User.Id == kvp.Key) is not MemberEntryDb member
&& server.Data.Set<UserProfileDb>().FirstOrDefault(u => u.Id == kvp.Key) is UserProfileDb mu)
{
member = new()
{
User = mu,
Level = kvp.Value,
};
server.Data.Update(member);
server.Data.Update(group);
server.Data.SaveChanges();
}
}
return ApiResult.Ok(new GroupMemberAddResponse(addedMembers.Select(m => m.ToClient()).ToList()));
}
[MessageHandler("/group/members/update", RequiesAuth = true)]
public ApiResult UpdateMember(UpdateRequest request, ApiRequestInfo info)
{
if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user)
|| server.Data?.Set<GroupDb>().FirstOrDefault(n => n.Id == request.Update.Id) is not GroupDb group
|| group.Members.FirstOrDefault(m => m.Id == request.Update.Id) is not MemberEntryDb member
|| !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Admin
|| user.IsNot(UserType.Admin)))
return ApiResult.Unauthorized();
if (request.Update is not MemberUpdate update)
return ApiResult.NotFound();
member.Level = update.Level;
server.Data.Update(member);
server.Data.SaveChanges();
return ApiResult.Ok();
}
[MessageHandler("/group/members/remove", RequiesAuth = true)]
public ApiResult RemoveMembers(DeleteObjectRequest request, ApiRequestInfo info)
{
if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user)
|| server.Data?.Set<MemberEntryDb>().FirstOrDefault(m => m.Id == request.ObjectId) is not MemberEntryDb member
|| server.Data?.Set<GroupDb>().FirstOrDefault(n => n.Members.Contains(member)) is not GroupDb group
|| !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Admin
|| user.IsNot(UserType.Admin)))
return ApiResult.Unauthorized();
group.Members.Remove(member);
server.Data.Remove(member);
server.Data.Update(group);
server.Data.SaveChanges();
return ApiResult.Ok();
}
}

View File

@@ -0,0 +1,30 @@
using OwnChar.Api.Packets;
using OwnChar.Api.Packets.General;
using OwnChar.Data.Model.Server;
using OwnChar.Server.Extensions;
using Pilz.Net.Api;
namespace OwnChar.Api.Endpoint;
internal class LoginApi(OwnCharApiServer server)
{
[MessageHandler("/auth/login")]
public ApiResult Login(LoginRequest request, ApiRequestInfo info)
{
if (server.Data?.Set<UserAccountDb>()?.FirstOrDefault(n => n.Username == request.Username && n.Password == request.Password) is UserAccountDb acc && acc.Profile != null)
{
if (info.IsAuthenticated)
server.Logout(info.AuthKey);
return ApiResult.Ok(new LoginResponse(acc.ToClient(), acc.Profile.ToClient(), server.Login(acc)));
}
return ApiResult.NotFound();
}
[MessageHandler("/auth/logout")]
public ApiResult Logout(ApiRequestInfo info)
{
if (info.IsAuthenticated)
server.Logout(info.AuthKey);
return ApiResult.Ok();
}
}

View File

@@ -0,0 +1,39 @@
namespace OwnChar.Api.Endpoint;
internal class UsersApi(IOwnCharApiServer server)
{
private IResult GetUsers()
{
}
private IResult GetUser(long userId)
{
}
private IResult GetUserProfile(long userId)
{
}
private IResult CreateUser(string username, string password)
{
}
private IResult DeleteUser(long userId)
{
}
private IResult UpdateUserPassword(long userId, string username, string password)
{
}
private IResult UpdateUserProfile(long profileId, string displayName)
{
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using OwnChar.Data.Model.Base;
using Pilz.Net.Api;
using System.Diagnostics.CodeAnalysis;
namespace OwnChar.Api;
public interface IOwnCharApiServer : IApiServer
{
DbContext? Data { get; }
[MemberNotNull(nameof(Data))]
void CheckLogin(string secret);
[MemberNotNullWhen(true, nameof(Data))]
bool IsLoggedIn(string secret);
[MemberNotNullWhen(true, nameof(Data))]
UserAccountBase? GetUser(string secret);
}

View File

@@ -0,0 +1,89 @@
using Microsoft.EntityFrameworkCore;
using OwnChar.Api;
using OwnChar.Api.Endpoint;
using OwnChar.Data.Model.Base;
using OwnChar.Server.Data;
using Pilz.Cryptography;
using Pilz.Extensions.Collections;
using Pilz.Net.Api;
namespace OwnChar.Api;
internal class OwnCharApiServer(string apiUrl, string dbServer, string dbDatabase, string dbUser, SecureString dbPassword) : ApiServer(apiUrl), IOwnCharApiServer
{
private readonly Dictionary<string, UserAccountBase> users = [];
public DbContext? Data { get; private set; }
public override void Start()
{
Log.Info("Prepairing server");
// Load database
Log.Debug("Loading database");
Data = new DatabaseContext(dbServer, dbDatabase, dbUser, dbPassword);
// Built-in endpoints
Log.Debug("Loading internal api endpoints");
RegisterHandler(new LoginApi(this));
RegisterHandler(new UsersApi(this));
RegisterHandler(new GroupsApi(this));
RegisterHandler(new CharactersApi(this));
// Run server
Log.Info("Starting webserver");
base.Start();
}
protected override string? DecodeAuthKey(string authKey)
{
if (!string.IsNullOrWhiteSpace(authKey))
return new SecureString(authKey, true).Value;
return authKey;
}
protected override bool CheckAuthentication(string authKey)
{
return string.IsNullOrWhiteSpace(authKey)
|| authKey.Split(":") is not string[] authKeyParts
|| authKey.Length != 2
|| string.IsNullOrWhiteSpace(authKeyParts[0])
|| string.IsNullOrWhiteSpace(authKeyParts[1])
|| !IsLoggedIn(authKeyParts[1]);
}
public string Login(UserAccountBase account)
{
var secret = new UniquieID(UniquieIDGenerationMode.GenerateOnInit).ID;
users.Add(secret, account);
Log.DebugFormat("Logged-in out user with secret {0}", secret);
return secret;
}
public void Logout(string secret)
{
users.Remove(secret);
Log.DebugFormat("Logged-out user with secret {0}", secret);
}
public bool IsLoggedIn(string secret)
{
Log.DebugFormat("Deleting user with secret {0}", secret);
return users.ContainsKey(secret);
}
public void CheckLogin(string secret)
{
Log.DebugFormat("Checking login for user with secret {0}", secret);
if (!IsLoggedIn(secret))
throw new UnauthorizedAccessException();
}
public UserAccountBase? GetUser(string secret)
{
Log.DebugFormat("Getting user with secret {0}", secret);
if (users.TryGetValue(secret, out UserAccountBase? value))
return value;
return null;
}
}

View File

@@ -0,0 +1,10 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Server;
public class CharacterDb : CharacterBase
{
public UserProfileDb? Owner { get; set; }
public List<PropertyCategoryDb> PropCats { get; } = [];
public List<PropertyDb> Props { get; } = [];
}

View File

@@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
using Pilz.Cryptography;
namespace OwnChar.Server.Data;
public class DatabaseContext(string dbHost, string dbDatabase, string dbUser, SecureString dbPassword) : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Server;
public class GroupDb : GroupBase
{
public List<CharacterDb> Characters { get; } = [];
public List<MemberEntryDb> Members { get; } = [];
}

View File

@@ -0,0 +1,8 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Server;
public class MemberEntryDb() : MemberEntryBase
{
public virtual UserProfileDb? User { get; set; }
}

View File

@@ -0,0 +1,7 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Server;
public class PropertyCategoryDb : PropertyCategoryBase
{
}

View File

@@ -0,0 +1,7 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Server;
public class PropertyDb : PropertyBase
{
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Server;
public class UserAccountDb : UserAccountBase
{
public virtual string? Password { get; set; }
public UserProfileDb? Profile { get; set; }
}

View File

@@ -0,0 +1,8 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Server;
public class UserProfileDb : UserProfileBase
{
public List<CharacterDb> Characters { get; } = [];
}

View File

@@ -0,0 +1,48 @@
using OwnChar.Data.Model.Client;
using OwnChar.Data.Model.Server;
namespace OwnChar.Server.Extensions;
public static class ModelExtensions
{
public static Group ToClient(this GroupDb @this)
{
return new()
{
Id = @this.Id,
Name = @this.Name,
Fandom = @this.Fandom,
IsInternal = @this.IsInternal,
};
}
public static UserAccount ToClient(this UserAccountDb @this)
{
return new()
{
Id = @this.Id,
Email = @this.Email,
Type = @this.Type,
Username = @this.Username,
};
}
public static UserProfile ToClient(this UserProfileDb @this)
{
return new()
{
Id = @this.Id,
Name = @this.Name,
};
}
public static MemberEntry ToClient(this MemberEntryDb @this)
{
return new()
{
Id = @this.Id,
UserProfileId = @this.User!.Id,
Level = @this.Level,
};
}
}

View File

@@ -0,0 +1,47 @@
using OwnChar.Api;
using OwnChar.Data;
using OwnChar.Data.Model.Base;
using OwnChar.Data.Model.Server;
using Pilz.Net.Api;
using System.Diagnostics.CodeAnalysis;
namespace OwnChar.Server.Extensions;
public static class OwnCharApiServerExtensions
{
public static bool CheckLogin(this IOwnCharApiServer server, ApiRequestInfo request, UserType userType)
{
if (server.Data is null
|| !request.IsAuthenticated
|| request.AuthKey.Split(":") is not string[] authKey
|| authKey.ElementAtOrDefault(0) is not string username
|| authKey.ElementAtOrDefault(1) is not string secret
|| string.IsNullOrWhiteSpace(username)
|| string.IsNullOrWhiteSpace(secret)
|| !server.IsLoggedIn(secret)
|| server.GetUser(secret) is not UserAccountBase usr
|| usr.Type < userType)
return false;
return true;
}
public static bool CheckLogin(this IOwnCharApiServer server, ApiRequestInfo request, UserType userType, [NotNullWhen(true)] out UserAccountDb? user)
{
if (server.Data is null
|| !request.IsAuthenticated
|| request.AuthKey.Split(":") is not string[] authKey
|| authKey.ElementAtOrDefault(0) is not string username
|| authKey.ElementAtOrDefault(1) is not string secret
|| string.IsNullOrWhiteSpace(username)
|| string.IsNullOrWhiteSpace(secret)
|| !server.IsLoggedIn(secret)
|| server.GetUser(secret) is not UserAccountDb usr
|| usr.Type < userType)
{
user = null;
return false;
}
user = usr;
return true;
}
}