diff --git a/.vscode/settings.json b/.vscode/settings.json index aade13c..5e70447 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,7 @@ "files.associations": { "*.css": "tailwindcss" }, + "typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"], "tailwindCSS.classAttributes": ["class", "ui"], "tailwindCSS.experimental.classRegex": [["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]] diff --git a/apps/backend/src/main/kotlin/org/tobynguyen/solitar/exception/UrlException.kt b/apps/backend/src/main/kotlin/org/tobynguyen/solitar/exception/UrlException.kt index 1e1e752..a00005b 100644 --- a/apps/backend/src/main/kotlin/org/tobynguyen/solitar/exception/UrlException.kt +++ b/apps/backend/src/main/kotlin/org/tobynguyen/solitar/exception/UrlException.kt @@ -7,3 +7,5 @@ class UrlExpiredException(override val message: String) : RuntimeException(messa class UrlDisabledException(override val message: String) : RuntimeException(message) class UrlShortCodeConflictedException(override val message: String) : RuntimeException(message) + +class UrlProtectedException(override val message: String) : RuntimeException(message) diff --git a/apps/backend/src/main/kotlin/org/tobynguyen/solitar/exception/UrlExceptionHandler.kt b/apps/backend/src/main/kotlin/org/tobynguyen/solitar/exception/UrlExceptionHandler.kt index a7552e3..395c313 100644 --- a/apps/backend/src/main/kotlin/org/tobynguyen/solitar/exception/UrlExceptionHandler.kt +++ b/apps/backend/src/main/kotlin/org/tobynguyen/solitar/exception/UrlExceptionHandler.kt @@ -35,6 +35,10 @@ class UrlExceptionHandler : ResponseEntityExceptionHandler() { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail) } + @ExceptionHandler(UrlProtectedException::class) + fun onUrlProtected(e: UrlProtectedException) = + ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, e.message) + @ExceptionHandler(UrlNotFoundException::class) fun onUrlNotFound(e: UrlNotFoundException) = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.message) diff --git a/apps/backend/src/main/kotlin/org/tobynguyen/solitar/service/UrlService.kt b/apps/backend/src/main/kotlin/org/tobynguyen/solitar/service/UrlService.kt index c0df132..e035ae6 100644 --- a/apps/backend/src/main/kotlin/org/tobynguyen/solitar/service/UrlService.kt +++ b/apps/backend/src/main/kotlin/org/tobynguyen/solitar/service/UrlService.kt @@ -8,6 +8,7 @@ import org.sqids.Sqids import org.tobynguyen.solitar.exception.UrlDisabledException import org.tobynguyen.solitar.exception.UrlExpiredException import org.tobynguyen.solitar.exception.UrlNotFoundException +import org.tobynguyen.solitar.exception.UrlProtectedException import org.tobynguyen.solitar.exception.UrlShortCodeConflictedException import org.tobynguyen.solitar.mapper.toResponseDto import org.tobynguyen.solitar.model.dto.UrlCreateDto @@ -43,7 +44,7 @@ class UrlService( UrlForwardResponseDto(urlEntity.toResponseDto().originalUrl) } else { if (password == null) - throw UrlDisabledException("Please provide a valid password to unlock this URL.") + throw UrlProtectedException("Please provide a valid password to unlock this URL.") if (argon2Encoder.matches(password, urlEntity.password)) { UrlForwardResponseDto(urlEntity.toResponseDto().originalUrl) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 2b29eb5..c89b210 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -16,12 +16,12 @@ "@nuxt/ui": "^4.4.0", "@nuxtjs/seo": "^3.4.0", "@vueuse/integrations": "^14.2.1", + "arktype": "^2.1.29", "dayjs": "^1.11.19", "nuxt": "^4.3.1", "qrcode": "^1.5.4", "tailwindcss": "^4.2.0", "ufo": "^1.6.3", - "valibot": "^1.2.0", "vue": "^3.5.28", "vue-router": "^4.6.4" }, diff --git a/apps/frontend/src/app/components/SecureWarning.vue b/apps/frontend/src/app/components/SecureWarning.vue new file mode 100644 index 0000000..5da9cbd --- /dev/null +++ b/apps/frontend/src/app/components/SecureWarning.vue @@ -0,0 +1,62 @@ + + + diff --git a/apps/frontend/src/app/components/form/QrGeneratorForm.vue b/apps/frontend/src/app/components/form/QrGeneratorForm.vue index d601dfa..483a8e6 100644 --- a/apps/frontend/src/app/components/form/QrGeneratorForm.vue +++ b/apps/frontend/src/app/components/form/QrGeneratorForm.vue @@ -1,20 +1,22 @@ diff --git a/apps/frontend/src/app/pages/unlock.vue b/apps/frontend/src/app/pages/unlock.vue new file mode 100644 index 0000000..fbf2100 --- /dev/null +++ b/apps/frontend/src/app/pages/unlock.vue @@ -0,0 +1,91 @@ + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f17cd51..d857b8f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: '@vueuse/integrations': specifier: ^14.2.1 version: 14.2.1(fuse.js@7.1.0)(qrcode@1.5.4)(vue@3.5.28(typescript@5.9.2)) + arktype: + specifier: ^2.1.29 + version: 2.1.29 dayjs: specifier: ^1.11.19 version: 1.11.19 @@ -52,9 +55,6 @@ importers: ufo: specifier: ^1.6.3 version: 1.6.3 - valibot: - specifier: ^1.2.0 - version: 1.2.0(typescript@5.9.2) vue: specifier: ^3.5.28 version: 3.5.28(typescript@5.9.2) @@ -96,6 +96,12 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@ark/schema@0.56.0': + resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==} + + '@ark/util@0.56.0': + resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -2677,6 +2683,12 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + arkregex@0.0.5: + resolution: {integrity: sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==} + + arktype@2.1.29: + resolution: {integrity: sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==} + ast-kit@2.2.0: resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} engines: {node: '>=20.19.0'} @@ -5441,6 +5453,12 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 + '@ark/schema@0.56.0': + dependencies: + '@ark/util': 0.56.0 + + '@ark/util@0.56.0': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -7980,6 +7998,16 @@ snapshots: dependencies: tslib: 2.8.1 + arkregex@0.0.5: + dependencies: + '@ark/util': 0.56.0 + + arktype@2.1.29: + dependencies: + '@ark/schema': 0.56.0 + '@ark/util': 0.56.0 + arkregex: 0.0.5 + ast-kit@2.2.0: dependencies: '@babel/parser': 7.29.0 @@ -10877,6 +10905,7 @@ snapshots: valibot@1.2.0(typescript@5.9.2): optionalDependencies: typescript: 5.9.2 + optional: true vaul-vue@0.4.1(reka-ui@2.7.0(typescript@5.9.2)(vue@3.5.28(typescript@5.9.2)))(vue@3.5.28(typescript@5.9.2)): dependencies: