mirror of
https://gitea.tendokyu.moe/Kayori/Medusa.net.git
synced 2026-10-10 08:18:09 +03:00
Added front end login and beginnen of card mangment page. Also cleaned up the main server endpoints and added docker file
This commit is contained in:
@@ -6,7 +6,7 @@ namespace Abstractions.Entities
|
||||
public class User: IdentityUser<int>
|
||||
{
|
||||
public int PaseliAmount { get; set; } = 0;
|
||||
public int Pin { get; init; }
|
||||
public string Pin { get; init; } = "0000";
|
||||
|
||||
public List<Card> Cards { get; init; } = null!;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Abstractions.Entities;
|
||||
|
||||
@@ -10,5 +11,9 @@ namespace Abstractions.Services
|
||||
string ConvertKonamiIdToUid(string konamiId);
|
||||
Task<Card?> FindByKonamiId(string konamiId);
|
||||
Task<Card?> FindByCardId(string cardId);
|
||||
Task<bool> Exists(string konamiId);
|
||||
Task<bool> ValidatePinAsync(string konamiId, string pin);
|
||||
Task<bool> IsRegistered(string konamiId, string pin);
|
||||
Task<List<Card>> GetCardsByUserIdAsync(int userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
namespace Abstractions.Services
|
||||
using Abstractions.Entities;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Abstractions.Services
|
||||
{
|
||||
public interface IUserService
|
||||
{
|
||||
Task<User?> GetUserByKonamiId(string konamiId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<StartupCommand>npm run dev</StartupCommand>
|
||||
<JavaScriptTestRoot>.\</JavaScriptTestRoot>
|
||||
<JavaScriptTestFramework>Jest</JavaScriptTestFramework>
|
||||
<JavaScriptTestFramework>Vitest</JavaScriptTestFramework>
|
||||
<!-- Allows the build (or compile) script located on package.json to run on Build -->
|
||||
<ShouldRunBuildScript>false</ShouldRunBuildScript>
|
||||
<!-- Folder where production build objects will be placed -->
|
||||
|
||||
+10
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "client",
|
||||
@@ -9,10 +10,12 @@
|
||||
"axios": "^1.10.0",
|
||||
"oh-vue-icons": "^1.0.0-rc3",
|
||||
"pinia": "^3.0.3",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss-primeui": "^0.6.1",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vite-plugin-vue-layouts-next": "^1.0.0",
|
||||
"vue": "^3.4.29",
|
||||
"vue-router": "4",
|
||||
@@ -600,6 +603,8 @@
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"primeicons": ["primeicons@7.0.0", "", {}, "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="],
|
||||
|
||||
"primevue": ["primevue@4.3.6", "", { "dependencies": { "@primeuix/styled": "^0.7.0", "@primeuix/styles": "^1.2.0", "@primeuix/utils": "^0.6.0", "@primevue/core": "4.3.6", "@primevue/icons": "4.3.6" } }, "sha512-Wwg2dH6pBmOdkj9L/OnrCQf9AKPHfY5CcfnDyWeh0tNlR+XjYKGl8qvMdJOvGO9jjg6UdsX5MSaU8vDDsSG+sg=="],
|
||||
|
||||
"property-expr": ["property-expr@2.0.6", "", {}, "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="],
|
||||
@@ -674,7 +679,7 @@
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="],
|
||||
|
||||
@@ -692,6 +697,8 @@
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vee-validate": ["vee-validate@4.15.1", "", { "dependencies": { "@vue/devtools-api": "^7.5.2", "type-fest": "^4.8.3" }, "peerDependencies": { "vue": "^3.4.26" } }, "sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg=="],
|
||||
|
||||
"vite": ["vite@5.4.11", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q=="],
|
||||
|
||||
"vite-plugin-vue-layouts-next": ["vite-plugin-vue-layouts-next@1.0.0", "", { "dependencies": { "debug": "^4.4.1", "fast-glob": "^3.3.3" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.4", "vue-router": "^4.0.11" } }, "sha512-y3oNYcJs5lrv4F/E0EXU7OXi88CH1/b/DOk9nsFAeglxmk4Ps635DTy9R29Nl9Vj7LteBRKaxOkKVSHKOMqo7A=="],
|
||||
@@ -796,6 +803,8 @@
|
||||
|
||||
"vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
|
||||
|
||||
"yup/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
Generated
+33
@@ -13,10 +13,12 @@
|
||||
"axios": "^1.10.0",
|
||||
"oh-vue-icons": "^1.0.0-rc3",
|
||||
"pinia": "^3.0.3",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss-primeui": "^0.6.1",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vite-plugin-vue-layouts-next": "^1.0.0",
|
||||
"vue": "^3.4.29",
|
||||
"vue-router": "4",
|
||||
@@ -4089,6 +4091,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/primeicons": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
||||
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/primevue": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.3.6.tgz",
|
||||
@@ -4701,6 +4709,31 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vee-validate": {
|
||||
"version": "4.15.1",
|
||||
"resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.15.1.tgz",
|
||||
"integrity": "sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.5.2",
|
||||
"type-fest": "^4.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.4.26"
|
||||
}
|
||||
},
|
||||
"node_modules/vee-validate/node_modules/type-fest": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
"axios": "^1.10.0",
|
||||
"oh-vue-icons": "^1.0.0-rc3",
|
||||
"pinia": "^3.0.3",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss-primeui": "^0.6.1",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vite-plugin-vue-layouts-next": "^1.0.0",
|
||||
"vue": "^3.4.29",
|
||||
"vue-router": "4",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss-primeui";
|
||||
|
||||
|
||||
/* Primary and Surface Palettes */
|
||||
:root {
|
||||
--p-primary-50: #ecfdf5;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/store/userStore';
|
||||
import Menu from '@/volt/Menu.vue';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const items = ref([
|
||||
{ label: 'Home', icon: 'pi pi-home' , command: () => router.push('/') },
|
||||
{ label: 'Card management', icon: 'pi pi-id-card' , command: () => router.push('/cards') },
|
||||
{ label: 'logout', icon: 'pi pi-sign-out', command: () => {
|
||||
const userStore = useUserStore();
|
||||
userStore.unsetAccessToken();
|
||||
userStore.unsetUser();
|
||||
userStore.unsetRefreshToken();
|
||||
userStore.unsetLoggedIn();
|
||||
sessionStorage.removeItem("accessToken");
|
||||
localStorage.removeItem("refreshToken");
|
||||
router.push('/auth');
|
||||
} },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-center w-2xs h-dvh rounded-none! bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-0 shadow-md"
|
||||
data-pc-name="card" pc4="" data-pc-section="root">
|
||||
<div class="p-5 flex flex-col gap-2 h-full" data-pc-section="body">
|
||||
<div class="flex flex-col gap-2" data-pc-section="caption">
|
||||
<div class="font-medium text-xl" data-pc-section="title">Medusa.net</div>
|
||||
</div>
|
||||
<div class="" data-pc-section="content">
|
||||
<Menu :model="items" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,121 @@
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
import type { AuthenticationResponse } from "@/types/authenticationResponse";
|
||||
import type { LoginRequest } from "@/types/loginRequest";
|
||||
import type { TokenResponse } from "@/types/tokenResponse";
|
||||
import { UserService } from "./userService";
|
||||
|
||||
export class AuthenticationService {
|
||||
static async refreshAccessToken(refreshToken: string, userId: string) {
|
||||
return false;
|
||||
static async refreshAccessToken(refreshToken: string) {
|
||||
const store = useUserStore();
|
||||
const response = await fetch("/api/auth/refresh", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
refreshToken: refreshToken
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
const tokenResponse = (await response.json()) as TokenResponse;
|
||||
|
||||
sessionStorage.setItem("accessToken", tokenResponse.accessToken);
|
||||
localStorage.setItem("refreshToken", tokenResponse.refreshToken);
|
||||
|
||||
store.setAccessToken(tokenResponse.accessToken);
|
||||
store.setRefreshToken(tokenResponse.refreshToken);
|
||||
store.setLoggedIn(true);
|
||||
|
||||
if (await UserService.GetUserClaims()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static async login(
|
||||
loginRequest: LoginRequest,
|
||||
): Promise<AuthenticationResponse> {
|
||||
const response = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(loginRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const json = await response.json();
|
||||
if (json.toString().includes(
|
||||
"Invalid email or password",
|
||||
)
|
||||
) {
|
||||
return {
|
||||
twoFactorRequired: false,
|
||||
incorrectCredentials: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const tokenResponse = (await response.json()) as TokenResponse;
|
||||
|
||||
sessionStorage.setItem("accessToken", tokenResponse.accessToken);
|
||||
localStorage.setItem("refreshToken", tokenResponse.refreshToken);
|
||||
|
||||
const store = useUserStore();
|
||||
|
||||
store.setAccessToken(tokenResponse.accessToken);
|
||||
store.setRefreshToken(tokenResponse.refreshToken);
|
||||
store.setLoggedIn(true);
|
||||
|
||||
if (await UserService.GetUserClaims()) {
|
||||
return {
|
||||
twoFactorRequired: false,
|
||||
incorrectCredentials: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
twoFactorRequired: true,
|
||||
incorrectCredentials: true,
|
||||
};
|
||||
}
|
||||
|
||||
static async registerUser(
|
||||
konamiId: string,
|
||||
pinCode: string,
|
||||
username: string,
|
||||
email: string,
|
||||
password: string,
|
||||
) {
|
||||
const response = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
konamiId,
|
||||
pin: pinCode,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(
|
||||
await this.login({
|
||||
email: email,
|
||||
password: password,
|
||||
twoFactorCode: undefined,
|
||||
twoFactorRecoveryCode: undefined,
|
||||
})
|
||||
).incorrectCredentials;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useUserStore } from "@/store/userStore";
|
||||
import type { UserClaim } from "@/types/userClaim";
|
||||
|
||||
export class UserService {
|
||||
static async GetUserClaims(): Promise<boolean> {
|
||||
const response = await fetch("/api/user/claims", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${sessionStorage.getItem("accessToken")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
const userClaims = (await response.json()) as UserClaim[];
|
||||
|
||||
const store = useUserStore();
|
||||
store.setUser(userClaims);
|
||||
store.setName(
|
||||
userClaims.find((claim) => claim.type === "name")?.value || "",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import Sidebar from '@/components/Sidebar.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
<div class="flex gap-4 h-full w-full">
|
||||
<Sidebar />
|
||||
<main class="w-full h-dvh flex items-center">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,4 +1,5 @@
|
||||
import './assets/main.css'
|
||||
import 'primeicons/primeicons.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import PrimeVue from 'primevue/config';
|
||||
|
||||
@@ -1,18 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { Form } from '@primevue/forms';
|
||||
import { Form, type FormSubmitEvent } from '@primevue/forms';
|
||||
import Card from '@/volt/Card.vue';
|
||||
import Button from '@/volt/Button.vue';
|
||||
import Message from '@/volt/Message.vue';
|
||||
import InputText from "@/volt/InputText.vue";
|
||||
import { useRouter } from 'vue-router';
|
||||
import * as yup from 'yup';
|
||||
import { yupResolver } from '@primevue/forms/resolvers/yup';
|
||||
import { ref } from 'vue';
|
||||
import { AuthenticationService } from '@/data/authenticationService';
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const resolver = yupResolver(
|
||||
yup.object({
|
||||
email: yup.string().required('Username or Email is required'),
|
||||
password: yup.string().required('Password is required'),
|
||||
})
|
||||
);
|
||||
|
||||
const onFormSubmit = async (event: FormSubmitEvent<Record<string, any>>) => {
|
||||
loading.value = true;
|
||||
if (!event.valid) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await AuthenticationService.login({ email: event.values.email!, password: event.values.password!, twoFactorCode: undefined, twoFactorRecoveryCode: undefined });
|
||||
loading.value = false;
|
||||
if (!result.incorrectCredentials) {
|
||||
router.push('/');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<route lang="yaml">
|
||||
@@ -23,26 +50,32 @@ meta:
|
||||
<template>
|
||||
<div>
|
||||
<Card class="p-24 pt-16 pb-16">
|
||||
<template #title>Login</template>
|
||||
<template #title>Login</template>
|
||||
|
||||
<template #content>
|
||||
<Form class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="email">Username or Email</label>
|
||||
<InputText id="email" name="email" />
|
||||
<template #content>
|
||||
<Form v-slot="$form" class="flex flex-col gap-4" :resolver="resolver" :validate-on-value-update="true"
|
||||
@submit="onFormSubmit">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="email">Username or Email</label>
|
||||
<InputText id="email" name="email" />
|
||||
<Message v-if="$form.email?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.email.error.message }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="password">Password</label>
|
||||
<InputText id="password" type="password" name="password" />
|
||||
<Message v-if="$form.password?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.password.error.message }}</Message>
|
||||
</div>
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="flex gap-4 mt-1 w-full">
|
||||
<Button @click="() => router.push('/auth/register')" label="Register" severity="secondary"
|
||||
variant="outlined" class="w-full" />
|
||||
<Button type="submit" label="Login" class="w-full" :loading="loading" loading-icon="ProgressSpinner">Login</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="password">Password</label>
|
||||
<InputText id="password" name="password" />
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex gap-4 mt-1">
|
||||
<Button @click="() => router.push('/auth/register')" label="Register" severity="secondary" variant="outlined" class="w-full" />
|
||||
<Button label="Login" class="w-full">Login</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,65 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { Form } from '@primevue/forms';
|
||||
import Card from '@/volt/Card.vue';
|
||||
import Button from '@/volt/Button.vue';
|
||||
import InputText from "@/volt/InputText.vue";
|
||||
import InputNumber from "@/volt/InputNumber.vue";
|
||||
import { useRouter } from 'vue-router';
|
||||
<script setup lang="ts">
|
||||
import { Form, type FormSubmitEvent } from '@primevue/forms';
|
||||
import Card from '@/volt/Card.vue';
|
||||
import Button from '@/volt/Button.vue';
|
||||
import Message from '@/volt/Message.vue';
|
||||
import InputText from "@/volt/InputText.vue";
|
||||
import { useRouter } from 'vue-router';
|
||||
import * as yup from 'yup';
|
||||
import { yupResolver } from '@primevue/forms/resolvers/yup';
|
||||
import { ref } from 'vue';
|
||||
import { AuthenticationService } from '@/data/authenticationService';
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
},
|
||||
})
|
||||
definePage({
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const cardResponse = ref<{ success: boolean; message: string }>({ success: false, message: 'Waiting for card' });
|
||||
|
||||
const resolver = yupResolver(
|
||||
yup.object({
|
||||
cardNumber: yup.string().length(16, 'Card number must be 16 digits').required('Card number is required')
|
||||
.test('validate-card', '', async function (value) {
|
||||
const { pinCode } = this.parent;
|
||||
const response = await validateCard(value!, pinCode!);
|
||||
cardResponse.value = response;
|
||||
return response.success;
|
||||
}),
|
||||
pinCode: yup
|
||||
.string()
|
||||
.required('Pin code is required')
|
||||
.matches(/^\d{4}$/, 'Pin code must be exactly 4 digits (0000–9999)')
|
||||
.test('validate-pin', '', async function (value) {
|
||||
const { cardNumber } = this.parent;
|
||||
const response = await validateCard(cardNumber!, value!);
|
||||
cardResponse.value = response;
|
||||
return response.success;
|
||||
}),
|
||||
username: yup.string().min(3, 'Username must be at least 3 characters').required('Username is required'),
|
||||
email: yup.string().email('Invalid email format').required('Email is required'),
|
||||
password: yup.string().required('Password is required').min(8, 'Password must be at least 6 characters')
|
||||
.matches(/(?=.*\d)/, 'Password must contain a number')
|
||||
.matches(/(?=.*[a-z])/, 'Password must contain a lowercase letter')
|
||||
.matches(/(?=.*[A-Z])/, 'Password must contain an uppercase letter')
|
||||
.matches(/(?=.*\W)/, 'Password must contain a special character'),
|
||||
passwordConfirmation: yup.string()
|
||||
.oneOf([yup.ref('password')], 'Passwords must match')
|
||||
.required('Password confirmation is required'),
|
||||
})
|
||||
);
|
||||
|
||||
const validateCard = async (cardNumber: string, pinCode: string) => {
|
||||
const response = await fetch('/api/cards/validate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ cardId: cardNumber, pin: pinCode }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, message: 'Failed to validate card' };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const onFormSubmit = async (event: FormSubmitEvent<Record<string, any>>) => {
|
||||
loading.value = true;
|
||||
console.log('Form Submitted', event);
|
||||
if (!event.valid) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
const result = await AuthenticationService.registerUser(event.values.cardNumber, event.values.pinCode, event.values.username, event.values.email, event.values.password);
|
||||
loading.value = false;
|
||||
if (result) {
|
||||
router.push('/auth/login');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: auth
|
||||
</route>
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: auth
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Card class="p-24 pt-16 pb-16">
|
||||
<template #title>Register</template>
|
||||
<template>
|
||||
<div>
|
||||
<Card class="p-24 pt-16 pb-16">
|
||||
<template #title>Register</template>
|
||||
|
||||
<template #content>
|
||||
<Form class="flex flex-col gap-4">
|
||||
<div class="flex gap-4">
|
||||
<template #content>
|
||||
<Form v-slot="$form" class="flex flex-col gap-4" :resolver="resolver" :validate-on-value-update="true"
|
||||
@submit="onFormSubmit">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="cardNumber">16 digit Card Number</label>
|
||||
<InputText id="cardNumber" name="cardNumber" />
|
||||
<Message v-if="$form.cardNumber?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.cardNumber.error.message }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="pinCode">Pin code</label>
|
||||
<InputText id="pinCode" name="pinCode" :format="false" />
|
||||
<Message v-if="$form.pinCode?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.pinCode.error.message }}</Message>
|
||||
</div>
|
||||
</div>
|
||||
<Message :severity="cardResponse?.success ? 'success' : 'warn'">{{ cardResponse?.message }}</Message>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="cardNumber">Card Number</label>
|
||||
<InputText id="cardNumber" name="cardNumber" />
|
||||
<label for="username">Username</label>
|
||||
<InputText id="username" name="username" />
|
||||
<Message v-if="$form.username?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.username.error.message }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="pinCode">Pin code</label>
|
||||
<InputNumber id="pinCode" name="pinCode" :format="false" max="9999"/>
|
||||
<label for="email">Email</label>
|
||||
<InputText id="email" name="email" />
|
||||
<Message v-if="$form.email?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.email.error.message }}</Message>
|
||||
</div>
|
||||
</div>
|
||||
<p>Card status:</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="username">Username</label>
|
||||
<InputText id="username" name="username" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="email">Email</label>
|
||||
<InputText id="email" name="email" />
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="password">Password</label>
|
||||
<InputText id="password" name="password" />
|
||||
<div class="flex gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="password">Password</label>
|
||||
<InputText id="password" type="password" name="password" />
|
||||
<Message v-if="$form.password?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.password.error.message }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="passwordConfirmation">Confirm Password</label>
|
||||
<InputText type="password" id="passwordConfirmation" name="passwordConfirmation" />
|
||||
<Message v-if="$form.passwordConfirmation?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.passwordConfirmation.error.message }}</Message>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="passwordConfirmation">Confirm Password</label>
|
||||
<InputText id="passwordConfirmation" name="passwordConfirmation" />
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="flex gap-4 mt-1 w-full">
|
||||
<Button @click="() => router.push('/auth/login')" label="Login" severity="secondary" variant="outlined"
|
||||
class="w-full" />
|
||||
<Button :loading="loading" type="submit" label="Register" class="w-full" loading-icon="ProgressSpinner">Register</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import Card from '@/volt/Card.vue';
|
||||
import DataTable from '@/volt/DataTable.vue';
|
||||
import Column from 'primevue/column';
|
||||
|
||||
import type { Card as MedusaCard } from '@/types/card';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useUserStore } from '@/store/userStore';
|
||||
|
||||
const cards = ref<MedusaCard[] | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
const store = useUserStore();
|
||||
fetch('/api/cards/my',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${store.accessToken}`,
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
cards.value = data;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex justify-center">
|
||||
<Card class="p-4 pt-6 pb-6">
|
||||
<template #title>Manage your cards</template>
|
||||
<template #content>
|
||||
<div v-if="cards === null">
|
||||
<p>Loading cards...</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<DataTable :value="cards" pt:table="min-w-200">
|
||||
<Column field="konamiId" header="KonamiId"></Column>
|
||||
<Column field="rawId" header="Raw Id"></Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,11 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Card from '@/volt/Card.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex justify-center">
|
||||
<Card class="p-4 pt-6 pb-6">
|
||||
<template #title>Welcome to Medusa.net</template>
|
||||
|
||||
<template #content>
|
||||
<p class="mb-16">This is the home page. Use the sidebar to navigate through the application.</p>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
@@ -15,17 +15,11 @@ router.beforeEach(async (to, from, next) => {
|
||||
let store = useUserStore();
|
||||
let user = store.user;
|
||||
|
||||
//dont let people go back to the splash page
|
||||
if (to.path === "/" && from.path !== "/") {
|
||||
next(from);
|
||||
return;
|
||||
}
|
||||
//if the user is not logged in, redirect them to the welcome page
|
||||
const requiresAuth =
|
||||
to.meta.requiresAuth !== undefined && to.meta.requiresAuth === false;
|
||||
if (!requiresAuth && user === undefined) {
|
||||
const refreshToken = localStorage.getItem("refreshToken") as string;
|
||||
const userId = localStorage.getItem("userId") as string;
|
||||
if (!refreshToken) {
|
||||
next("/auth");
|
||||
return;
|
||||
@@ -33,12 +27,11 @@ router.beforeEach(async (to, from, next) => {
|
||||
|
||||
//i think store gets reset when you switch between apps. So we need to rehydrate the store
|
||||
const success = await AuthenticationService.refreshAccessToken(
|
||||
refreshToken,
|
||||
userId
|
||||
refreshToken
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
next("/");
|
||||
next("/auth");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,46 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { UserClaim } from "@/types/userClaim";
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => {
|
||||
return {
|
||||
user: undefined as UserClaim[] | undefined,
|
||||
refreshToken: undefined as string | undefined,
|
||||
accessToken: undefined as string | undefined,
|
||||
name: undefined as string | undefined,
|
||||
profilePicture: undefined as string | undefined,
|
||||
notificationCount: 0
|
||||
};
|
||||
export const useUserStore = defineStore("user", {
|
||||
state: () => {
|
||||
return {
|
||||
user: undefined as UserClaim[] | undefined,
|
||||
refreshToken: undefined as string | undefined,
|
||||
accessToken: undefined as string | undefined,
|
||||
isLoggedIn: false,
|
||||
name: undefined as string | undefined,
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
setUser(user: UserClaim[]) {
|
||||
this.user = user;
|
||||
},
|
||||
actions: {
|
||||
setUser(user: UserClaim[]) {
|
||||
this.user = user;
|
||||
},
|
||||
setRefreshToken(refreshToken: string) {
|
||||
this.refreshToken = refreshToken;
|
||||
},
|
||||
setAccessToken(accessToken: string) {
|
||||
this.accessToken = accessToken;
|
||||
},
|
||||
setName(name: string) {
|
||||
this.name = name;
|
||||
},
|
||||
setProfilePicture(profilePicture: string) {
|
||||
this.profilePicture = profilePicture;
|
||||
},
|
||||
setNotificationCount(count: number) {
|
||||
this.notificationCount = count;
|
||||
},
|
||||
unsetUser() {
|
||||
this.user = undefined;
|
||||
},
|
||||
unsetRefreshToken() {
|
||||
this.refreshToken = undefined;
|
||||
},
|
||||
unsetAccessToken() {
|
||||
this.accessToken = undefined;
|
||||
},
|
||||
unsetName() {
|
||||
this.name = undefined;
|
||||
},
|
||||
unsetProfilePicture() {
|
||||
this.profilePicture = undefined;
|
||||
},
|
||||
resetNotificationCount() {
|
||||
this.notificationCount = 0;
|
||||
}
|
||||
}
|
||||
})
|
||||
setRefreshToken(refreshToken: string) {
|
||||
this.refreshToken = refreshToken;
|
||||
},
|
||||
setAccessToken(accessToken: string) {
|
||||
this.accessToken = accessToken;
|
||||
},
|
||||
setLoggedIn(isLoggedIn: boolean) {
|
||||
this.isLoggedIn = isLoggedIn;
|
||||
},
|
||||
setName(name: string) {
|
||||
this.name = name;
|
||||
},
|
||||
unsetUser() {
|
||||
this.user = undefined;
|
||||
},
|
||||
unsetRefreshToken() {
|
||||
this.refreshToken = undefined;
|
||||
},
|
||||
unsetAccessToken() {
|
||||
this.accessToken = undefined;
|
||||
},
|
||||
unsetLoggedIn() {
|
||||
this.isLoggedIn = false;
|
||||
},
|
||||
unsetName() {
|
||||
this.name = undefined;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export type AuthenticationResponse = {
|
||||
incorrectCredentials: boolean;
|
||||
twoFactorRequired: boolean;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export type Card = {
|
||||
konamiId: string;
|
||||
rawId: string;
|
||||
userId: number;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export type LoginRequest = {
|
||||
email: string;
|
||||
password: string;
|
||||
twoFactorCode: string | null | undefined;
|
||||
twoFactorRecoveryCode: string | null | undefined;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export type TokenResponse = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<Menu
|
||||
ref="el"
|
||||
unstyled
|
||||
:pt="theme"
|
||||
:ptOptions="{
|
||||
mergeProps: ptViewMerge
|
||||
}"
|
||||
>
|
||||
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
|
||||
<slot :name="slotName" v-bind="slotProps ?? {}" />
|
||||
</template>
|
||||
</Menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Menu, { type MenuPassThroughOptions, type MenuProps } from 'primevue/menu';
|
||||
import { ref } from 'vue';
|
||||
import { ptViewMerge } from './utils';
|
||||
|
||||
interface Props extends /* @vue-ignore */ MenuProps {}
|
||||
defineProps<Props>();
|
||||
|
||||
const theme = ref<MenuPassThroughOptions>({
|
||||
root: `bg-surface-0 dark:bg-surface-900
|
||||
text-surface-700 dark:text-surface-0
|
||||
border border-surface-200 dark:border-surface-700
|
||||
rounded-md min-w-52
|
||||
p-popup:shadow-[0_4px_6px_-1px_rgba(0,0,0,0.1),0_2px_4px_-2px_rgba(0,0,0,0.1)]`,
|
||||
list: `m-0 p-1 list-none outline-none flex flex-col gap-[2px]`,
|
||||
item: `p-disabled:opacity-60 p-disabled:pointer-events-none`,
|
||||
itemContent: `group transition-colors duration-200 rounded-sm text-surface-700 dark:text-surface-0
|
||||
p-focus:bg-surface-100 dark:p-focus:bg-surface-800 p-focus:text-surface-800 dark:p-focus:text-surface-0
|
||||
hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-800 dark:hover:text-surface-0`,
|
||||
itemLink: `cursor-pointer flex items-center no-underline overflow-hidden relative text-inherit
|
||||
px-3 py-2 gap-2 select-none outline-none`,
|
||||
itemIcon: `text-surface-400 dark:text-surface-500
|
||||
p-focus:text-surface-500 dark:p-focus:text-surface-400
|
||||
group-hover:text-surface-500 dark:group-hover:text-surface-400`,
|
||||
itemLabel: ``,
|
||||
submenuLabel: `bg-transparent px-3 py-2 text-surface-500 dark:text-surface-400 font-semibold`,
|
||||
separator: `border-t border-surface-200 dark:border-surface-700`,
|
||||
transition: {
|
||||
enterFromClass: 'opacity-0 scale-y-75',
|
||||
enterActiveClass: 'transition duration-120 ease-[cubic-bezier(0,0,0.2,1)]',
|
||||
leaveActiveClass: 'transition-opacity duration-100 ease-linear',
|
||||
leaveToClass: 'opacity-0'
|
||||
}
|
||||
});
|
||||
|
||||
const el = ref();
|
||||
defineExpose({
|
||||
toggle: (event) => el.value.toggle(event)
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<Message
|
||||
unstyled
|
||||
:pt="theme"
|
||||
:ptOptions="{
|
||||
mergeProps: ptViewMerge
|
||||
}"
|
||||
>
|
||||
<template #closeicon>
|
||||
<TimesIcon />
|
||||
</template>
|
||||
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
|
||||
<slot :name="slotName" v-bind="slotProps ?? {}" />
|
||||
</template>
|
||||
</Message>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TimesIcon from '@primevue/icons/times';
|
||||
import Message, { type MessagePassThroughOptions, type MessageProps } from 'primevue/message';
|
||||
import { ref } from 'vue';
|
||||
import { ptViewMerge } from './utils';
|
||||
|
||||
interface Props extends /* @vue-ignore */ MessageProps {}
|
||||
defineProps<Props>();
|
||||
|
||||
const theme = ref<MessagePassThroughOptions>({
|
||||
root: `rounded-md outline outline-1
|
||||
p-outlined:bg-transparent p-outlined:outline p-outlined:outline-1
|
||||
p-simple:bg-transparent p-simple:outline-none
|
||||
p-info:bg-blue-50/95 p-info:outline-blue-200 p-info:text-blue-600 dark:p-info:bg-blue-500/15 dark:p-info:outline-blue-700/35 dark:p-info:text-blue-500
|
||||
p-info:p-outlined:text-blue-500 p-info:p-outlined:outline-blue-500 dark:p-info:p-outlined:text-blue-600 dark:p-info:p-outlined:outline-blue-600
|
||||
p-info:p-simple:text-blue-500 dark:p-info:p-simple:text-blue-600
|
||||
p-success:bg-green-50/95 p-success:outline-green-200 p-success:text-green-600 dark:p-success:bg-green-500/15 dark:p-success:outline-green-700/35 dark:p-success:text-green-500
|
||||
p-success:p-outlined:text-green-500 p-success:p-outlined:outline-green-500 dark:p-success:p-outlined:text-green-600 dark:p-success:p-outlined:outline-green-600
|
||||
p-success:p-simple:text-green-500 dark:p-success:p-simple:text-green-600
|
||||
p-warn:bg-yellow-50/95 p-warn:outline-yellow-200 p-warn:text-yellow-600 dark:p-warn:bg-yellow-500/15 dark:p-warn:outline-yellow-700/35 dark:p-warn:text-yellow-500
|
||||
p-warn:p-outlined:text-yellow-500 p-warn:p-outlined:outline-yellow-500 dark: p-warn:p-outlined:text-yellow-600 dark:p-warn:p-outlined:outline-yellow-600
|
||||
p-warn:p-simple:text-yellow-500 dark:p-warn:p-simple:text-yellow-600
|
||||
p-error:bg-red-50/95 p-error:outline-red-200 p-error:text-red-600 dark:p-error:bg-red-500/15 dark:p-error:outline-red-700/35 dark:p-error:text-red-500
|
||||
p-error:p-outlined:text-red-500 p-error:p-outlined:outline-red-500 dark:p-error:p-outlined:text-red-600 dark:p-error:p-outlined:outline-red-600
|
||||
p-error:p-simple:text-red-500 dark:p-error:p-simple:text-red-600
|
||||
p-secondary:bg-surface-100 p-secondary:outline-surface-200 p-secondary:text-surface-600 dark:p-secondary:bg-surface-800 dark:p-secondary:outline-surface-700 dark:p-secondary:text-surface-300
|
||||
p-secondary:p-outlined:text-surface-500 p-secondary:p-outlined:outline-surface-500 dark:p-secondary:p-outlined:text-surface-400 dark:p-secondary:p-outlined:outline-surface-400
|
||||
p-secondary:p-simple:text-surface-500 dark:p-secondary:p-simple:text-surface-400
|
||||
p-contrast:bg-surface-900 p-contrast:outline-surface-950 p-contrast:text-surface-50 dark:p-contrast:bg-surface-0 dark:p-contrast:outline-surface-100 dark:p-contrast:text-surface-950
|
||||
p-contrast:p-outlined:text-surface-950 p-contrast:p-outlined:outline-surface-950 dark:p-contrast:p-outlined:text-surface-0 dark:p-contrast:p-outlined:outline-surface-0
|
||||
p-contrast:p-simple:text-surface-950 dark:p-contrast:p-simple:text-surface-0`,
|
||||
content: `flex items-center
|
||||
p-simple:p-0
|
||||
px-3 py-2 gap-2 h-full
|
||||
p-small:px-2.5 p-small:py-[0.375rem]
|
||||
p-large:px-3.5 p-large:py-2.5`,
|
||||
icon: `flex-shrink-0 text-lg w-[1.125rem] h-[1.125rem]
|
||||
p-small:w-[0.875rem] p-small:h-[0.875rem] p-small:text-sm
|
||||
p-large:w-5 p-large:h-5 p-large:text-xl`,
|
||||
text: `text-base font-medium p-small:text-sm p-large:text-xl`,
|
||||
closeButton: `flex items-center justify-center flex-shrink-0 ms-auto overflow-hidden relative cursor-pointer select-none
|
||||
w-7 h-7 rounded-full bg-transparent transition-colors duration-200 text-inherit p-0 border-none
|
||||
focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2
|
||||
p-info:hover:bg-blue-100 p-info:focus-visible:outline-blue-600 dark:p-info:hover:bg-white/5 dark:p-info:focus-visible:outline-blue-500
|
||||
p-success:hover:bg-green-100 p-success:focus-visible:outline-green-600 dark:p-success:hover:bg-white/5 dark:p-success:focus-visible:outline-green-500
|
||||
p:warn:hover:bg-yellow-100 p:warn:focus-visible:outline-yellow-600 dark:p:warn:hover:bg-white/5 dark:p:warn:focus-visible:outline-yellow-500
|
||||
p-error:hover:bg-red-100 p-error:focus-visible:outline-red-600 dark:p-error:hover:bg-white/5 dark:p-error:focus-visible:outline-red-500
|
||||
p-secondary:hover:bg-surface-200 p-secondary:focus-visible:outline-surface-600 dark:p-secondary:hover:bg-surface-700 dark:p-secondary:focus-visible:outline-surface-300
|
||||
p-contrast:hover:bg-surface-800 p-contrast:focus-visible:outline-surface-50 dark:p-contrast:hover:bg-surface-100 dark:p-contrast:focus-visible:outline-surface-950
|
||||
p-outlined:hover:bg-transparent p-simple:hover:bg-transparent`,
|
||||
closeIcon: `w-4 h-4 text-base
|
||||
p-small:w-3.5 p-small:h-3.5 p-small:text-sm
|
||||
p-large:w-[1.125rem] p-large:h-[1.125rem] p-large:text-xl`,
|
||||
transition: {
|
||||
enterFromClass: 'opacity-0',
|
||||
enterActiveClass: 'transition-opacity duration-300',
|
||||
leaveFromClass: 'max-h-40',
|
||||
leaveActiveClass: 'overflow-hidden transition-all duration-300 ease-in',
|
||||
leaveToClass: 'max-h-0 opacity-0 !m-0'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Vendored
+1
@@ -21,5 +21,6 @@ declare module 'vue-router/auto-routes' {
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||
'/auth/': RouteRecordInfo<'/auth/', '/auth', Record<never, never>, Record<never, never>>,
|
||||
'/auth/register': RouteRecordInfo<'/auth/register', '/auth/register', Record<never, never>, Record<never, never>>,
|
||||
'/cards/': RouteRecordInfo<'/cards/', '/cards', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,10 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'^/weatherforecast': {
|
||||
'^/api': {
|
||||
target,
|
||||
secure: false
|
||||
}
|
||||
},
|
||||
},
|
||||
port: 5173,
|
||||
https: {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using Abstractions.Services;
|
||||
using Facet.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Server.Models.Request;
|
||||
using Server.Models.Response;
|
||||
|
||||
namespace Server.Api;
|
||||
|
||||
public static class CardsApi
|
||||
{
|
||||
public static RouteGroupBuilder MapCardsApiEndpoints(this RouteGroupBuilder group)
|
||||
{
|
||||
var cardsGroup = group.MapGroup("/cards").WithTags("Cards");
|
||||
cardsGroup.MapPost("/validate", ValidateCard).Produces<ValidateCardResponse>(200);
|
||||
cardsGroup.MapGet("/my", GetUserCards).Produces<CardDto[]>(200).RequireAuthorization();
|
||||
return group;
|
||||
}
|
||||
|
||||
internal static async Task<IResult> ValidateCard(ValidateCardRequest request, [FromServices] ICardService cardService)
|
||||
{
|
||||
var isRegistered = await cardService.IsRegistered(request.CardId, request.Pin);
|
||||
if (isRegistered)
|
||||
{
|
||||
return Results.Ok(new ValidateCardResponse
|
||||
{
|
||||
Message = "Card already registered.",
|
||||
Success = false
|
||||
});
|
||||
}
|
||||
|
||||
var exists = await cardService.Exists(request.CardId);
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
return Results.Ok(new ValidateCardResponse
|
||||
{
|
||||
Message = "Card does not exist.",
|
||||
Success = false
|
||||
});
|
||||
}
|
||||
|
||||
var validPin = await cardService.ValidatePinAsync(request.CardId, request.Pin);
|
||||
|
||||
if (validPin)
|
||||
{
|
||||
return Results.Ok(new ValidateCardResponse
|
||||
{
|
||||
Message = "Card is valid.",
|
||||
Success = true
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(new ValidateCardResponse
|
||||
{
|
||||
Message = "Card does not exist.",
|
||||
Success = false
|
||||
});
|
||||
}
|
||||
|
||||
internal static async Task<IResult> GetUserCards(HttpContext httpContext, [FromServices] ICardService cardService)
|
||||
{
|
||||
var user = httpContext.User;
|
||||
if (!(httpContext.User.Identity?.IsAuthenticated ?? false))
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
var userId = user.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value;
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
var cards = await cardService.GetCardsByUserIdAsync(int.Parse(userId));
|
||||
|
||||
return Results.Ok(cards.SelectFacets<CardDto>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Server.Api;
|
||||
|
||||
public static class UserApi
|
||||
{
|
||||
public static RouteGroupBuilder MapUserApiEndpoints(this RouteGroupBuilder group)
|
||||
{
|
||||
var userGroup = group.MapGroup("/user").WithTags("User");
|
||||
|
||||
userGroup.MapGet("/claims", GetClaims).Produces<Claim[]>(200).RequireAuthorization();
|
||||
return group;
|
||||
}
|
||||
|
||||
internal static IResult GetClaims(HttpContext httpContext)
|
||||
{
|
||||
var user = httpContext.User;
|
||||
if(!(httpContext.User.Identity?.IsAuthenticated ?? false))
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var claims = user.Claims.Select(c => new Claim(c.Type, c.Value)).ToArray();
|
||||
return Results.Ok(claims);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using Abstractions.Entities;
|
||||
using Abstractions.Services;
|
||||
using Microsoft.AspNetCore.Authentication.BearerToken;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Http.Metadata;
|
||||
@@ -31,13 +32,16 @@ public static class AuthEndpoints
|
||||
// We'll figure out a unique endpoint name based on the final route pattern during endpoint generation.
|
||||
string? confirmEmailEndpointName = null;
|
||||
|
||||
var routeGroup = endpoints.MapGroup("");
|
||||
var routeGroup = endpoints.MapGroup("/auth");
|
||||
|
||||
routeGroup.MapPost("/register", async Task<Results<Ok, ValidationProblem>>
|
||||
([FromBody] RegisterRequest registration, HttpContext context, [FromServices] IServiceProvider sp) =>
|
||||
([FromBody] Models.Request.RegisterRequest registration, HttpContext context, [FromServices] IServiceProvider sp) =>
|
||||
{
|
||||
var userManager = sp.GetRequiredService<UserManager<User>>();
|
||||
|
||||
var signinManager = sp.GetRequiredService<SignInManager<User>>();
|
||||
var userService = sp.GetRequiredService<IUserService>();
|
||||
var cardService = sp.GetRequiredService<ICardService>();
|
||||
|
||||
if (!userManager.SupportsUserEmail)
|
||||
{
|
||||
throw new NotSupportedException($"{nameof(MapIdentityApi)} requires a user store with email support.");
|
||||
@@ -51,17 +55,34 @@ public static class AuthEndpoints
|
||||
{
|
||||
return CreateValidationProblem(IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(email)));
|
||||
}
|
||||
|
||||
var user = new User();
|
||||
await userStore.SetUserNameAsync(user, email, CancellationToken.None);
|
||||
|
||||
var validCard = await cardService.ValidatePinAsync(registration.KonamiId, registration.Pin);
|
||||
|
||||
if (!validCard)
|
||||
{
|
||||
return CreateValidationProblem("InvalidCard",
|
||||
"The provided Card is invalid");
|
||||
}
|
||||
|
||||
var user = await userService.GetUserByKonamiId(registration.KonamiId);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return CreateValidationProblem("InvalidKonamiId",
|
||||
"The provided Card is not found");
|
||||
}
|
||||
|
||||
await userStore.SetUserNameAsync(user, registration.Username, CancellationToken.None);
|
||||
await emailStore.SetEmailAsync(user, email, CancellationToken.None);
|
||||
var result = await userManager.CreateAsync(user, registration.Password);
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return CreateValidationProblem(result);
|
||||
}
|
||||
|
||||
|
||||
result = await userManager.AddPasswordAsync(user, registration.Password);
|
||||
|
||||
await SendConfirmationEmailAsync(user, userManager, context, email);
|
||||
return TypedResults.Ok();
|
||||
});
|
||||
@@ -75,8 +96,16 @@ public static class AuthEndpoints
|
||||
var isPersistent = (useCookies == true) && (useSessionCookies != true);
|
||||
signInManager.AuthenticationScheme = useCookieScheme ? IdentityConstants.ApplicationScheme : IdentityConstants.BearerScheme;
|
||||
|
||||
var result = await signInManager.PasswordSignInAsync(login.Email, login.Password, isPersistent, lockoutOnFailure: true);
|
||||
|
||||
var user = await signInManager.UserManager.FindByEmailAsync(login.Email);
|
||||
user ??= await signInManager.UserManager.FindByNameAsync(login.Email);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return TypedResults.Problem("Invalid login attempt.", statusCode: StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
|
||||
var result = await signInManager.PasswordSignInAsync(user!.UserName!, login.Password, isPersistent, lockoutOnFailure: true);
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(login.TwoFactorCode))
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
# This stage is used when running from VS in fast mode (Default for Debug configuration)
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
|
||||
# This stage is used to build the service project
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS with-node
|
||||
RUN apt-get update
|
||||
RUN apt-get install curl
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash
|
||||
RUN apt-get -y install nodejs
|
||||
|
||||
|
||||
FROM with-node AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Core/Server/Server.csproj", "Core/Server/"]
|
||||
COPY ["Core/Client/Client.esproj", "Core/Client/"]
|
||||
RUN dotnet restore "./Core/Server/Server.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Core/Server"
|
||||
RUN dotnet build "./Server.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# This stage is used to publish the service project to be copied to the final stage
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Server.dll"]
|
||||
@@ -25,7 +25,7 @@ public class AuthPassCardManegementHandler(ICardService cardService) : Handler<A
|
||||
status = 116;
|
||||
}
|
||||
|
||||
if (card!.User.Pin != int.Parse(req.Password))
|
||||
if (card!.User.Pin != req.Password)
|
||||
{
|
||||
status = 116;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public class GetRefIdCardManagementHandler(ICardService cardService, UserManager
|
||||
var konamiId = cardService.ConvertUidToKonamiId(req.CardId);
|
||||
|
||||
var card = new Abstractions.Entities.Card { RawId = req.CardId, KonamiId = konamiId };
|
||||
var user = new User { Pin = int.Parse(req.Password), UserName = konamiId, Cards = [card] };
|
||||
var user = new User { Pin = req.Password, UserName = konamiId, Cards = [card] };
|
||||
|
||||
await userManager.CreateAsync(user);
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+314
@@ -0,0 +1,314 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Server;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Server.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260118164523_SavePinAsStringInsteadOfInt")]
|
||||
partial class SavePinAsStringInsteadOfInt
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("Abstractions.Entities.Card", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KonamiId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RawId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Cards");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Abstractions.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PaseliAmount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Pin")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<int>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<int>", b =>
|
||||
{
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||
{
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Abstractions.Entities.Card", b =>
|
||||
{
|
||||
b.HasOne("Abstractions.Entities.User", "User")
|
||||
.WithMany("Cards")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<int>", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||
{
|
||||
b.HasOne("Abstractions.Entities.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||
{
|
||||
b.HasOne("Abstractions.Entities.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<int>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<int>", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Abstractions.Entities.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||
{
|
||||
b.HasOne("Abstractions.Entities.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Abstractions.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("Cards");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Server.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SavePinAsStringInsteadOfInt : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Pin",
|
||||
table: "AspNetUsers",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "Pin",
|
||||
table: "AspNetUsers",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,103 @@ namespace Server.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("Abstractions.Entities.Card", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KonamiId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RawId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Cards");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Abstractions.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PaseliAmount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Pin")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<int>", b =>
|
||||
{
|
||||
@@ -143,99 +239,15 @@ namespace Server.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Server.Entities.Card", b =>
|
||||
modelBuilder.Entity("Abstractions.Entities.Card", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
b.HasOne("Abstractions.Entities.User", "User")
|
||||
.WithMany("Cards")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Property<string>("KonamiId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RawId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Cards", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Server.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PaseliAmount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Pin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||
@@ -249,7 +261,7 @@ namespace Server.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||
{
|
||||
b.HasOne("Server.Entities.User", null)
|
||||
b.HasOne("Abstractions.Entities.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -258,7 +270,7 @@ namespace Server.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||
{
|
||||
b.HasOne("Server.Entities.User", null)
|
||||
b.HasOne("Abstractions.Entities.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -273,7 +285,7 @@ namespace Server.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Server.Entities.User", null)
|
||||
b.HasOne("Abstractions.Entities.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -282,25 +294,14 @@ namespace Server.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||
{
|
||||
b.HasOne("Server.Entities.User", null)
|
||||
b.HasOne("Abstractions.Entities.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Server.Entities.Card", b =>
|
||||
{
|
||||
b.HasOne("Server.Entities.User", "User")
|
||||
.WithMany("Cards")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Server.Entities.User", b =>
|
||||
modelBuilder.Entity("Abstractions.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("Cards");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Server.Models.Request;
|
||||
|
||||
public class RegisterRequest
|
||||
{
|
||||
public required string KonamiId { get; set; }
|
||||
public required string Pin { get; set; }
|
||||
public required string Username { get; set; }
|
||||
public required string Email { get; set; }
|
||||
public required string Password { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Server.Models.Request;
|
||||
|
||||
public class ValidateCardRequest
|
||||
{
|
||||
public required string CardId { get; set; }
|
||||
public required string Pin { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Abstractions.Entities;
|
||||
using Facet;
|
||||
using System;
|
||||
|
||||
namespace Server.Models.Response;
|
||||
|
||||
[Facet(typeof(Card), exclude: nameof(Card.User))]
|
||||
public partial class CardDto
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Server.Models.Response;
|
||||
|
||||
public class ValidateCardResponse
|
||||
{
|
||||
public required string Message { get; set; }
|
||||
public bool Success { get; set; } = false;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Server;
|
||||
using Server.Api;
|
||||
using Server.Authentication;
|
||||
using Server.Extensions;
|
||||
using Server.Middlewares;
|
||||
@@ -56,12 +57,13 @@ builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlite("Data S
|
||||
builder.Services.AddIdentityCore<User>(config =>
|
||||
{
|
||||
config.Password.RequiredLength = 8;
|
||||
config.SignIn.RequireConfirmedEmail = true;
|
||||
config.SignIn.RequireConfirmedEmail = false;
|
||||
config.Lockout.AllowedForNewUsers = true;
|
||||
}).AddEntityFrameworkStores<AppDbContext>();
|
||||
|
||||
builder.Services.AddTransient<ICardService, CardService>();
|
||||
builder.Services.AddSingleton<IPluginService>(pluginService);
|
||||
builder.Services.AddTransient<IUserService, UserService>();
|
||||
builder.Services.AddSingleton<IXmlLogService, XmlLogService>();
|
||||
|
||||
var app = builder.Build();
|
||||
@@ -96,7 +98,10 @@ foreach (var plugin in plugins)
|
||||
await plugin.OnAppInitialize(app);
|
||||
}
|
||||
|
||||
app.MapIdentityApi();
|
||||
var apiGroup = app.MapGroup("/api").WithTags("API");
|
||||
apiGroup.MapCardsApiEndpoints();
|
||||
apiGroup.MapUserApiEndpoints();
|
||||
apiGroup.MapIdentityApi();
|
||||
|
||||
var eamuseGroup = app.MapGroup("eamuse");
|
||||
|
||||
|
||||
@@ -39,6 +39,16 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
|
||||
}
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker",
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_HTTPS_PORTS": "8081",
|
||||
"ASPNETCORE_HTTP_PORTS": "8080"
|
||||
},
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-19
@@ -1,14 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<SpaRoot>..\Client</SpaRoot>
|
||||
<UserSecretsId>43f0ff47-d090-4bfc-a428-9a16f4291fa3</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<SpaRoot>..\Client</SpaRoot>
|
||||
<SpaProxyLaunchCommand>npm run dev</SpaProxyLaunchCommand>
|
||||
<SpaProxyServerUrl>https://localhost:5173</SpaProxyServerUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Endpoints\**" />
|
||||
<Content Remove="Endpoints\**" />
|
||||
<EmbeddedResource Remove="Endpoints\**" />
|
||||
<None Remove="Endpoints\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Abstractions\Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Client\Client.esproj">
|
||||
@@ -17,26 +26,29 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FastEndpoints" Version="7.0.1" />
|
||||
<PackageReference Include="FastEndpoints.ClientGen" Version="7.0.1" />
|
||||
<PackageReference Include="FastEndpoints.Security" Version="7.0.1" />
|
||||
<PackageReference Include="KbinXml.Net" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Facet" Version="5.4.1" />
|
||||
<PackageReference Include="Facet.Extensions" Version="5.4.1" />
|
||||
<PackageReference Include="FastEndpoints" Version="7.2.0" />
|
||||
<PackageReference Include="FastEndpoints.ClientGen" Version="7.2.0" />
|
||||
<PackageReference Include="FastEndpoints.Security" Version="7.2.0" />
|
||||
<PackageReference Include="KbinXml.Net" Version="2.1.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
|
||||
<Version>9.0.0</Version>
|
||||
<Version>10.*-*</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.2.1" />
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -48,10 +60,6 @@
|
||||
<None Remove="ResponseXml_1.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Endpoints\" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CreateCustomFolder" AfterTargets="Build">
|
||||
<MakeDir Directories="$(OutputPath)/plugins" />
|
||||
</Target>
|
||||
|
||||
@@ -203,4 +203,24 @@ public class CardService : ICardService
|
||||
{
|
||||
return _context.Cards.Include(x => x.User).FirstOrDefaultAsync(c => c.RawId == cardId);
|
||||
}
|
||||
|
||||
public Task<bool> Exists(string konamiId)
|
||||
{
|
||||
return _context.Cards.AnyAsync(c => c.KonamiId == konamiId);
|
||||
}
|
||||
|
||||
public Task<bool> ValidatePinAsync(string konamiId, string pin)
|
||||
{
|
||||
return _context.Cards.AnyAsync(c => c.KonamiId == konamiId && c.User.Pin == pin);
|
||||
}
|
||||
|
||||
public Task<bool> IsRegistered(string konamiId, string pin)
|
||||
{
|
||||
return _context.Cards.AnyAsync(c => c.KonamiId == konamiId && c.User.Pin == pin && c.User.PasswordHash != null);
|
||||
}
|
||||
|
||||
public Task<List<Card>> GetCardsByUserIdAsync(int userId)
|
||||
{
|
||||
return _context.Cards.Where(c => c.UserId == userId).ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
using Abstractions.Services;
|
||||
using Abstractions.Entities;
|
||||
using Abstractions.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Server.Services
|
||||
{
|
||||
public class UserService: IUserService
|
||||
public class UserService(AppDbContext context) : IUserService
|
||||
{
|
||||
public Task<User?> GetUserByKonamiId(string konamiId)
|
||||
{
|
||||
return context.Users
|
||||
.Include(u => u.Cards)
|
||||
.FirstOrDefaultAsync(u => u.Cards.Any(c => c.KonamiId == konamiId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"MyHttpEndpoint": {
|
||||
"Url": "http://0.0.0.0:5120"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
Reference in New Issue
Block a user