start frontend

This commit is contained in:
purifetchi 2025-01-13 00:11:40 +01:00
parent 81b824bb79
commit 7417a2c35c
34 changed files with 3592 additions and 14 deletions

179
.gitignore vendored
View file

@ -1,7 +1,174 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
x64/
[Bb]in/
[Oo]bj/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.log
*.svclog
*.scc
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# Click-Once directory
publish/
# Publish Web Output
*.Publish.xml
*.pubxml
*.azurePubxml
# NuGet Packages Directory
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
packages/
## TODO: If the tool you use requires repositories.config, also uncomment the next line
!packages/repositories.config
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
![Ss]tyle[Cc]op.targets
~$*
*~
*.dbmdl
*.[Pp]ublish.xml
*.publishsettings
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
App_Data/*.mdf
App_Data/*.ldf
# =========================
# Windows detritus
# =========================
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Mac desktop service store files
.DS_Store
_NCrunch*
.idea/
mizuki.db
appsettings.json
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
PinkSea.Gateway/wwwroot/
mizuki.db*
uploads/

View file

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Mizuki.Dtos;
using Mizuki.Services;
@ -24,7 +25,7 @@ public class LoginController(
[HttpPost]
[Route("login")]
public async Task<RedirectResult> Login(
LoginDataDto dto)
[FromForm] LoginDataDto dto)
{
if (!await userService.CheckPasswordForUser(dto.Username, dto.Password))
{
@ -34,6 +35,8 @@ public class LoginController(
var user = await userService.GetUserForUsername(dto.Username);
await loginService.LoginAsUser(user);
Console.WriteLine($"Logged in as {user.Username}");
return Redirect("/");
}
@ -57,7 +60,7 @@ public class LoginController(
[Route("register")]
[HttpPost]
public async Task<RedirectResult> Register(
LoginDataDto dto)
[FromForm] LoginDataDto dto)
{
if (await userService.UsernameTaken(dto.Username))
return Redirect("/register?error=This user already exists.");
@ -78,4 +81,25 @@ public class LoginController(
return Redirect("/");
}
/// <summary>
/// Checks whether we have logged in.
/// </summary>
/// <returns>Either an OK or a forbidden result.</returns>
[Route("check")]
[HttpGet]
public async Task<Results<Ok, ForbidHttpResult>> Check()
{
try
{
var user = await loginService.GetActiveUser();
Console.WriteLine($"Get active user returned {user.Username}");
return TypedResults.Ok();
}
catch
{
Console.WriteLine($"Get active user returned FORBID");
return TypedResults.Forbid();
}
}
}

View file

@ -18,7 +18,8 @@ public class ServeController(
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>The file, or an error.</returns>
[Route("/{filename}")]
[Route("{filename}")]
[HttpGet]
public async Task<Results<FileStreamHttpResult, NotFound>> GetFile(
[FromRoute] string filename)
{

View file

@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Mizuki.Database.Models;
namespace Mizuki.Database;
@ -23,6 +24,28 @@ public class MizukiDbContext : DbContext
{
modelBuilder.Entity<User>();
modelBuilder.Entity<Upload>();
if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite")
{
// SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
// here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
// To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset
// use the DateTimeOffsetToBinaryConverter
// Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
// This only supports millisecond precision, but should be sufficient for most use cases.
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset)
|| p.PropertyType == typeof(DateTimeOffset?));
foreach (var property in properties)
{
modelBuilder
.Entity(entityType.Name)
.Property(property.Name)
.HasConversion(new DateTimeOffsetToBinaryConverter());
}
}
}
}
/// <inheritdoc />

View file

@ -5,6 +5,9 @@ using Mizuki.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllers();
builder.Services.AddValidatorsFromAssembly(
Assembly.GetCallingAssembly());
@ -14,9 +17,6 @@ builder.Services.AddScoped<UploadService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddDbContext<MizukiDbContext>();
builder.Services.AddHttpContextAccessor();
var app = builder.Build();
builder.Services.AddAuthentication("MizukiAuth")
.AddCookie("MizukiAuth", options =>
{
@ -25,8 +25,10 @@ builder.Services.AddAuthentication("MizukiAuth")
options.AccessDeniedPath = "/";
});
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapControllers();
app.MapFallbackToFile("index.html");
app.UseAuthentication();
app.UseAuthorization();

View file

@ -47,4 +47,17 @@ public class DriveService
File.Delete(path);
}
/// <summary>
/// Saves a file with the given filename.
/// </summary>
/// <param name="filename">The filename.</param>
/// <param name="fileStream">The file stream.</param>
public void SaveFileByFilename(string filename, Stream fileStream)
{
var actualFilename = Path.GetFileName(filename);
var path = Path.Combine(UploadsFolder, actualFilename);
using var writeStream = File.OpenWrite(path);
fileStream.CopyTo(writeStream);
}
}

View file

@ -50,7 +50,7 @@ public class LoginService(
var username = httpContextAccessor.HttpContext!
.User
.FindFirst(ClaimTypes.Name)!
.ToString();
.Value;
return await userService.GetUserForUsername(username);
}

View file

@ -38,6 +38,8 @@ public class UploadService(
await dbContext.Uploads.AddAsync(upload);
await dbContext.SaveChangesAsync();
driveService.SaveFileByFilename(upload.Filename, file.OpenReadStream());
return upload;
}

30
mizuki-frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View file

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

33
mizuki-frontend/README.md Normal file
View file

@ -0,0 +1,33 @@
# mizuki-frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

1
mizuki-frontend/env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2929
mizuki-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
{
"name": "mizuki-frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^7.0.2",
"typescript": "~5.6.3",
"vite": "^6.0.5",
"vite-plugin-vue-devtools": "^7.6.8",
"vue-tsc": "^2.1.10"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,11 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style scoped>
</style>

View file

View file

@ -0,0 +1 @@
@import './base.css';

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import {onBeforeMount, ref} from "vue";
import type FileDto from "@/dto/file-dto.ts";
import FileListItem from "@/components/FileListItem.vue";
const files = ref<FileDto[]>([]);
onBeforeMount(async () => {
const items = await fetch('/api/file/all')
.then(i => i.json())
.then(j => j as FileDto[]);
files.value = items;
});
</script>
<template>
<div v-if="files.length > 0">
<FileListItem v-for="file of files" :item="file" :key="file.filename" />
</div>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,21 @@
<script setup lang="ts">
import type FileDto from "@/dto/file-dto.ts";
import {computed} from "vue";
const props = defineProps<{
item: FileDto
}>();
const url = computed(() => {
return `http://localhost:5118/f/${props.item.filename}`;
})
</script>
<template>
<div>{{ item.filename }} - {{ item.originalFilename }}</div>
<a :href="url">Download</a>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,37 @@
<script setup lang="ts">
const sendData = async () => {
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
// noinspection JSIncompatibleTypesComparison
if (input.files === null || input.files.length < 1) {
return;
}
const data = new FormData();
data.append('formFile', input.files[0]);
const resp = await fetch('/api/file/upload', {
method: 'POST',
body: data,
credentials: 'include'
});
const json = resp.json();
if ('filename' in json) {
alert(`Your file is able to be downloaded at https://localhost:5881/f/${json.filename}!`);
} else if ('reason' in json) {
alert(json.reason);
}
};
</script>
<template>
<div>
<input type="file" name="formFile" />
<input type="submit" v-on:click.prevent="sendData" />
</div>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,5 @@
export default interface FileDto {
filename: string,
originalFilename: string,
sizeInBytes: number
}

View file

@ -0,0 +1,11 @@
export const checkIfLoggedIn = async () => {
try {
const resp = await fetch("/api/user/check", {
redirect: 'error',
credentials: 'include'
});
return resp.ok;
} catch {
return false;
}
};

View file

@ -0,0 +1,11 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')

View file

@ -0,0 +1,27 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from "@/views/LoginView.vue";
import RegisterView from "@/views/RegisterView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/login',
name: 'login',
component: LoginView,
},
{
path: '/register',
name: 'register',
component: RegisterView,
}
],
})
export default router

View file

@ -0,0 +1,32 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { checkIfLoggedIn } from "@/helpers/api.ts";
import { useRouter } from "vue-router";
import Uploader from "@/components/Uploader.vue";
import FileList from "@/components/FileList.vue";
const router = useRouter();
onMounted(async () => {
const loggedIn = await checkIfLoggedIn();
if (!loggedIn) {
await router.push('/login');
return;
}
});
const logout = async () => {
await fetch('/api/user/logout');
window.location.reload();
};
</script>
<template>
<FileList />
<Uploader />
<a href="#" v-on:click.prevent="logout">Log out</a>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,28 @@
<script setup lang="ts">
import { onBeforeMount, ref } from "vue";
import { checkIfLoggedIn } from "@/helpers/api.ts";
import { useRouter } from "vue-router";
const router = useRouter();
const loggedIn = ref<boolean>(true);
onBeforeMount(async () => {
loggedIn.value = await checkIfLoggedIn();
if (loggedIn.value) {
await router.push('/');
return;
}
});
</script>
<template>
<form v-if="!loggedIn" action="/api/user/login" method="post">
<span>Username</span> <input type="text" name="Username" /><br />
<span>Password</span> <input type="password" name="Password" />
<input type="submit" value="Login" />
</form>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,29 @@
<script setup lang="ts">
import { onBeforeMount, ref } from "vue";
import { checkIfLoggedIn } from "@/helpers/api.ts";
import { useRouter } from "vue-router";
const router = useRouter();
const loggedIn = ref<boolean>(true);
onBeforeMount(async () => {
loggedIn.value = await checkIfLoggedIn();
if (loggedIn.value) {
await router.push('/');
return;
}
});
</script>
<template>
<form v-if="!loggedIn" method="post" action="/api/user/register">
<span>Username</span> <input type="text" name="Username" /><br />
<span>Password</span> <input type="password" name="Password" /><br />
<span>Repeat Password</span> <input type="password" name="ValidatePassword" /><br />
<input type="submit" value="Register" />
</form>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

View file

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View file

@ -0,0 +1,18 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View file

@ -0,0 +1,29 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
proxy: {
'^/api': {
target: 'http://localhost:5118',
changeOrigin: true,
cookiePathRewrite: {
"*": "/",
}
}
}
}
})

1
wwwroot/index.html Normal file
View file

@ -0,0 +1 @@
<h1>test</h1>