Skip to content

Commit 1d60daa

Browse files
feat: add shared types and Hono rpc (#51)
1 parent 16f6114 commit 1d60daa

29 files changed

+191
-208
lines changed

app/components/Toolbar.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import useAuth from '~/composables/useAuth';
33
4-
const { logOut } = useAuth();
4+
const { logout } = useAuth();
55
66
const menu = [
77
[
@@ -20,7 +20,7 @@ const menu = [
2020
label: 'Logout',
2121
icon: 'websymbol:logout',
2222
async click() {
23-
const result = await logOut();
23+
const result = await logout();
2424
result.onSuccess(() => navigateTo('/login'));
2525
}
2626
}

app/composables/useAuth.ts

+22-22
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
1-
import { Result } from 'typescript-result';
2-
import type { User, UserCredentials } from '~~/shared/types';
1+
import { AsyncResult, Result } from 'typescript-result';
2+
import type { User, UserCredentials } from '#shared/types';
33

4-
export default () => {
5-
const loginUser = (
6-
credentials: UserCredentials
7-
): Promise<Result<User, Error>> =>
4+
export default function () {
5+
const loginUser = (credentials: UserCredentials): AsyncResult<User, Error> =>
86
Result.try(async () => {
9-
const res = await $fetch('/api/auth/login', {
10-
method: 'POST',
11-
body: credentials
7+
const res = await client.auth.login.$post({
8+
json: credentials
129
});
13-
14-
if (res.error) {
15-
throw res.error;
10+
if (!res.ok) {
11+
const errorMessage = await res.text();
12+
throw new Error(errorMessage);
1613
}
1714

18-
console.log(res);
19-
20-
return res;
15+
const userBody = await res.json();
16+
return userBody;
2117
});
2218

23-
const logOut = (): Promise<Result<void, Error>> =>
19+
const logout = (): AsyncResult<void, Error> =>
2420
Result.try(async () => {
25-
const req = await $fetch('/api/auth/logout', {
26-
method: 'POST'
21+
const sessionToken = useCookie('auth_session');
22+
const res = await client.auth.logout.$post(undefined, {
23+
headers: {
24+
Cookie: `auth_session=${sessionToken}`
25+
}
2726
});
2827

29-
if (req.error) {
30-
throw req.error;
28+
if (!res.ok) {
29+
const errorMessage = await res.text();
30+
throw new Error(errorMessage);
3131
}
3232
});
3333

3434
return {
3535
loginUser,
36-
logOut
36+
logout
3737
};
38-
};
38+
}

app/composables/useProfile.ts

+25-16
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,43 @@
1-
import { Result } from 'typescript-result';
2-
import type { Profile, User, UserCredentials } from '~~/shared/types';
1+
import { AsyncResult, Result } from 'typescript-result';
2+
import type { Profile } from '#shared/types';
33

44
export default () => {
55
const headers = useRequestHeaders(['cookie']);
6+
const cookie = headers.cookie ?? '';
67

7-
const getProfiles = (): Promise<Result<Profile[], Error>> =>
8+
const getProfiles = (): AsyncResult<Profile[], Error> =>
89
Result.try(async () => {
9-
const req = await $fetch('/api/profile/all', {
10-
method: 'GET'
10+
const res = await client.profile.all.$get(undefined, {
11+
headers: {
12+
Cookie: cookie
13+
}
1114
});
12-
13-
if (req.error) {
14-
throw req.error;
15+
if (!res.ok) {
16+
const errorMessage = await res.text();
17+
throw new Error(errorMessage);
1518
}
1619

17-
return req;
20+
const profiles = await res.json();
21+
return profiles;
1822
});
1923

20-
const getSelf = (): Promise<Result<Profile, Error>> =>
24+
const getSelf = (): AsyncResult<Profile, Error> =>
2125
Result.try(async () => {
22-
const req = await $fetch('/api/profile', {
23-
method: 'GET',
24-
headers: headers
26+
console.log('In server, before request call');
27+
const res = await client.profile.$get(undefined, {
28+
headers: {
29+
Cookie: cookie
30+
}
2531
});
32+
console.log('In server, AFTER request call');
2633

27-
if (req.error) {
28-
throw req.error;
34+
if (!res.ok) {
35+
const errorMessage = await res.text();
36+
throw new Error(errorMessage);
2937
}
3038

31-
return req;
39+
const profile = await res.json();
40+
return profile;
3241
});
3342

3443
return {

app/composables/useTitle.ts

-5
This file was deleted.

app/pages/login.vue

+19-55
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,42 @@
11
<script lang="ts" setup>
2-
import { passwordPattern } from '~~/shared/types';
3-
import { z } from 'zod';
4-
import useAuth from '~/composables/useAuth';
2+
import { postLoginBody } from '#shared/schemas';
3+
import type { UserCredentials } from '~~/shared/types';
54
6-
const { loginUser, logOut } = useAuth();
7-
const profileStore = useProfileStore();
8-
9-
const userCredentials = reactive({
5+
const userCredentials = reactive<UserCredentials>({
106
email: '',
117
password: ''
128
});
13-
const loginSchema = z.object({
14-
email: z
15-
.string({ message: 'Email is required' })
16-
.email('Invalid email address'),
17-
password: z
18-
.string({ message: 'Password is required' })
19-
.regex(
20-
passwordPattern,
21-
'Password must contain at least 8 characters with one lowercase letter, one uppercase letter and one number'
22-
)
23-
});
24-
const errors = ref({
25-
emailError: [] as string[],
26-
passwordError: [] as string[]
27-
});
28-
const handleLogin = async () => {
29-
const validateSchema = async () => {
30-
const validatedSchema = await loginSchema.safeParseAsync(userCredentials);
31-
if (!validatedSchema.success) {
32-
const error = validatedSchema.error.format();
33-
errors.value.emailError = error.email?._errors ?? [];
34-
errors.value.passwordError = error.password?._errors ?? [];
35-
return;
36-
}
37-
return validatedSchema.data;
38-
};
399
40-
try {
41-
const data = validateSchema();
42-
if (!data) return;
10+
const handleLogin = async () => {
11+
// UForm already validates the form
12+
const { loginUser } = useAuth();
13+
const loginResult = await loginUser(userCredentials);
4314
44-
const response = await loginUser(userCredentials);
45-
response.fold(
46-
userState => {
47-
console.log('We here =)');
48-
profileStore.setProfile(userState);
49-
navigateTo('/');
50-
},
51-
error => {
52-
console.error(error);
53-
}
54-
);
55-
} catch (error) {
56-
console.log('we here =(');
57-
console.log(error);
58-
}
59-
// handle form submission
15+
loginResult.fold(
16+
() => navigateTo('/'),
17+
error => {
18+
alert(`Server error: ${error.message}`);
19+
}
20+
);
6021
};
6122
</script>
6223

6324
<template>
6425
<h2 class="font-semibold text-5xl text-center">Login Screen</h2>
6526
<UContainer class="flex flex-col justify-center items-center">
66-
<UForm :schema="loginSchema" :state="userCredentials" @submit="handleLogin">
67-
<UFormGroup label="Email" required>
27+
<UForm
28+
:schema="postLoginBody"
29+
:state="userCredentials"
30+
@submit="handleLogin">
31+
<UFormGroup label="Email" name="email" required>
6832
<UInput
6933
v-model="userCredentials.email"
7034
placeholder="Enter Email"
7135
type="email"
7236
icon="i-heroicons-envelope" />
7337
</UFormGroup>
7438

75-
<UFormGroup label="Password" required>
39+
<UFormGroup label="Password" name="password" required>
7640
<UInput
7741
v-model="userCredentials.password"
7842
placeholder="Enter Password"

app/pages/users.vue

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
<script setup lang="ts">
2-
import { type CreateUserDetails, roles } from '~~/shared/types';
2+
import { roles } from '#shared/types';
33
import { z } from 'zod';
44
import useProfile from '~/composables/useProfile';
55
import useAdmin from '~/composables/useAdmin';
6-
import useTitle from '~/composables/useTitle';
76
87
// UI related properties
98
const isAddUserPopupOpen = ref(false);
@@ -98,8 +97,6 @@ async function handleSubmit() {
9897
definePageMeta({
9998
middleware: ['require-auth']
10099
});
101-
102-
useTitle('Users');
103100
</script>
104101

105102
<template>

app/stores/profile.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineStore } from 'pinia';
2-
import type { Profile, User } from '~~/shared/types';
2+
import type { Profile } from '~~/shared/types';
33

44
export const useProfileStore = defineStore('user', () => {
55
const profile = ref<Profile | null>(null);

app/utils/httpClient.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { hc } from 'hono/client';
2+
import type { PerryApi } from '#shared/types';
3+
4+
/*
5+
* The reason why different URLs are supplied here is because during server rendering, the
6+
* default fetch function used by `hc` does have the necessary context of the server's URL.
7+
* Therefore, the URL must be supplied manually. In the production environment, the URL may
8+
* have to change to use the actual server URL.
9+
* However, in the browser (client-side), the URL is relative to the current domain.
10+
*/
11+
export const client = hc<PerryApi>(
12+
import.meta.server ? 'http://localhost:3000/api' : '/api'
13+
);

server/src/auth/__tests__/createUser.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { eq, sql } from 'drizzle-orm';
44
import { testClient } from 'hono/testing';
55
import app from '../../app';
66
import { db } from '../../drizzle';
7-
import { roles } from '../../types';
7+
import { roles, type Role } from '../../types';
88
import { users } from '../schema';
99

10-
const sessionIds: Partial<Record<(typeof roles)[number], string>> = {};
10+
const sessionIds: Partial<Record<Role, string>> = {};
1111

1212
const client = testClient(app);
1313

server/src/auth/__tests__/deleteUser.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { describe, expect, test, beforeAll } from 'bun:test';
22
import { db } from '../../drizzle';
33
import { users } from '../schema';
4-
import { roles } from '../../types';
4+
import { roles, type Role } from '../../types';
55
import { createUserWithSession } from '../../testHelpers';
66
import { testClient } from 'hono/testing';
77
import app from '../../app';
88
import { eq, sql } from 'drizzle-orm';
99

10-
const sessionIds: Partial<Record<(typeof roles)[number], string>> = {};
11-
const userIds: Partial<Record<(typeof roles)[number], string>> = {};
10+
const sessionIds: Partial<Record<Role, string>> = {};
11+
const userIds: Partial<Record<Role, string>> = {};
1212

1313
const client = testClient(app);
1414

0 commit comments

Comments
 (0)