Compare commits

...

26 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
Schedel Pascal
f268058cdc finish migration to Pilz.NET 2024-08-23 11:18:50 +02:00
6b5445ccdf party migrate to Pilz.Net 2024-08-22 15:13:08 +02:00
24708eb798 push my ork 2024-08-15 09:20:31 +02:00
Schedel Pascal
b08c7619a4 uff, lot of work 2024-08-02 11:47:38 +02:00
Schedel Pascal
db5191b0b8 bit more work on api client 2024-07-31 11:25:19 +02:00
38dc09ab12 rework model to have a client and server variant 2024-07-21 09:52:43 +02:00
32708e3e26 some work (need to thing about client and server data model as next step) 2024-07-18 16:42:46 +02:00
986bc3853c begin new and last rework 2024-07-18 14:44:44 +02:00
Schedel Pascal
a721b5ab6c and even more 2024-07-09 10:57:20 +02:00
Schedel Pascal
bd0053cc03 some fixes 2024-07-09 10:23:05 +02:00
Schedel Pascal
9e77090bd4 completely rework IDataProvider
- make it more dynamic
- prevent incremental method count increase
2024-07-09 10:13:38 +02:00
Schedel Pascal
f1deeeda6c work 2024-07-08 15:19:00 +02:00
Schedel Pascal
342c6dfcb2 more extension methods 2024-07-08 09:38:35 +02:00
Schedel Pascal
cd32786115 minor renames & updates 2024-07-05 11:56:55 +02:00
Schedel Pascal
444bc0b44f urg 2024-07-03 08:01:01 +02:00
2e9acf1578 asdjf 2024-06-30 18:33:23 +02:00
6a48628fd8 d 2024-06-30 18:32:07 +02:00
2f1e9716fe big big update 2024-06-30 18:19:00 +02:00
Zoe Fenris
b592dde7bb add owner-property to character 2024-06-21 15:10:41 +02:00
Schedel Pascal
e381f99b5a simpler nullcheck 2024-06-17 08:38:10 +02:00
Schedel Pascal
7ab05d41f0 fix null warning in CheckLogin() 2024-06-17 08:31:51 +02:00
Schedel Pascal
48b8faed4e simplyfy null checks 2024-06-17 08:30:40 +02:00
Schedel Pascal
d3b935bebc null checks 2024-06-17 08:27:18 +02:00
c453434c58 d 2024-06-13 22:03:52 +02:00
16038c9e6a add missing h 2024-06-13 22:01:01 +02:00
106 changed files with 1481 additions and 830 deletions

View File

@@ -0,0 +1,5 @@
namespace OwnChar.Api.Clients;
internal class CharactersApiClient(OwnCharApiClient client) : ICharactersApiClient
{
}

View File

@@ -0,0 +1,43 @@
using OwnChar.Api.Packets.General;
using OwnChar.Api.Packets.Groups;
using OwnChar.Data.Model.Client;
namespace OwnChar.Api.Clients;
internal class GroupsApiClient(IOwnCharApiClient client) : IGroupsApiClient
{
public async Task<Group?> GetGroup(long id)
{
var result = await client.SendRequest<GetSingleObjectResponse<Group>>("/groups/get/byid", new GetSinlgeObjectRequest(id));
result.EnsureOk();
return result.Message.Result;
}
public async Task<IEnumerable<Group>> GetGroupsForProfile(long userProfileId)
{
var result = await client.SendRequest<GetGroupsResponse>("/groups/get", new GetGroupsRequest
{
ProfileId = userProfileId,
UseProfileId = true,
});
result.EnsureOk();
return result.Message.Groups;
}
public async Task<IEnumerable<Group>> GetPublicGroups()
{
var result = await client.SendRequest<GetGroupsResponse>("/groups/get", new GetGroupsRequest());
result.EnsureOk();
return result.Message.Groups;
}
public async Task<IEnumerable<Group>> GetAllGroups()
{
var result = await client.SendRequest<GetGroupsResponse>("/groups/get", new GetGroupsRequest
{
IncludeNonPublic = true,
});
result.EnsureOk();
return result.Message.Groups;
}
}

View File

@@ -0,0 +1,5 @@
namespace OwnChar.Api.Clients;
public interface ICharactersApiClient
{
}

View File

