Skip to content

Commit 5043584

Browse files
committed
feat: add password form
1 parent 59d121d commit 5043584

File tree

6 files changed

+178
-64
lines changed

6 files changed

+178
-64
lines changed

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)
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/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>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<script setup lang="ts">
2+
import type { FormSubmitEvent } from "@nuxt/ui";
3+
import { type } from "arktype";
4+
5+
const route = useRoute();
6+
7+
const shortCode = route.query.c!.toString();
8+
9+
if (!shortCode) {
10+
await navigateTo("/");
11+
}
12+
13+
const { $api } = useNuxtApp();
14+
const urlRepository = repository($api);
15+
const toast = useToast();
16+
17+
const schema = type({
18+
password: "3 <= string <= 255",
19+
});
20+
21+
type Schema = typeof schema.infer;
22+
23+
const state = ref<Schema>({
24+
password: "",
25+
});
26+
27+
async function onSubmit(event: FormSubmitEvent<Schema>) {
28+
event.preventDefault();
29+
30+
const password = event.data.password;
31+
32+
try {
33+
const originalUrl = await urlRepository.getLongUrl({ shortCode, password });
34+
35+
await navigateTo(originalUrl, {
36+
external: true,
37+
redirectCode: 302,
38+
});
39+
} catch (error: any) {
40+
const e = error.data as { title: string; detail: string };
41+
42+
toast.add({
43+
title: e.title,
44+
description: e.detail,
45+
color: "error",
46+
});
47+
}
48+
}
49+
</script>
50+
51+
<template>
52+
<div class="grid place-items-center h-screen">
53+
<div class="flex flex-col justify-center items-center gap-5">
54+
<UIcon name="i-carbon:locked" class="size-12 text-warning" />
55+
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
56+
<UPageCard class="max-w-lg" :reverse="false">
57+
<template #title>
58+
<p>Protected by password</p>
59+
</template>
60+
61+
<template #description>
62+
<div class="flex flex-col gap-3">
63+
<p>
64+
This link has been protected by a password, please enter the
65+
password to unlock it
66+
</p>
67+
<UFormField name="password" label="Password">
68+
<UInput
69+
type="password"
70+
v-model="state.password"
71+
class="w-full"
72+
size="xl" />
73+
</UFormField>
74+
</div>
75+
</template>
76+
77+
<template #footer>
78+
<div class="flex flex-row gap-3">
79+
<UButton
80+
label="Unlock"
81+
icon="i-carbon:unlocked"
82+
type="submit"
83+
class="hover:cursor-pointer"
84+
color="success" />
85+
</div>
86+
</template>
87+
</UPageCard>
88+
</UForm>
89+
</div>
90+
</div>
91+
</template>

0 commit comments

Comments
 (0)