Skip to content

Commit 0c6f9f8

Browse files
authored
Merge pull request #5 from solitar-dev/feat/password
Fix issues
2 parents 77062ae + 5043584 commit 0c6f9f8

File tree

11 files changed

+239
-109
lines changed

11 files changed

+239
-109
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"files.associations": {
1616
"*.css": "tailwindcss"
1717
},
18+
"typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"],
1819

1920
"tailwindCSS.classAttributes": ["class", "ui"],
2021
"tailwindCSS.experimental.classRegex": [["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]]

apps/backend/src/main/kotlin/org/tobynguyen/solitar/exception/UrlException.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ class UrlExpiredException(override val message: String) : RuntimeException(messa
77
class UrlDisabledException(override val message: String) : RuntimeException(message)
88

99
class UrlShortCodeConflictedException(override val message: String) : RuntimeException(message)
10+
11+
class UrlProtectedException(override val message: String) : RuntimeException(message)

apps/backend/src/main/kotlin/org/tobynguyen/solitar/exception/UrlExceptionHandler.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ class UrlExceptionHandler : ResponseEntityExceptionHandler() {
3535
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail)
3636
}
3737

38+
@ExceptionHandler(UrlProtectedException::class)
39+
fun onUrlProtected(e: UrlProtectedException) =
40+
ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, e.message)
41+
3842
@ExceptionHandler(UrlNotFoundException::class)
3943
fun onUrlNotFound(e: UrlNotFoundException) =
4044
ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.message)