@@ -0,0 +1,10 @@
using OwnChar.Data.Model.Client;
namespace OwnChar.Api.Clients;
public interface IGroupsApiClient
{
Task<IEnumerable<Group>> GetAllGroups();
Task<Group?> GetGroup(long id);
Task<IEnumerable<Group>> GetGroupsForProfile(long userProfileId);
Task<IEnumerable<Group>> GetPublicGroups();
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Data.Model.Client;
using Pilz.Cryptography;
namespace OwnChar.Api.Clients;
public interface ILoginApiClient
{
Task<UserProfile?> Login(string username, SecureString password);
Task<bool> Logout();
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OwnChar.Api.Clients;
public interface IUsersApiClient
{
}

View File

@@ -0,0 +1,30 @@
using OwnChar.Api.Packets.General;
using OwnChar.Data.Model.Client;
using Pilz.Cryptography;
namespace OwnChar.Api.Clients;
internal class LoginApiClient(OwnCharApiClient client) : ILoginApiClient
{
public async Task<UserProfile?> Login(string username, SecureString password)
{
var result = await client.SendRequest<LoginResponse>("/auth/login", new LoginRequest(username, password));
result.EnsureOk();
if (!string.IsNullOrEmpty(result.Message.Secret) && result.Message.Profile != null && result.Message.Account != null)
{
client.AuthKey = result.Message.Secret;
client.CurrentUser = result.Message.Account;
return result.Message.Profile;
}
return null;
}
public async Task<bool> Logout()
{
var res = await client.SendRequest("/auth/logout");
client.AuthKey = null;
return res.IsOk;
}
}

View File

@@ -0,0 +1,5 @@
namespace OwnChar.Api.Clients;
internal class UsersApiClient(IOwnCharApiClient client) : IUsersApiClient
{
}

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,5 @@
namespace OwnChar.Api.Exceptions;
public class ApiException(string message) : Exception(message)
{
}

View File

@@ -1,4 +1,4 @@
namespace OwnChar.Manager.Exceptions; namespace OwnChar.Api.Exceptions;
public class LoginException(string message) : Exception(message) public class LoginException(string message) : Exception(message)
{ {

View File

@@ -0,0 +1,21 @@
using OwnChar.Api.Clients;
using OwnChar.Data.Model.Client;
using Pilz.Net.Api;
using System.Diagnostics.CodeAnalysis;
namespace OwnChar.Api;
public interface IOwnCharApiClient : IApiClient
{
UserAccount? CurrentUser { get; }
[MemberNotNullWhen(true, nameof(AuthKey), nameof(CurrentUser))]
bool IsLoggedIn { get; }
ILoginApiClient Auth { get; }
IGroupsApiClient Groups { get; }
ICharactersApiClient Characters { get; }
IUsersApiClient Users { get; }
}

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,38 @@
using OwnChar.Api.Clients;
using OwnChar.Data.Model.Client;
using Pilz.Cryptography;
using Pilz.Net.Api;
using System.Diagnostics.CodeAnalysis;
namespace OwnChar.Api;
internal class OwnCharApiClient : ApiClient, IOwnCharApiClient
{
public UserAccount? CurrentUser { get; internal set; }
[MemberNotNullWhen(true, nameof(AuthKey), nameof(CurrentUser))]
public bool IsLoggedIn => CurrentUser != null && !string.IsNullOrWhiteSpace(AuthKey);
public ILoginApiClient Auth { get; }
public IGroupsApiClient Groups { get; }
public ICharactersApiClient Characters { get; }
public IUsersApiClient Users { get; }
public OwnCharApiClient(string apiUrl) : base(apiUrl)
{
Auth = new LoginApiClient(this);
Groups = new GroupsApiClient(this);
Characters = new CharactersApiClient(this);
Users = new UsersApiClient(this);
}
protected override string? EncodeAuthKey()
{
if (IsLoggedIn && !string.IsNullOrWhiteSpace(CurrentUser.Username))
return new SecureString($"{CurrentUser}:{AuthKey}", false).EncryptedValue;
return AuthKey;
}
}

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,8 @@
using OwnChar.Api.Packets;
namespace OwnChar.Api.Packets.General;
public class DeleteObjectRequest(long objectId) : OwnCharRequest
{
public long ObjectId { get; } = objectId;
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Api.Packets;
using OwnChar.Data;
namespace OwnChar.Api.Packets.General;
public class GetSingleObjectResponse<T>(T? result) : OwnCharResponse where T : OwnCharObject
{
public T? Result { get; } = result;
}

View File

@@ -0,0 +1,8 @@
using OwnChar.Api.Packets;
namespace OwnChar.Api.Packets.General;
public class GetSinlgeObjectRequest(long objectId) : OwnCharRequest
{
public long ObjectId { get; } = objectId;
}

View File

@@ -0,0 +1,10 @@
using OwnChar.Api.Packets;
using Pilz.Cryptography;
namespace OwnChar.Api.Packets.General;
public class LoginRequest(string username, SecureString password) : OwnCharRequest
{
public new string Username { get; set; } = username;
public SecureString Password { get; } = password;
}

View File

@@ -0,0 +1,11 @@
using OwnChar.Api.Packets;
using OwnChar.Data.Model.Client;
namespace OwnChar.Api.Packets.General;
public class LoginResponse(UserAccount? account, UserProfile? profile, string? secret) : OwnCharResponse
{
public UserAccount? Account { get; } = account;
public UserProfile? Profile { get; } = profile;
public string? Secret { get; } = secret;
}

View File

@@ -0,0 +1,7 @@
namespace OwnChar.Api.Packets.General;
public class SetOwnerRequest : OwnCharRequest
{
public long ObjectId { get; set; }
public bool Enforce { get; set; }
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Api.Packets;
using OwnChar.Api.Updates;
namespace OwnChar.Api.Packets.General;
public class UpdateRequest(OwnCharObjectUpdate update) : OwnCharRequest
{
public OwnCharObjectUpdate Update { get; } = update;
}

View File

@@ -0,0 +1,8 @@
using OwnChar.Api.Packets;
namespace OwnChar.Api.Packets.Groups;
public class CreateGroupRequest : OwnCharRequest
{
public string? Name { get; set; }
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Api.Packets;
using OwnChar.Data.Model.Client;
namespace OwnChar.Api.Packets.Groups;
public class CreateGroupResponse(Group? group) : OwnCharResponse
{
public Group? Group { get; } = group;
}

View File

@@ -0,0 +1,8 @@
using OwnChar.Api.Packets;
namespace OwnChar.Api.Packets.Groups;
public class GetGroupMembersRequest(long groupId) : OwnCharRequest
{
public long GroupId { get; } = groupId;
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Data;
using OwnChar.Data.Model.Client;
namespace OwnChar.Api.Packets.Groups;
public class GetGroupMembersResponse(List<MemberEntry> members) : OwnCharResponse
{
public List<MemberEntry> Members { get; } = members;
}

View File

@@ -0,0 +1,10 @@
using OwnChar.Api.Packets;
namespace OwnChar.Api.Packets.Groups;
public class GetGroupsRequest : OwnCharRequest
{
public long ProfileId { get; set; } = -1;
public bool UseProfileId { get; set; }
public bool IncludeNonPublic { get; set; }
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Api.Packets;
using OwnChar.Data.Model.Client;
namespace OwnChar.Api.Packets.Groups;
public class GetGroupsResponse(List<Group> groups) : OwnCharResponse
{
public List<Group> Groups { get; } = groups;
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Data;
namespace OwnChar.Api.Packets.Groups;
public class GroupMemberAddRequest() : OwnCharRequest
{
public long GroupId { get; set; } = -1;
public Dictionary<long, MemberLevel> Members { get; set; } = [];
}

View File

@@ -0,0 +1,8 @@
using OwnChar.Data.Model.Client;
namespace OwnChar.Api.Packets.Groups;
public class GroupMemberAddResponse(List<MemberEntry> addedMembers) : OwnCharResponse
{
public List<MemberEntry> AddedMembers { get; } = addedMembers;
}

View File

@@ -0,0 +1,7 @@
using Pilz.Net.Api;
namespace OwnChar.Api.Packets;
public class OwnCharRequest : ApiMessage
{
}

View File

@@ -0,0 +1,7 @@
using Pilz.Net.Api;
namespace OwnChar.Api.Packets;
public class OwnCharResponse : ApiMessage
{
}

View File

@@ -0,0 +1,8 @@
using OwnChar.Api.Packets;
namespace OwnChar.Api.Packets.UserProfiles;
public class GetUserProfileRequest(long userProfileId) : OwnCharRequest
{
public long UserProfileId { get; } = userProfileId;
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Api.Packets;
using OwnChar.Data.Model.Client;
namespace OwnChar.Api.Packets.UserProfiles;
public class GetUserProfileResponse(UserProfile? profile) : OwnCharResponse
{
public UserProfile? Profile { get; } = profile;
}

View File

@@ -0,0 +1,8 @@
using OwnChar.Api.Packets;
namespace OwnChar.Api.Packets.UserProfiles;
public class GetUserProfilesRequest : OwnCharRequest
{
public bool AllProfiles { get; set; }
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Api.Packets;
using OwnChar.Data.Model.Client;
namespace OwnChar.Api.Packets.UserProfiles;
public class GetUserProfilesResponse(List<UserProfile> profiles) : OwnCharResponse
{
public List<UserProfile> Profiles { get; } = profiles;
}

View File

@@ -0,0 +1,19 @@
using OwnChar.Data.Model.Client;
namespace OwnChar.Api.Updates;
public class GroupUpdate : OwnCharObjectUpdate
{
public string? Name { get; set; }
public string? Fandom { get; set; }
public GroupUpdate() : base()
{
}
public GroupUpdate(Group group) : base(group)
{
Name = group.Name;
Fandom = group.Fandom;
}
}

View File

@@ -0,0 +1,9 @@
using OwnChar.Data;
namespace OwnChar.Api.Updates;
public class MemberUpdate : OwnCharObjectUpdate
{
public long GroupId { get; set; }
public MemberLevel Level { get; set; }
}

View File

@@ -0,0 +1,17 @@
using OwnChar.Data;
namespace OwnChar.Api.Updates;
public abstract class OwnCharObjectUpdate
{
public long Id { get; set; }
public OwnCharObjectUpdate()
{
}
public OwnCharObjectUpdate(OwnCharObject obj) : this()
{
Id = obj.Id;
}
}

3
OwnChar/AssemblyInfo.cs Normal file
View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("OwnChar.Server")]

View File

@@ -1,30 +0,0 @@
using OwnChar.Model;
namespace OwnChar.Data;
public interface IDataManager
{
// Login
abstract UserAccount? Login(string username, string password);
abstract bool Logout(UserAccount? account);
// User management
abstract UserAccount? CreateUserAccount(string username, string password);
abstract UserProfile? GetUserProfile(UserAccount account);
abstract bool DeleteUserAccount(UserAccount account);
// Group management
abstract UserProfile? GetOwner(UserAccount account, Group group);
abstract IEnumerable<UserProfile>? GetMembers(UserAccount account, Group group);
abstract bool AddMember(UserAccount account, Group group, UserProfile user);
abstract bool RemoveMember(UserAccount account, Group group, UserProfile user);
abstract Group? CreateGroup(UserAccount account, string name);
abstract bool DeleteGroup(UserAccount account, Group group);
// Character management
abstract UserProfile? GetOwner(UserAccount account, Character group);
abstract IEnumerable<Character>? GetCharacters(UserAccount account, Group group);
abstract IEnumerable<Character>? GetCharacters(UserAccount account, UserProfile profile);
abstract Character? CreateCharacter(UserAccount account, string name, Group? group);
abstract bool DeleteCharacter(UserAccount account, Character character);
}

View File

@@ -1,36 +0,0 @@
using OwnChar.Model;
namespace OwnChar.Data;
public interface IDataProvider
{
// General
abstract bool IsInitialized();
abstract void SetInitialized();
abstract bool SaveDatabase();
// Model
abstract T? Create<T>() where T : class, IOwnCharObject;
abstract bool Save<T>(T obj) where T : class, IOwnCharObject;
abstract bool Delete<T>(T obj) where T : class, IOwnCharObject;
// Hierarchy
abstract bool SetParent(UserProfile profile, UserAccount parent);
abstract bool SetParent(Character character, Group parent);
abstract bool SetParent(Property property, Character character);
abstract bool SetOwner(Group group, UserProfile owner);
abstract bool SetOwner(Character group, UserProfile owner);
// Gets
abstract UserAccount? GetUserAccount(string username, string password);
abstract UserProfile? GetUserProfile(string username);
abstract IEnumerable<UserProfile>? GetMembers(Group group);
abstract UserProfile? GetOwner(Group group);
abstract UserProfile? GetOwner(Character character);
abstract IEnumerable<Character>? GetCharacters(Group group);
abstract IEnumerable<Character>? GetCharacters(UserProfile jprofile);
// Sets
abstract bool AddMember(Group group, UserProfile user);
abstract bool RemoveMember(Group group, UserProfile user);
}

View File

@@ -1,5 +0,0 @@
namespace OwnChar.Data.Managers;
public class ClientDataManager
{
}

View File

@@ -1,179 +0,0 @@
using OwnChar.Model;
namespace OwnChar.Data.Managers;
public class DefaultDataManager : IDataManager
{
private const string defaultUsername = "admin";
private const string defaultPassword = "admin";
public IDataProvider DataProvider { get; }
public DefaultDataManager(IDataProvider dataProvider)
{
DataProvider = dataProvider;
Initialize(false);
}
public IEnumerable<UserProfile>? GetMembers(UserAccount account, Group group)
{
if (!account.HasPermission(UserType.Guest))
return null;
return DataProvider.GetMembers(group);
}
public UserProfile? GetOwner(UserAccount account, Group group)
{
if (!account.HasPermission(UserType.Guest))
return null;
return DataProvider.GetOwner(group);
}
public UserProfile? GetOwner(UserAccount account, Character character)
{
if (!account.HasPermission(UserType.Guest))
return null;
return DataProvider.GetOwner(character);
}
public UserProfile? GetUserProfile(UserAccount account)
{
ArgumentException.ThrowIfNullOrWhiteSpace(account.Username, nameof(account.Username));
return DataProvider.GetUserProfile(account.Username);
}
public UserAccount? Login(string username, string password)
{
return DataProvider.GetUserAccount(username, password);
}
public bool Logout(UserAccount? account)
{
if (account != null && account.HasPermission(UserType.User))
DataProvider.SaveDatabase();
return true;
}
public bool Initialize(bool force)
{
var result = false;
if (force || !DataProvider.IsInitialized())
{
result = CreateUserAccount(defaultUsername, Utils.HashPassword(defaultUsername, defaultPassword)) != null;
DataProvider.SetInitialized();
}
return result;
}
public UserAccount? CreateUserAccount(string username, string password)
{
var account = DataProvider.Create<UserAccount>();
var profile = DataProvider.Create<UserProfile>();
var group = DataProvider.Create<Group>();
ArgumentNullException.ThrowIfNull(account, nameof(account));
ArgumentNullException.ThrowIfNull(profile, nameof(profile));
ArgumentNullException.ThrowIfNull(group, nameof(group));
account.Username = username;
account.Password = password;
profile.Name = username;
DataProvider.SetParent(profile, account);
group.IsInternal = true;
DataProvider.SetOwner(group, profile);
DataProvider.Save(account);
DataProvider.Save(profile);
DataProvider.Save(group);
return account;
}
public bool DeleteUserAccount(UserAccount account)
{
if (!string.IsNullOrWhiteSpace(account.Username) && DataProvider.GetUserProfile(account.Username) is UserProfile userProfile)
userProfile.Name = "Deleted user";
DataProvider.Delete(account);
return true;
}
public IEnumerable<Character>? GetCharacters(UserAccount account, Group group)
{
if (!account.HasPermission(UserType.Guest))
return null;
return DataProvider.GetCharacters(group);
}
public IEnumerable<Character>? GetCharacters(UserAccount account, UserProfile profile)
{
if (!account.HasPermission(UserType.Guest))
return null;
return DataProvider.GetCharacters(profile);
}
public bool AddMember(UserAccount account, Group group, UserProfile user)
{
if (GetUserProfile(account) is not UserProfile profile || DataProvider.GetOwner(group) is not UserProfile owner || !account.HasPermission(profile == owner ? UserType.User : UserType.Admin))
return false;
return DataProvider.AddMember(group, user);
}
public bool RemoveMember(UserAccount account, Group group, UserProfile user)
{
if (GetUserProfile(account) is not UserProfile profile || DataProvider.GetOwner(group) is not UserProfile owner || !account.HasPermission(profile == owner ? UserType.User : UserType.Admin))
return false;
return DataProvider.RemoveMember(group, user);
}
public Group? CreateGroup(UserAccount account, string name)
{
if (!account.HasPermission(UserType.User) || GetUserProfile(account) is not UserProfile profile || DataProvider.Create<Group>() is not Group group)
return null;
group.Name = name;
DataProvider.Save(group);
DataProvider.SetOwner(group, profile);
return group;
}
public bool DeleteGroup(UserAccount account, Group group)
{
if (GetUserProfile(account) is not UserProfile profile || DataProvider.GetOwner(group) is not UserProfile owner || !account.HasPermission(profile == owner ? UserType.User : UserType.Admin))
return false;
return DataProvider.Delete(group);
}
public Character? CreateCharacter(UserAccount account, string name, Group? group)
{
if (!account.HasPermission(UserType.User) || GetUserProfile(account) is not UserProfile profile || DataProvider.Create<Character>() is not Character character)
return null;
character.Name = name;
DataProvider.Save(character);
DataProvider.SetOwner(character, profile);
if (group != null)
DataProvider.SetParent(character, group);
return character;
}
public bool DeleteCharacter(UserAccount account, Character character)
{
if (GetUserProfile(account) is not UserProfile profile || DataProvider.GetOwner(character) is not UserProfile owner || !account.HasPermission(profile == owner ? UserType.User : UserType.Admin))
return false;
DataProvider.Delete(character);
return true;
}
}

View File

@@ -0,0 +1,10 @@
namespace OwnChar.Data;
public enum MemberLevel
{
None,
Guest,
Member,
Admin,
Owner,
}

View File

@@ -1,6 +1,6 @@
namespace OwnChar.Model; namespace OwnChar.Data.Model.Base;
public abstract class Character : IOwnCharObject public abstract class CharacterBase : OwnCharObject
{ {
public virtual string? Name { get; set; } public virtual string? Name { get; set; }
public virtual string? Fandom { get; set; } public virtual string? Fandom { get; set; }

View File

@@ -0,0 +1,8 @@
namespace OwnChar.Data.Model.Base;
public abstract class GroupBase : OwnCharObject
{
public virtual string? Name { get; set; }
public virtual string? Fandom { get; set; }
public virtual bool IsInternal { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace OwnChar.Data.Model.Base;
public abstract class MemberEntryBase : OwnCharObject
{
public virtual MemberLevel Level { get; set; }
}

View File

@@ -1,6 +1,6 @@
namespace OwnChar.Model; namespace OwnChar.Data.Model.Base;
public abstract class Property : IOwnCharObject public abstract class PropertyBase : OwnCharObject
{ {
public virtual string? Name { get; set; } public virtual string? Name { get; set; }
public virtual object? Value { get; set; } public virtual object? Value { get; set; }

View File

@@ -0,0 +1,6 @@
namespace OwnChar.Data.Model.Base;
public abstract class PropertyCategoryBase : OwnCharObject
{
public virtual string? Name { get; set; }
}

View File

@@ -1,9 +1,8 @@
namespace OwnChar.Model; namespace OwnChar.Data.Model.Base;
public abstract class UserAccount : IOwnCharObject public abstract class UserAccountBase : OwnCharObject
{ {
public virtual string? Username { get; set; } public virtual string? Username { get; set; }
public virtual string? Password { get; set; }
public virtual string? Email { get; set; } public virtual string? Email { get; set; }
public virtual UserType Type { get; set; } public virtual UserType Type { get; set; }
} }

View File

@@ -0,0 +1,6 @@
namespace OwnChar.Data.Model.Base;
public abstract class UserProfileBase : OwnCharObject
{
public virtual string? Name { get; set; }
}

View File

@@ -0,0 +1,13 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Client;
public class Character() : CharacterBase
{
internal Character(CharacterBase character) : this()
{
Id = character.Id;
Name = character.Name;
Fandom = character.Fandom;
}
}

View File

@@ -0,0 +1,7 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Client;
public class Group : GroupBase
{
}

View File

@@ -0,0 +1,8 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Client;
public class MemberEntry() : MemberEntryBase
{
public long UserProfileId { get; set; }
}

View File

@@ -0,0 +1,13 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Client;
public class Property() : PropertyBase
{
internal Property(PropertyBase prop) : this()
{
Id = prop.Id;
Name = prop.Name;
Value = prop.Value;
}
}

View File

@@ -0,0 +1,12 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Client;
public class PropertyCategory() : PropertyCategoryBase
{
internal PropertyCategory(PropertyCategoryBase propCat) : this()
{
Id = propCat.Id;
Name = propCat.Name;
}
}

View File

@@ -0,0 +1,7 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Client;
public class UserAccount() : UserAccountBase
{
}

View File

@@ -0,0 +1,7 @@
using OwnChar.Data.Model.Base;
namespace OwnChar.Data.Model.Client;
public class UserProfile() : UserProfileBase
{
}

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,6 @@
namespace OwnChar.Data;
public class OwnCharObject
{
public virtual long Id { get; set; }
}

View File

@@ -1,11 +0,0 @@
using OwnChar.Data.Providers.JsonFile.Model;
namespace OwnChar.Data.Providers.JsonFile;
public class JsonFile
{
public bool IsInitialized { get; set; }
public List<JsonUserAccount> UserAccounts { get; } = [];
public List<JsonCharacter> Characters { get; } = [];
public List<JsonGroup> Groups { get; } = [];
}

View File

@@ -1,265 +0,0 @@
using Newtonsoft.Json;
using OwnChar.Data.Providers.JsonFile.Model;
using OwnChar.Model;
namespace OwnChar.Data.Providers.JsonFile;
public class JsonFileDataProvider : IDataProvider
{
public JsonFile JsonFile { get; protected set; }
public string JsonFilePath { get; protected set; }
public JsonFileDataProvider(string filePath)
{
JsonFilePath = filePath;
LoadFile();
// Get rid of bad compiler warnings
if (JsonFile == null)
throw new Exception("Something went incredible wrong at initialization of the JsonFile.");
}
protected void LoadFile()
{
if (File.Exists(JsonFilePath) && JsonConvert.DeserializeObject<JsonFile>(File.ReadAllText(JsonFilePath), CreateJsonSerializerSettings()) is JsonFile jsonFile)
JsonFile = jsonFile;
JsonFile ??= new();
}
protected void SaveFile()
{
File.WriteAllText(JsonFilePath, JsonConvert.SerializeObject(JsonFile, CreateJsonSerializerSettings()));
}
protected JsonSerializerSettings CreateJsonSerializerSettings()
{
return new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.All,
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
TypeNameHandling = TypeNameHandling.Auto,
Formatting = Formatting.Indented,
};
}
public T? Create<T>() where T : class, IOwnCharObject
{
var t = typeof(T);
IOwnCharObject? obj;
if (t == typeof(Property))
obj = new JsonProp();
else if (t == typeof(PropertyCategory))
obj = new JsonPropCat();
else if (t == typeof(Character))
obj = new JsonCharacter();
else if (t == typeof(Group))
obj = new JsonGroup();
else if (t == typeof(UserAccount))
obj = new JsonUserAccount();
else if (t == typeof(UserProfile))
obj = new JsonUserProfile();
else
obj = null;
return obj as T;
}
public bool Save<T>(T obj) where T : class, IOwnCharObject
{
if (obj is JsonCharacter character)
{
if (!JsonFile.Characters.Contains(character))
JsonFile.Characters.Add(character);
}
else if (obj is JsonGroup group)
{
if (!JsonFile.Groups.Contains(group))
JsonFile.Groups.Add(group);
}
else if (obj is JsonUserAccount account)
{
if (!JsonFile.UserAccounts.Contains(account))
JsonFile.UserAccounts.Add(account);
}
return true;
}
public bool IsInitialized()
{
return JsonFile.IsInitialized;
}
public void SetInitialized()
{
JsonFile.IsInitialized = true;
}
public bool Delete<T>(T obj) where T : class, IOwnCharObject
{
if (obj is JsonCharacter character)
{
JsonFile.Groups.ForEach(n => n.Characters.Remove(character));
return true;
}
else if (obj is JsonProp prop)
{
JsonFile.Characters.ForEach(n => n.Properties.Remove(prop));
return true;
}
else if (obj is JsonPropCat propCat)
{
JsonFile.Characters.ForEach(n => n.Properties.ForEach(m =>
{
if (m.Category == propCat)
m.Category = null;
}));
JsonFile.Characters.ForEach(n => n.PropertyCategories.Remove(propCat));
return true;
}
else if (obj is JsonGroup group)
{
JsonFile.Groups.Remove(group);
return true;
}
else if (obj is JsonUserAccount account)
{
JsonFile.UserAccounts.Remove(account);
return true;
}
else if (obj is JsonUserProfile profile)
{
// We don't delete profiles at the moment!
profile.Name = "Deleted user";
return true;
}
return false;
}
public bool SetParent(UserProfile profile, UserAccount parent)
{
if (parent is JsonUserAccount jaccount && profile is JsonUserProfile jprofile)
{
jaccount.Profile = jprofile;
return true;
}
return false;
}
public bool SetParent(Character character, Group parent)
{
if (character is JsonCharacter jcharacter && parent is JsonGroup jgroup)
{
if (!jgroup.Characters.Contains(jcharacter))
jgroup.Characters.Add(jcharacter);
return true;
}
return false;
}
public bool SetParent(Property property, Character character)
{
if (property is JsonProp jprop && character is JsonCharacter jcharacter)
{
if (!jcharacter.Properties.Contains(jprop))
jcharacter.Properties.Add(jprop);
return true;
}
return false;
}
public bool SetOwner(Group group, UserProfile owner)
{
if (group is JsonGroup jgroup && owner is JsonUserProfile jprofile)
{
jgroup.Owner = jprofile;
return true;
}
return false;
}
public bool SetOwner(Character character, UserProfile owner)
{
if (character is JsonCharacter jcharacter && owner is JsonUserProfile jprofile)
{
jcharacter.Owner = jprofile;
return true;
}
return false;
}
public UserAccount? GetUserAccount(string username, string password)
{
ArgumentException.ThrowIfNullOrWhiteSpace(username, nameof(username));
ArgumentException.ThrowIfNullOrWhiteSpace(password, nameof(password));
return JsonFile.UserAccounts.FirstOrDefault(n => n.Username == username && n.Password == password);
}
public UserProfile? GetUserProfile(string username)
{
return JsonFile.UserAccounts.FirstOrDefault(n => n.Username == username)?.Profile;
}
public IEnumerable<UserProfile>? GetMembers(Group group)
{
if (group is JsonGroup jgroup)
return jgroup.Members;
return null;
}
public UserProfile? GetOwner(Group group)
{
if (group is JsonGroup jgroup)
return jgroup.Owner;
return null;
}
public UserProfile? GetOwner(Character character)
{
if (character is JsonCharacter jcharacter)
return jcharacter.Owner;
return null;
}
public IEnumerable<Character>? GetCharacters(Group group)
{
if (group is JsonGroup jgroup)
return jgroup.Characters;
return null;
}
public IEnumerable<Character>? GetCharacters(UserProfile profile)
{
if (profile is UserProfile jprofile)
return JsonFile.Characters.Where(n => n.Owner == profile);
return null;
}
public bool AddMember(Group group, UserProfile user)
{
if (group is JsonGroup jgroup && user is JsonUserProfile jprofile)
{
if (!jgroup.Members.Contains(jprofile))
jgroup.Members.Add(jprofile);
return true;
}
return false;
}
public bool RemoveMember(Group group, UserProfile user)
{
if (group is JsonGroup jgroup && user is JsonUserProfile jprofile)
{
jgroup.Members.Remove(jprofile);
return true;
}
return false;
}
public bool SaveDatabase()
{
SaveFile();
return true;
}
}

View File

@@ -1,10 +0,0 @@
using OwnChar.Model;
namespace OwnChar.Data.Providers.JsonFile.Model;
public class JsonCharacter : Character
{
public virtual JsonUserProfile? Owner { get; set; }
public virtual List<JsonProp> Properties { get; } = [];
public virtual List<JsonPropCat> PropertyCategories { get; } = [];
}

View File

@@ -1,10 +0,0 @@
using OwnChar.Model;
namespace OwnChar.Data.Providers.JsonFile.Model;
public class JsonGroup : Group
{
public virtual JsonUserProfile? Owner { get; set; }
public virtual List<JsonUserProfile> Members { get; } = [];
public virtual List<JsonCharacter> Characters { get; } = [];
}

View File

@@ -1,8 +0,0 @@
using OwnChar.Model;
namespace OwnChar.Data.Providers.JsonFile.Model;
public class JsonProp : Property
{
public virtual JsonPropCat? Category { get; set; } = null;
}

View File

@@ -1,7 +0,0 @@
using OwnChar.Model;
namespace OwnChar.Data.Providers.JsonFile.Model;
public class JsonPropCat : PropertyCategory
{
}

View File

@@ -1,13 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using OwnChar.Model;
namespace OwnChar.Data.Providers.JsonFile.Model;
public class JsonUserAccount : UserAccount
{
public virtual JsonUserProfile? Profile { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public override UserType Type { get => base.Type; set => base.Type = value; }
}

View File

@@ -1,7 +0,0 @@
using OwnChar.Model;
namespace OwnChar.Data.Providers.JsonFile.Model;
public class JsonUserProfile : UserProfile
{
}

9
OwnChar/Data/UserType.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace OwnChar.Data;
public enum UserType
{
None,
Guest,
User,
Admin,
}

View File

@@ -1,11 +0,0 @@
using OwnChar.Model;
namespace OwnChar;
public static class Extensions
{
public static bool HasPermission(this UserAccount account, UserType permissions)
{
return account.Type >= permissions;
}
}

View File

@@ -0,0 +1,11 @@
using Pilz.Net.Api;
namespace OwnChar.Extensions;
public static class ApiRequestInfoExtensions
{
public static string? GetUsername(this ApiRequestInfo @this)
{
return @this.AuthKey?.Split(':').ElementAtOrDefault(0);
}
}

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

View File

@@ -0,0 +1,23 @@
using OwnChar.Api;
using OwnChar.Data.Model.Client;
using OwnChar.Modules;
using Pilz.Cryptography;
namespace OwnChar;
public interface IOwnCharManager
{
bool IsLoggedIn { get; }
IOwnCharApiClient? Api { get; }
IUserManager Users { get; }
IGroupsManager Groups { get; }
ICharacterManager Characters { get; }
Task<UserProfile?> Login(string? username, SecureString? password);
Task<bool> Logout();
}

39
OwnChar/JsonHelpers.cs Normal file
View File

@@ -0,0 +1,39 @@
using Newtonsoft.Json;
using OwnChar.Api.Packets;
namespace OwnChar;
public static class JsonHelpers
{
private static JsonSerializerSettings? defaultSerializerSettings;
public static JsonSerializerSettings DefaultSerializerSettings => defaultSerializerSettings ??= CreateDefaultSerializerSettings();
private static JsonSerializerSettings CreateDefaultSerializerSettings()
{
return new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
};
}
public static string? SerializeRequest<T>(T request) where T : OwnCharRequest
{
return JsonConvert.SerializeObject(request, DefaultSerializerSettings);
}
public static string? SerializeResponse<T>(T request) where T : OwnCharResponse
{
return JsonConvert.SerializeObject(request, DefaultSerializerSettings);
}
public static T? DeserializeRequest<T>(string request) where T : OwnCharRequest
{
return JsonConvert.DeserializeObject<T>(request, DefaultSerializerSettings);
}
public static T? DeserializeResponse<T>(string request) where T : OwnCharResponse
{
return JsonConvert.DeserializeObject<T>(request, DefaultSerializerSettings);
}
}

View File

@@ -1,51 +0,0 @@
using OwnChar.Model;
namespace OwnChar.Manager;
public class CharacterManager(OwnCharManager manager)
{
public OwnCharManager Manager { get; } = manager;
public IEnumerable<Character>? GetCharacters(Group? group)
{
Manager.CheckLogin();
if (group != null)
return Manager.DataManager?.GetCharacters(Manager.CurrentUser!, group);
return null;
}
public IEnumerable<Character>? GetCharacters(UserProfile? profile)
{
Manager.CheckLogin();
if (profile != null)
return Manager.DataManager?.GetCharacters(Manager.CurrentUser!, profile);
return null;
}
public Character? CreateCharacter(string? name)
{
return CreateCharacter(name, null);
}
public Character? CreateCharacter(string? name, Group? destination)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));
Manager.CheckLogin();
return Manager.DataManager?.CreateCharacter(Manager.CurrentUser!, name, destination);
}
public bool DeleteCharacter(Character? character)
{
ArgumentNullException.ThrowIfNull(character, nameof(character));
Manager.CheckLogin();
return Manager.DataManager?.DeleteCharacter(Manager.CurrentUser!, character) ?? false;
}
}

View File

@@ -1,50 +0,0 @@
using OwnChar.Model;
namespace OwnChar.Manager;
public class GroupsManager(OwnCharManager manager)
{
public OwnCharManager Manager { get; } = manager;
public UserProfile? GetOwner(Group? group)
{
ArgumentNullException.ThrowIfNull(group, nameof(group));
return Manager.DataManager?.GetOwner(Manager.CurrentUser!, group);
}
public IEnumerable<UserProfile>? GetMembers(Group? group)
{
ArgumentNullException.ThrowIfNull(group, nameof(group));
return Manager.DataManager?.GetMembers(Manager.CurrentUser!, group);
}
public bool AddMember(UserProfile? profile, Group? group)
{
Manager.CheckLogin();
ArgumentNullException.ThrowIfNull(profile, nameof(profile));
ArgumentNullException.ThrowIfNull(group, nameof(group));
return Manager.DataManager?.AddMember(Manager.CurrentUser!, group, profile) ?? false;
}
public bool DeleteMember(UserProfile? profile, Group? group)
{
Manager.CheckLogin();
ArgumentNullException.ThrowIfNull(profile, nameof(profile));
ArgumentNullException.ThrowIfNull(group, nameof(group));
return Manager.DataManager?.RemoveMember(Manager.CurrentUser!, group, profile) ?? false;
}
public Group? CreateGroup(string? name)
{
Manager.CheckLogin();
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));
return Manager.DataManager?.CreateGroup(Manager.CurrentUser!, name);
}
public bool DeleteGroup(Group? group)
{
Manager.CheckLogin();
ArgumentNullException.ThrowIfNull(group, nameof(group));
return Manager.DataManager?.DeleteGroup(Manager.CurrentUser!, group) ?? false;
}
}

View File

@@ -1,60 +0,0 @@
using OwnChar.Data;
using OwnChar.Manager.Exceptions;
using OwnChar.Model;
using Pilz.Cryptography;
namespace OwnChar.Manager;
public class OwnCharManager
{
// User
public bool IsLoggedIn => CurrentUser != null;
public UserAccount? CurrentUser { get; private set; }
// Data Provider
public IDataManager? DataManager { get; set; }
// Manager
public UserManager Users { get; }
public GroupsManager Groups { get; }
public CharacterManager Characters { get; }
public OwnCharManager()
{
Users = new(this);
Groups = new(this);
Characters = new(this);
}
internal protected void CheckLogin()
{
if (!IsLoggedIn)
throw new LoginException("You are already logged in!");
}
/// <summary>
/// Tries to login on the given data provider.
/// </summary>
/// <returns>Returns <see cref="true"/> if the login was successfull and <see cref="false"/> if not.</returns>
public bool Login(IDataManager? proxy, string? username, SecureString? password)
{
ArgumentNullException.ThrowIfNull(proxy, nameof(proxy));
ArgumentException.ThrowIfNullOrWhiteSpace(username, nameof(username));
ArgumentException.ThrowIfNullOrWhiteSpace(password, nameof(password));
username = username.Trim().ToLower();
CurrentUser = proxy.Login(username, Utils.HashPassword(username, password));
DataManager = proxy;
return IsLoggedIn;
}
/// <summary>
/// Closes the session on the current data provider.
/// </summary>
/// <returns></returns>
public bool Logout()
{
return DataManager?.Logout(CurrentUser) ?? true;
}
}

View File

@@ -1,29 +0,0 @@
using OwnChar.Model;
using Pilz.Cryptography;
namespace OwnChar.Manager;
public class UserManager(OwnCharManager manager)
{
public OwnCharManager Manager { get; } = manager;
public UserProfile? GetOwnUserProfile()
{
Manager.CheckLogin();
return Manager.DataManager!.GetUserProfile(Manager.CurrentUser!);
}
public UserAccount? CreateUserAccount(string? username, SecureString? password)
{
ArgumentException.ThrowIfNullOrWhiteSpace(username, nameof(username));
ArgumentException.ThrowIfNullOrWhiteSpace(password, nameof(password));
username = username.Trim().ToLower();
return Manager.DataManager?.CreateUserAccount(username, Utils.HashPassword(username, password));
}
public bool DeleteUserAccount(UserAccount? account)
{
ArgumentNullException.ThrowIfNull(account, nameof(account));
return Manager.DataManager?.DeleteUserAccount(account) ?? false;
}
}

View File

@@ -1,8 +0,0 @@
namespace OwnChar.Model;
public abstract class Group : IOwnCharObject
{
public virtual string? Name { get; set; }
public virtual string? Fandom { get; set; }
public virtual bool IsInternal { get; set; }
}

View File

@@ -1,5 +0,0 @@
namespace OwnChar.Model;
public interface IOwnCharObject
{
}

View File

@@ -1,6 +0,0 @@
namespace OwnChar.Model;
public abstract class PropertyCategory : IOwnCharObject
{
public virtual string? Name { get; set; }
}

View File

@@ -1,6 +0,0 @@
namespace OwnChar.Model;
public abstract class UserProfile : IOwnCharObject
{
public virtual string? Name { get; set; }
}

View File

@@ -1,8 +0,0 @@
namespace OwnChar.Model;
public enum UserType
{
Guest,
User,
Admin
}

View File

@@ -0,0 +1,53 @@
using OwnChar.Data;
using OwnChar.Model;
namespace OwnChar.Modules;
internal class CharacterManager(OwnCharManager manager) : OwnCharManagerModule(manager), ICharacterManager
{
public IQueryable<CharacterBase>? GetCharacters(GroupBase? group)
{
Manager.CheckLogin();
if (group != null)
return Manager.DataManager.ExecuteAction(DataManagerActions.Getter.Character, DataManagerActionType.Get, Manager.CurrentUser, group).Result as IQueryable<CharacterBase>;
return null;
}
public IQueryable<CharacterBase>? GetCharacters(UserProfileBase? profile)
{
Manager.CheckLogin();
if (profile != null)
return Manager.DataManager.ExecuteAction(DataManagerActions.Getter.Character, DataManagerActionType.Get, Manager.CurrentUser, profile).Result as IQueryable<CharacterBase>;
return null;
}
public CharacterBase? CreateCharacter(string? name)
{
return CreateCharacter(name, null);
}
public CharacterBase? CreateCharacter(string? name, GroupBase? destination)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Getter.PropertyCategory, DataManagerActionType.Set, Manager.CurrentUser, null, name, destination).Result as CharacterBase;
}
public bool DeleteCharacter(CharacterBase? character)
{
ArgumentNullException.ThrowIfNull(character, nameof(character));
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Delete, DataManagerActionType.Default, Manager.CurrentUser, character).HasSuccess;
}
public UserProfileBase? GetOwner(CharacterBase? character)
{
ArgumentNullException.ThrowIfNull(character, nameof(character));
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Association.Owner, DataManagerActionType.Get, Manager.CurrentUser, character).Result as UserProfileBase;
}
}

View File

@@ -0,0 +1,61 @@
namespace OwnChar.Modules;
internal class GroupsManager(OwnCharManager manager) : OwnCharManagerModule(manager), IGroupsManager
{
public UserProfileBase? GetOwner(GroupBase? group)
{
ArgumentNullException.ThrowIfNull(group, nameof(group));
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Association.Owner, DataManagerActionType.Get, Manager.CurrentUser, group).Result as UserProfileBase;
}
public IQueryable<GroupBase>? GetGroups(UserProfileBase? profile)
{
ArgumentNullException.ThrowIfNull(profile, nameof(profile));
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Getter.Group, DataManagerActionType.Get, Manager.CurrentUser, profile) as IQueryable<GroupBase>;
}
public IQueryable<GroupBase>? GetGroups()
{
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Getter.Group, DataManagerActionType.Get, Manager.CurrentUser, null) as IQueryable<GroupBase>;
}
public IQueryable<UserProfileBase>? GetMembers(GroupBase? group)
{
ArgumentNullException.ThrowIfNull(group, nameof(group));
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Association.Members, DataManagerActionType.Get, Manager.CurrentUser, group).Result as IQueryable<UserProfileBase>;
}
public bool AddMember(UserProfileBase? profile, GroupBase? group)
{
ArgumentNullException.ThrowIfNull(profile, nameof(profile));
ArgumentNullException.ThrowIfNull(group, nameof(group));
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Association.Members, DataManagerActionType.Set, Manager.CurrentUser, group, profile).HasSuccess;
}
public bool RemoveMember(UserProfileBase? profile, GroupBase? group)
{
ArgumentNullException.ThrowIfNull(profile, nameof(profile));
ArgumentNullException.ThrowIfNull(group, nameof(group));
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Association.Members, DataManagerActionType.Delete, Manager.CurrentUser, group, profile).HasSuccess;
}
public GroupBase? CreateGroup(string? name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Getter.Group, DataManagerActionType.Set, Manager.CurrentUser, null, name).Result as GroupBase;
}
public bool DeleteGroup(GroupBase? group)
{
ArgumentNullException.ThrowIfNull(group, nameof(group));
Manager.CheckLogin();
return Manager.DataManager.ExecuteAction(DataManagerActions.Delete, DataManagerActionType.Default, Manager.CurrentUser, group).HasSuccess;
}
}

