diff --git a/DysonNetwork.Drive/Billing/UsageController.cs b/DysonNetwork.Drive/Billing/UsageController.cs new file mode 100644 index 0000000..4f043e7 --- /dev/null +++ b/DysonNetwork.Drive/Billing/UsageController.cs @@ -0,0 +1,6 @@ +namespace DysonNetwork.Drive.Billing; + +public class UsageController +{ + +} \ No newline at end of file diff --git a/DysonNetwork.Drive/Client/src/layouts/dashboard.vue b/DysonNetwork.Drive/Client/src/layouts/dashboard.vue new file mode 100644 index 0000000..daf49db --- /dev/null +++ b/DysonNetwork.Drive/Client/src/layouts/dashboard.vue @@ -0,0 +1,42 @@ + + + diff --git a/DysonNetwork.Drive/Client/src/layouts/default.vue b/DysonNetwork.Drive/Client/src/layouts/default.vue index f9bd58c..cb1d6e8 100644 --- a/DysonNetwork.Drive/Client/src/layouts/default.vue +++ b/DysonNetwork.Drive/Client/src/layouts/default.vue @@ -15,7 +15,7 @@ - + @@ -28,12 +28,15 @@ import { LogInOutlined, PersonAddAlt1Outlined, PersonOutlineRound, + DataUsageRound, } from '@vicons/material' import { useUserStore } from '@/stores/user' import { useRoute, useRouter } from 'vue-router' import { useServicesStore } from '@/stores/services' const userStore = useUserStore() + +const router = useRouter() const route = useRoute() const hideUserMenu = computed(() => { @@ -60,6 +63,14 @@ const guestOptions = [ ] const userOptions = computed(() => [ + { + label: 'Usage', + key: 'dashboardUsage', + icon: () => + h(NIcon, null, { + default: () => h(DataUsageRound), + }), + }, { label: 'Profile', key: 'profile', @@ -67,7 +78,7 @@ const userOptions = computed(() => [ h(NIcon, null, { default: () => h(PersonOutlineRound), }), - } + }, ]) const servicesStore = useServicesStore() @@ -83,6 +94,8 @@ function handleGuestMenuSelect(key: string) { function handleUserMenuSelect(key: string) { if (key === 'profile') { window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'accounts/me')!, '_blank') + } else { + router.push({ name: key }) } } diff --git a/DysonNetwork.Drive/Client/src/router/index.ts b/DysonNetwork.Drive/Client/src/router/index.ts index 8c8d1d5..7980352 100644 --- a/DysonNetwork.Drive/Client/src/router/index.ts +++ b/DysonNetwork.Drive/Client/src/router/index.ts @@ -1,5 +1,6 @@ import { createRouter, createWebHistory } from 'vue-router' import { useUserStore } from '@/stores/user' +import { useServicesStore } from '@/stores/services' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -7,26 +8,53 @@ const router = createRouter({ { path: '/', name: 'index', - component: () => import('../views/index.vue') + component: () => import('../views/index.vue'), }, { path: '/files/:fileId', name: 'files', component: () => import('../views/files.vue'), - } - ] + }, + { + path: '/dashboard', + name: 'dashboard', + component: () => import('../layouts/dashboard.vue'), + meta: { requiresAuth: true }, + children: [ + { + path: 'usage', + name: 'dashboardUsage', + component: () => import('../views/dashboard/usage.vue'), + meta: { requiresAuth: true }, + }, + ], + }, + { + path: '/:notFound(.*)', + name: 'errorNotFound', + component: () => import('../views/not-found.vue'), + }, + ], }) router.beforeEach(async (to, from, next) => { const userStore = useUserStore() + const servicesStore = useServicesStore() // Initialize user state if not already initialized if (!userStore.user && localStorage.getItem('authToken')) { - await userStore.initialize() + await userStore.fetchUser() } if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) { - next({ name: 'login', query: { redirect: to.fullPath } }) + window.open( + servicesStore.getSerivceUrl( + 'DysonNetwork.Pass', + 'login?redirect=' + encodeURIComponent(window.location.href), + )!, + '_blank', + ) + next('/') } else { next() } diff --git a/DysonNetwork.Drive/Client/src/stores/user.ts b/DysonNetwork.Drive/Client/src/stores/user.ts index 62f586f..7dca8a6 100644 --- a/DysonNetwork.Drive/Client/src/stores/user.ts +++ b/DysonNetwork.Drive/Client/src/stores/user.ts @@ -11,7 +11,8 @@ export const useUserStore = defineStore('user', () => { const isAuthenticated = computed(() => !!user.value) // Actions - async function fetchUser() { + async function fetchUser(reload = true) { + if (!reload && user.value) return isLoading.value = true error.value = null try { @@ -21,9 +22,6 @@ export const useUserStore = defineStore('user', () => { if (!response.ok) { // If the token is invalid, clear it and the user state - if (response.status === 401) { - logout() - } throw new Error('Failed to fetch user information.') } @@ -36,13 +34,6 @@ export const useUserStore = defineStore('user', () => { } } - function logout() { - user.value = null - localStorage.removeItem('authToken') - // Optionally, redirect to login page - // router.push('/login') - } - function initialize() { const allowedOrigin = import.meta.env.DEV ? window.location.origin : 'https://id.solian.app' window.addEventListener('message', (event) => { @@ -69,7 +60,6 @@ export const useUserStore = defineStore('user', () => { error, isAuthenticated, fetchUser, - logout, initialize, } }) diff --git a/DysonNetwork.Drive/Client/src/types/pool.ts b/DysonNetwork.Drive/Client/src/types/pool.ts index 740668a..df0f55b 100644 --- a/DysonNetwork.Drive/Client/src/types/pool.ts +++ b/DysonNetwork.Drive/Client/src/types/pool.ts @@ -1,35 +1,36 @@ export interface SnFilePool { - id: string; - name: string; - storage_config: StorageConfig; - billing_config: BillingConfig; - public_indexable: boolean; - public_usable: boolean; - no_optimization: boolean; - no_metadata: boolean; - allow_encryption: boolean; - allow_anonymous: boolean; - require_privilege: number; - account_id: null; - resource_identifier: string; - created_at: Date; - updated_at: Date; - deleted_at: null; + id: string + name: string + description: string + storage_config: StorageConfig + billing_config: BillingConfig + public_indexable: boolean + public_usable: boolean + no_optimization: boolean + no_metadata: boolean + allow_encryption: boolean + allow_anonymous: boolean + require_privilege: number + account_id: null + resource_identifier: string + created_at: Date + updated_at: Date + deleted_at: null } export interface BillingConfig { - cost_multiplier: number; + cost_multiplier: number } export interface StorageConfig { - region: string; - bucket: string; - endpoint: string; - secret_id: string; - secret_key: string; - enable_signed: boolean; - enable_ssl: boolean; - image_proxy: null; - access_proxy: null; - expiration: null; + region: string + bucket: string + endpoint: string + secret_id: string + secret_key: string + enable_signed: boolean + enable_ssl: boolean + image_proxy: null + access_proxy: null + expiration: null } diff --git a/DysonNetwork.Drive/Client/src/views/dashboard/usage.vue b/DysonNetwork.Drive/Client/src/views/dashboard/usage.vue new file mode 100644 index 0000000..34f5c8f --- /dev/null +++ b/DysonNetwork.Drive/Client/src/views/dashboard/usage.vue @@ -0,0 +1,9 @@ + + + diff --git a/DysonNetwork.Drive/Client/src/views/files.vue b/DysonNetwork.Drive/Client/src/views/files.vue index 12eed1e..3d8d5ba 100644 --- a/DysonNetwork.Drive/Client/src/views/files.vue +++ b/DysonNetwork.Drive/Client/src/views/files.vue @@ -143,6 +143,7 @@ import { useRoute } from 'vue-router' import { computed, onMounted, ref } from 'vue' import { downloadAndDecryptFile } from './secure' +import { formatBytes } from './format' import hljs from 'highlight.js/lib/core' import json from 'highlight.js/lib/languages/json' @@ -200,13 +201,4 @@ function downloadFile() { window.open(fileSource.value, '_blank') } } - -function formatBytes(bytes: number, decimals = 2): string { - if (bytes === 0) return '0 Bytes' - const k = 1024 - const dm = decimals < 0 ? 0 : decimals - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] -} diff --git a/DysonNetwork.Drive/Client/src/views/format.ts b/DysonNetwork.Drive/Client/src/views/format.ts new file mode 100644 index 0000000..34f0d4f --- /dev/null +++ b/DysonNetwork.Drive/Client/src/views/format.ts @@ -0,0 +1,8 @@ +export function formatBytes(bytes: number, decimals = 2): string { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const dm = decimals < 0 ? 0 : decimals + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] +} diff --git a/DysonNetwork.Drive/Client/src/views/index.vue b/DysonNetwork.Drive/Client/src/views/index.vue index 3f615b4..69add13 100644 --- a/DysonNetwork.Drive/Client/src/views/index.vue +++ b/DysonNetwork.Drive/Client/src/views/index.vue @@ -49,7 +49,9 @@ type="password" class="mb-2" /> -

Only available for Stellar Program and certian file pool.

+

+ Only available for Stellar Program and certian file pool. +

@@ -110,11 +112,15 @@ import { type SelectOption, type SelectRenderTag, type UploadFileInfo, + useMessage, + NDivider, + NTooltip, } from 'naive-ui' import { computed, h, onMounted, ref } from 'vue' import { CloudUploadRound } from '@vicons/material' import { useUserStore } from '@/stores/user' import type { SnFilePool } from '@/types/pool' +import { formatBytes } from './format' import * as tus from 'tus-js-client' @@ -160,6 +166,42 @@ function renderPoolSelectLabel(option: SelectOption & SnFilePool) { }, [ h('div', null, [option.name as string]), + option.description && + h( + 'div', + { + style: { + fontSize: '0.875rem', + opacity: '0.75', + }, + }, + option.description, + ), + h( + 'div', + { + style: { + display: 'flex', + marginBottom: '4px', + fontSize: '0.75rem', + opacity: '0.75', + }, + }, + [ + policy.max_file_size && h('span', `Max ${formatBytes(policy.max_file_size)}`), + policy.accept_types && + h( + NTooltip, + {}, + { + trigger: () => h('span', `Accept limited types`), + default: () => h('span', policy.accept_types.join(', ')), + }, + ), + ].flatMap((el, idx, arr) => + idx < arr.length - 1 ? [el, h(NDivider, { vertical: true })] : [el], + ), + ), h( 'div', { @@ -228,6 +270,8 @@ const currentFilePool = computed(() => { return pools.value?.find((pool) => pool.id === filePool.value) ?? null }) +const messageDisplay = useMessage() + function customRequest({ file, data, @@ -246,13 +290,21 @@ function customRequest({ retryDelays: [0, 3000, 5000, 10000, 20000], metadata: { filename: file.name, - filetype: file.type ?? 'application/octet-stream', + 'content-type': file.type ?? 'application/octet-stream', }, headers: { ...requestHeaders, ...headers, }, onError: function (error) { + if (error instanceof tus.DetailedError) { + const failedBody = error.originalResponse?.getBody() + if (failedBody != null) + messageDisplay.error(`Upload failed: ${failedBody}`, { + duration: 10000, + closable: true, + }) + } console.error('[DRIVE] Upload failed:', error) onError() }, @@ -290,8 +342,7 @@ function createThumbnailUrl( function customDownload(file: UploadFileInfo) { const { url, name } = file - if (!url) - return + if (!url) return window.open(url.replace('/api', ''), '_blank') } diff --git a/DysonNetwork.Drive/Client/src/views/not-found.vue b/DysonNetwork.Drive/Client/src/views/not-found.vue new file mode 100644 index 0000000..b5c8da9 --- /dev/null +++ b/DysonNetwork.Drive/Client/src/views/not-found.vue @@ -0,0 +1,16 @@ + + + diff --git a/DysonNetwork.Drive/Migrations/20250726120323_AddFilePoolDescription.Designer.cs b/DysonNetwork.Drive/Migrations/20250726120323_AddFilePoolDescription.Designer.cs new file mode 100644 index 0000000..7483097 --- /dev/null +++ b/DysonNetwork.Drive/Migrations/20250726120323_AddFilePoolDescription.Designer.cs @@ -0,0 +1,271 @@ +// +using System; +using System.Collections.Generic; +using DysonNetwork.Drive; +using DysonNetwork.Drive.Storage; +using DysonNetwork.Shared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DysonNetwork.Drive.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20250726120323_AddFilePoolDescription")] + partial class AddFilePoolDescription + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property>("FileMeta") + .HasColumnType("jsonb") + .HasColumnName("file_meta"); + + b.Property("HasCompression") + .HasColumnType("boolean") + .HasColumnName("has_compression"); + + b.Property("HasThumbnail") + .HasColumnType("boolean") + .HasColumnName("has_thumbnail"); + + b.Property("Hash") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("hash"); + + b.Property("IsEncrypted") + .HasColumnType("boolean") + .HasColumnName("is_encrypted"); + + b.Property("IsMarkedRecycle") + .HasColumnType("boolean") + .HasColumnName("is_marked_recycle"); + + b.Property("MimeType") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("mime_type"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.Property>("SensitiveMarks") + .HasColumnType("jsonb") + .HasColumnName("sensitive_marks"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("StorageId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("storage_id"); + + b.Property("StorageUrl") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("storage_url"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("uploaded_at"); + + b.Property("UploadedTo") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("uploaded_to"); + + b.Property>("UserMeta") + .HasColumnType("jsonb") + .HasColumnName("user_meta"); + + b.HasKey("Id") + .HasName("pk_files"); + + b.HasIndex("PoolId") + .HasDatabaseName("ix_files_pool_id"); + + b.ToTable("files", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("FileId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("file_id"); + + b.Property("ResourceId") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("resource_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Usage") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("usage"); + + b.HasKey("Id") + .HasName("pk_file_references"); + + b.HasIndex("FileId") + .HasDatabaseName("ix_file_references_file_id"); + + b.ToTable("file_references", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BillingConfig") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("billing_config"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("PolicyConfig") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("policy_config"); + + b.Property("StorageConfig") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("storage_config"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_pools"); + + b.ToTable("pools", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => + { + b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") + .WithMany() + .HasForeignKey("PoolId") + .HasConstraintName("fk_files_pools_pool_id"); + + b.Navigation("Pool"); + }); + + modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => + { + b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_file_references_files_file_id"); + + b.Navigation("File"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DysonNetwork.Drive/Migrations/20250726120323_AddFilePoolDescription.cs b/DysonNetwork.Drive/Migrations/20250726120323_AddFilePoolDescription.cs new file mode 100644 index 0000000..fc30a97 --- /dev/null +++ b/DysonNetwork.Drive/Migrations/20250726120323_AddFilePoolDescription.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DysonNetwork.Drive.Migrations +{ + /// + public partial class AddFilePoolDescription : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "description", + table: "pools", + type: "character varying(8192)", + maxLength: 8192, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "description", + table: "pools"); + } + } +} diff --git a/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs index 44cedb3..f31a577 100644 --- a/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs +++ b/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs @@ -209,6 +209,12 @@ namespace DysonNetwork.Drive.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("deleted_at"); + b.Property("Description") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("description"); + b.Property("Name") .IsRequired() .HasMaxLength(1024) diff --git a/DysonNetwork.Drive/Storage/FilePool.cs b/DysonNetwork.Drive/Storage/FilePool.cs index 47c3522..186553c 100644 --- a/DysonNetwork.Drive/Storage/FilePool.cs +++ b/DysonNetwork.Drive/Storage/FilePool.cs @@ -41,6 +41,7 @@ public class FilePool : ModelBase, IIdentifiedResource { public Guid Id { get; set; } = Guid.NewGuid(); [MaxLength(1024)] public string Name { get; set; } = string.Empty; + [MaxLength(8192)] public string Description { get; set; } = string.Empty; [Column(TypeName = "jsonb")] public RemoteStorageConfig StorageConfig { get; set; } = new(); [Column(TypeName = "jsonb")] public BillingConfig BillingConfig { get; set; } = new(); [Column(TypeName = "jsonb")] public PolicyConfig PolicyConfig { get; set; } = new(); diff --git a/DysonNetwork.Drive/Storage/TusService.cs b/DysonNetwork.Drive/Storage/TusService.cs index 93fd74e..9c0137a 100644 --- a/DysonNetwork.Drive/Storage/TusService.cs +++ b/DysonNetwork.Drive/Storage/TusService.cs @@ -156,9 +156,9 @@ public abstract class TusService var metadata = eventContext.Metadata; var contentType = metadata.TryGetValue("content-type", out var ct) ? ct.GetString(Encoding.UTF8) : null; - + var scope = eventContext.HttpContext.RequestServices.CreateScope(); - + var rejected = false; var fs = scope.ServiceProvider.GetRequiredService(); @@ -173,9 +173,22 @@ public abstract class TusService // Do the policy check var policy = pool!.PolicyConfig; + if (!rejected && !pool.PolicyConfig.AllowEncryption) + { + var encryptPassword = eventContext.HttpContext.Request.Headers["X-FilePass"].FirstOrDefault(); + if (!string.IsNullOrEmpty(encryptPassword)) + { + eventContext.FailRequest( + HttpStatusCode.Forbidden, + "File encryption is not allowed in this pool" + ); + rejected = true; + } + } + if (!rejected && policy.AcceptTypes is not null) { - if (contentType is null) + if (string.IsNullOrEmpty(contentType)) { eventContext.FailRequest( HttpStatusCode.BadRequest, diff --git a/DysonNetwork.Pass/Client/src/router/index.ts b/DysonNetwork.Pass/Client/src/router/index.ts index c30f0e2..c523217 100644 --- a/DysonNetwork.Pass/Client/src/router/index.ts +++ b/DysonNetwork.Pass/Client/src/router/index.ts @@ -34,7 +34,12 @@ const router = createRouter({ name: 'me', component: () => import('../views/accounts/me.vue'), meta: { requiresAuth: true } - } + }, + { + path: '/:notFound(.*)', + name: 'errorNotFound', + component: () => import('../views/not-found.vue'), + }, ] }) diff --git a/DysonNetwork.Pass/Client/src/views/not-found.vue b/DysonNetwork.Pass/Client/src/views/not-found.vue new file mode 100644 index 0000000..b5c8da9 --- /dev/null +++ b/DysonNetwork.Pass/Client/src/views/not-found.vue @@ -0,0 +1,16 @@ + + +