apps/backend/src/main/kotlin/org/tobynguyen/solitar/service/UrlService.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import org.sqids.Sqids
88
import org.tobynguyen.solitar.exception.UrlDisabledException
99
import org.tobynguyen.solitar.exception.UrlExpiredException
1010
import org.tobynguyen.solitar.exception.UrlNotFoundException
11+
import org.tobynguyen.solitar.exception.UrlProtectedException
1112
import org.tobynguyen.solitar.exception.UrlShortCodeConflictedException
1213
import org.tobynguyen.solitar.mapper.toResponseDto
1314
import org.tobynguyen.solitar.model.dto.UrlCreateDto
@@ -43,7 +44,7 @@ class UrlService(
4344
UrlForwardResponseDto(urlEntity.toResponseDto().originalUrl)
4445
} else {
4546
if (password == null)
46-
throw UrlDisabledException("Please provide a valid password to unlock this URL.")
47+
throw UrlProtectedException("Please provide a valid password to unlock this URL.")
4748

4849
if (argon2Encoder.matches(password, urlEntity.password)) {
4950
UrlForwardResponseDto(urlEntity.toResponseDto().originalUrl)

apps/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
"@nuxt/ui": "^4.4.0",
1717
"@nuxtjs/seo": "^3.4.0",
1818
"@vueuse/integrations": "^14.2.1",
19+
"arktype": "^2.1.29",
1920
"dayjs": "^1.11.19",
2021
"nuxt": "^4.3.1",
2122
"qrcode": "^1.5.4",
2223
"tailwindcss": "^4.2.0",
2324
"ufo": "^1.6.3",
24-
"valibot": "^1.2.0",
2525
"vue": "^3.5.28",
2626
"vue-router": "^4.6.4"
2727
},
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script setup lang="ts">
2+
const { url } = defineProps<{ url: string }>();
3+
4+
const forceHttps = () => {
5+
const secureUrl = url.replace(/^http:/, "https:");
6+
navigateTo(secureUrl, { external: true });
7+
};
8+
9+
const acceptRisk = () => {
10+
navigateTo(url, { external: true });
11+
};
12+
</script>
13+
14+
<template>
15+
<div class="flex flex-col justify-center items-center gap-5">
16+
<UIcon name="i-tabler-alert-triangle" class="size-12 text-warning" />
17+
<UPageCard class="max-w-lg" :reverse="false">
18+
<template #title>
19+
<p>Insecure Connection Warning</p>
20+
</template>
21+
22+
<template #description>
23+
<div class="flex flex-col gap-3">
24+
<p>
25+
The link you're about to visit does
26+
<span class="font-bold">not use HTTPS</span>, which means your connection is
27+
<span class="font-bold">not encrypted</span>. Your data could be intercepted
28+
by third parties.
29+
</p>
30+
<UCard>
31+
<template #header>
32+
<p class="text-warning">{{ url }}</p>
33+
</template>
34+
</UCard>
35+
<ULink
36+
class="text-left"
37+
to="https://www.cloudflare.com/learning/ssl/why-is-http-not-secure/"
38+
:external="true"
39+
target="_blank"
40+
>What is HTTPS and why is it important?</ULink
41+
>
42+
</div>
43+
</template>
44+
45+
<template #footer>
46+
<div class="flex flex-row gap-3">
47+
<UButton
48+
label="Enforce HTTPS"
49+
icon="i-tabler-lock"
50+
@click="forceHttps"
51+
class="hover:cursor-pointer" />
52+
<UButton
53+
label="Accept Risk & Continue"
54+
variant="ghost"
55+
color="error"
56+
@click="acceptRisk"
57+
class="hover:cursor-pointer" />
58+
</div>
59+
</template>
60+
</UPageCard>
61+
</div>
62+
</template>

apps/frontend/src/app/components/form/QrGeneratorForm.vue

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
<script setup lang="ts">
22
import { QrCodeModal } from "#components";
33
import type { FormSubmitEvent } from "@nuxt/ui";
4-
import * as v from "valibot";
4+
import { type } from "arktype";
55
6-
const schema = v.object({
7-
url: v.pipe(v.string("URL is required"), v.url("Invalid URL")),
6+
const schema = type({
7+
url: "string.url",
88
});
99
10-
const state = ref({
11-
url: undefined,
10+
type Schema = typeof schema.infer;
11+
12+
const state = ref<Schema>({
13+
url: "",
1214
});
1315
1416
const overlay = useOverlay();
1517
const modal = overlay.create(QrCodeModal);
1618
17-
async function onSubmit(event: FormSubmitEvent<v.InferOutput<typeof schema>>) {
19+
async function onSubmit(event: FormSubmitEvent<Schema>) {
1820
event.preventDefault();
1921
2022
modal.open({

apps/frontend/src/app/components/form/UrlShortenerForm.vue

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script setup lang="ts">
2-
import * as v from "valibot";
32
import type { FormSubmitEvent, SelectItem } from "@nuxt/ui";
43
import { ShortenedUrlModal } from "#components";
54
import { joinURL } from "ufo";
5+
import { type } from "arktype";
66
77
const selectItems: SelectItem[] = [
88
{
@@ -35,38 +35,22 @@ const selectItems: SelectItem[] = [
3535
},
3636
];
3737
38-
const schema = v.object({
39-
longUrl: v.pipe(
40-
v.string(),
41-
v.regex(
42-
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/,
43-
"Invalid URL",
44-
),
45-
),
46-
alias: v.optional(
47-
v.pipe(
48-
v.string(),
49-
v.regex(/^[a-zA-Z0-9_-]*$/, "Invalid alias"),
50-
v.minLength(7, "Alias must be at least 7 characters"),
51-
v.maxLength(255, "Alias is too long"),
52-
),
53-
),
54-
password: v.optional(
55-
v.pipe(
56-
v.string(),
57-
v.minLength(3, "Password must be at least 3 characters"),
58-
v.maxLength(255, "Password is too long"),
59-
),
60-
),
61-
neverExpire: v.boolean(),
62-
expireTime: v.number(),
63-
expireUnit: v.pipe(v.string(), v.picklist(expireUnits)),
38+
const schema = type({
39+
longUrl:
40+
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/,
41+
"alias?": "'' | 7 <= string <= 255",
42+
"password?": "'' | 3 <= string <= 255",
43+
neverExpire: "boolean",
44+
expireTime: "number",
45+
expireUnit: "'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'",
6446
});
6547
66-
const state = ref({
48+
type Schema = typeof schema.infer;
49+
50+
const state = ref<Schema>({
6751
longUrl: "",
68-
alias: undefined,
69-
password: undefined,
52+
alias: "",
53+
password: "",
7054
neverExpire: true,
7155
expireTime: 30,
7256
expireUnit: "second",
@@ -80,15 +64,15 @@ const siteConfig = useSiteConfig();
8064
8165
const modal = overlay.create(ShortenedUrlModal);
8266
83-
async function onSubmit(event: FormSubmitEvent<v.InferOutput<typeof schema>>) {
67+
async function onSubmit(event: FormSubmitEvent<Schema>) {
8468
event.preventDefault();
8569
8670
const { longUrl, alias, neverExpire, password } = event.data;
8771
8872
const body: UrlShortenerBody = {
8973
url: longUrl,
90-
alias,
91-
password,
74+
alias: alias === "" ? undefined : alias,
75+
password: password === "" ? undefined : password,
9276
...(!neverExpire && {
9377
expireTime: generateExpireTime(event.data.expireTime, event.data.expireUnit),
9478
}),
@@ -106,8 +90,8 @@ async function onSubmit(event: FormSubmitEvent<v.InferOutput<typeof schema>>) {
10690
});
10791
} else {
10892
toast.add({
109-
title: error.data.error,
110-
description: error.data.message,
93+
title: error.data.title,
94+
description: error.data.detail,
11195
color: "error",
11296
});
11397
}

apps/frontend/src/app/pages/[shortCode].vue

Lines changed: 17 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,22 @@ const shortCode = route.params.shortCode!.toString();
1010
const { data, error } = await useAsyncData(() => urlRepository.getLongUrl({ shortCode }));
1111
1212
if (error.value) {
13-
const e = error.value.data as { error: string; message: string };
14-
15-
throw createError({
16-
statusCode: error.value?.status || 500,
17-
statusMessage: e.error || "Internal Server Error",
18-
message: e.message || "Failed to resolve short URL",
19-
});
13+
if (error.value.status == 401) {
14+
await navigateTo({
15+
path: "/unlock",
16+
query: {
17+
c: shortCode,
18+
},
19+
});
20+
} else {
21+
const e = error.value.data as { title: string; detail: string };
22+
23+
throw createError({
24+
statusCode: error.value?.status || 500,
25+
statusMessage: e.title || "Internal Server Error",
26+
message: e.detail || "Failed to resolve short URL",
27+
});
28+
}
2029
}
2130
2231
const originalUrl = data.value as string;
@@ -29,65 +38,10 @@ if (isSecure) {
2938
redirectCode: 302,
3039
});
3140
}
32-
33-
const forceHttps = () => {
34-
const secureUrl = originalUrl.replace(/^http:/, "https:");
35-
navigateTo(secureUrl, { external: true });
36-
};
37-
38-
const acceptRisk = () => {
39-
navigateTo(originalUrl, { external: true });
40-
};
4141
</script>
4242

4343
<template>
4444
<div class="w-full h-screen grid place-items-center" v-if="!isSecure">
45-
<div class="flex flex-col justify-center items-center gap-5">
46-
<UIcon name="i-tabler-alert-triangle" class="size-12 text-warning" />
47-
<UPageCard class="max-w-lg" :reverse="false">
48-
<template #title>
49-
<p>Insecure Connection Warning</p>
50-
</template>
51-
52-
<template #description>
53-
<div class="flex flex-col gap-3">
54-
<p>
55-
The link you're about to visit does
56-
<span class="font-bold">not use HTTPS</span>, which means your
57-
connection is <span class="font-bold">not encrypted</span>. Your data
58-
could be intercepted by third parties.
59-
</p>
60-
<UCard>
61-
<template #header>
62-
<p class="text-warning">{{ originalUrl }}</p>
63-
</template>
64-
</UCard>
65-
<ULink
66-
class="text-left"
67-
to="https://www.cloudflare.com/learning/ssl/why-is-http-not-secure/"
68-
:external="true"
69-
target="_blank"
70-
>What is HTTPS and why is it important?</ULink
71-
>
72-
</div>
73-
</template>
74-
75-
<template #footer>
76-
<div class="flex flex-row gap-3">
77-
<UButton
78-
label="Enforce HTTPS"
79-
icon="i-tabler-lock"
80-
@click="forceHttps"
81-
class="hover:cursor-pointer" />
82-
<UButton
83-
label="Accept Risk & Continue"
84-
variant="ghost"
85-
color="error"
86-
@click="acceptRisk"
87-
class="hover:cursor-pointer" />
88-
</div>
89-
</template>
90-
</UPageCard>
91-
</div>
45+
<SecureWarning :url="originalUrl" />
9246
</div>
9347
</template>

0 commit comments

Comments
 (0)