View File

@@ -0,0 +1,12 @@
using OwnChar.Model;
namespace OwnChar.Modules;
public interface ICharacterManager
{
CharacterBase? CreateCharacter(string? name);
CharacterBase? CreateCharacter(string? name, GroupBase? destination);
bool DeleteCharacter(CharacterBase? character);
IQueryable<CharacterBase>? GetCharacters(GroupBase? group);
IQueryable<CharacterBase>? GetCharacters(UserProfileBase? profile);
UserProfileBase? GetOwner(CharacterBase? character);
}

View File

@@ -0,0 +1,14 @@
using OwnChar.Model;
namespace OwnChar.Modules;
public interface IGroupsManager
{
IQueryable<GroupBase>? GetGroups(UserProfileBase? profile);
IQueryable<GroupBase>? GetGroups();
bool AddMember(UserProfileBase? profile, GroupBase? group);
GroupBase? CreateGroup(string? name);
bool DeleteGroup(GroupBase? group);
bool RemoveMember(UserProfileBase? profile, GroupBase? group);
IQueryable<UserProfileBase>? GetMembers(GroupBase? group);
UserProfileBase? GetOwner(GroupBase? group);
}

View File

@@ -0,0 +1,12 @@
using OwnChar.Model;
using Pilz.Cryptography;
namespace OwnChar.Modules;
public interface IUserManager
{
IQueryable<UserAccountBase>? GetUserAccounts();
UserAccountBase? CreateUserAccount(string? username, SecureString? password);
bool DeleteUserAccount(UserAccountBase? account);
UserProfileBase? GetOwnUserProfile();
}

Some files were not shown because too many files have changed in this diff Show More