From f6d75964c1c73e8a3c4e1de7a36484c01813637a Mon Sep 17 00:00:00 2001 From: trev Date: Sun, 16 Mar 2025 06:50:11 -0400 Subject: [PATCH] feat: file uploads --- client/src/app.html | 1 + client/src/lib/services/user/v1/user_pb.ts | 182 +++++-- client/src/lib/ui/Button.svelte | 29 + client/src/lib/ui/Modal.svelte | 2 +- client/src/routes/(app)/+layout.svelte | 67 ++- client/src/routes/(app)/+page.svelte | 13 +- client/src/routes/(app)/items/+page.svelte | 76 ++- client/src/routes/(app)/settings/+page.svelte | 210 ++++++++ client/src/routes/auth/+page.svelte | 18 +- client/static/openapi/openapi.yaml | 157 +++++- client/vite.config.ts | 4 + flake.nix | 5 +- proto/user/v1/user.proto | 37 +- server/internal/database/migrate.go | 2 +- server/internal/handlers/auth.go | 14 +- server/internal/handlers/client.go | 19 + server/internal/handlers/file.go | 66 +++ server/internal/handlers/item.go | 10 +- server/internal/handlers/user.go | 92 +++- server/internal/interceptors/auth.go | 27 +- server/internal/models/file.go | 12 + server/internal/models/user.go | 24 + server/internal/services/user/v1/user.pb.go | 502 ++++++++++++++---- .../user/v1/userv1connect/user.connect.go | 135 +++-- server/main.go | 9 +- 25 files changed, 1393 insertions(+), 320 deletions(-) create mode 100644 client/src/lib/ui/Button.svelte create mode 100644 client/src/routes/(app)/settings/+page.svelte create mode 100644 server/internal/handlers/client.go create mode 100644 server/internal/handlers/file.go create mode 100644 server/internal/models/file.go diff --git a/client/src/app.html b/client/src/app.html index 0700a8b..d429518 100644 --- a/client/src/app.html +++ b/client/src/app.html @@ -4,6 +4,7 @@ + TrevStack %sveltekit.head% diff --git a/client/src/lib/services/user/v1/user_pb.ts b/client/src/lib/services/user/v1/user_pb.ts index 8a79ef6..10e9d71 100644 --- a/client/src/lib/services/user/v1/user_pb.ts +++ b/client/src/lib/services/user/v1/user_pb.ts @@ -10,12 +10,69 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file user/v1/user.proto. */ export const file_user_v1_user: GenFile = /*@__PURE__*/ - fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiXQoVQ2hhbmdlUGFzc3dvcmRSZXF1ZXN0EhQKDG9sZF9wYXNzd29yZBgBIAEoCRIUCgxuZXdfcGFzc3dvcmQYAiABKAkSGAoQY29uZmlybV9wYXNzd29yZBgDIAEoCSIYChZDaGFuZ2VQYXNzd29yZFJlc3BvbnNlIjsKDUFQSUtleVJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkSGAoQY29uZmlybV9wYXNzd29yZBgCIAEoCSIdCg5BUElLZXlSZXNwb25zZRILCgNrZXkYASABKAkynwEKC1VzZXJTZXJ2aWNlElMKDkNoYW5nZVBhc3N3b3JkEh4udXNlci52MS5DaGFuZ2VQYXNzd29yZFJlcXVlc3QaHy51c2VyLnYxLkNoYW5nZVBhc3N3b3JkUmVzcG9uc2UiABI7CgZBUElLZXkSFi51c2VyLnYxLkFQSUtleVJlcXVlc3QaFy51c2VyLnYxLkFQSUtleVJlc3BvbnNlIgBCnQEKC2NvbS51c2VyLnYxQglVc2VyUHJvdG9QAVpGZ2l0aHViLmNvbS9zcG90ZGVtbzQvdHJldnN0YWNrL3NlcnZlci9pbnRlcm5hbC9zZXJ2aWNlcy91c2VyL3YxO3VzZXJ2MaICA1VYWKoCB1VzZXIuVjHKAgdVc2VyXFYx4gITVXNlclxWMVxHUEJNZXRhZGF0YeoCCFVzZXI6OlYxYgZwcm90bzM"); + fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiVgoEVXNlchIKCgJpZBgBIAEoDRIQCgh1c2VybmFtZRgCIAEoCRIcCg9wcm9maWxlX3BpY3R1cmUYAyABKAlIAIgBAUISChBfcHJvZmlsZV9waWN0dXJlIhAKDkdldFVzZXJSZXF1ZXN0Ii4KD0dldFVzZXJSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIl0KFVVwZGF0ZVBhc3N3b3JkUmVxdWVzdBIUCgxvbGRfcGFzc3dvcmQYASABKAkSFAoMbmV3X3Bhc3N3b3JkGAIgASgJEhgKEGNvbmZpcm1fcGFzc3dvcmQYAyABKAkiNQoWVXBkYXRlUGFzc3dvcmRSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIj4KEEdldEFQSUtleVJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkSGAoQY29uZmlybV9wYXNzd29yZBgCIAEoCSIgChFHZXRBUElLZXlSZXNwb25zZRILCgNrZXkYASABKAkiPgobVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXF1ZXN0EhEKCWZpbGVfbmFtZRgBIAEoCRIMCgRkYXRhGAIgASgMIjsKHFVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2USGwoEdXNlchgBIAEoCzINLnVzZXIudjEuVXNlcjLPAgoLVXNlclNlcnZpY2USPgoHR2V0VXNlchIXLnVzZXIudjEuR2V0VXNlclJlcXVlc3QaGC51c2VyLnYxLkdldFVzZXJSZXNwb25zZSIAElMKDlVwZGF0ZVBhc3N3b3JkEh4udXNlci52MS5VcGRhdGVQYXNzd29yZFJlcXVlc3QaHy51c2VyLnYxLlVwZGF0ZVBhc3N3b3JkUmVzcG9uc2UiABJECglHZXRBUElLZXkSGS51c2VyLnYxLkdldEFQSUtleVJlcXVlc3QaGi51c2VyLnYxLkdldEFQSUtleVJlc3BvbnNlIgASZQoUVXBkYXRlUHJvZmlsZVBpY3R1cmUSJC51c2VyLnYxLlVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVxdWVzdBolLnVzZXIudjEuVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXNwb25zZSIAQp0BCgtjb20udXNlci52MUIJVXNlclByb3RvUAFaRmdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvc2VydmljZXMvdXNlci92MTt1c2VydjGiAgNVWFiqAgdVc2VyLlYxygIHVXNlclxWMeICE1VzZXJcVjFcR1BCTWV0YWRhdGHqAghVc2VyOjpWMWIGcHJvdG8z"); /** - * @generated from message user.v1.ChangePasswordRequest + * @generated from message user.v1.User */ -export type ChangePasswordRequest = Message<"user.v1.ChangePasswordRequest"> & { +export type User = Message<"user.v1.User"> & { + /** + * @generated from field: uint32 id = 1; + */ + id: number; + + /** + * @generated from field: string username = 2; + */ + username: string; + + /** + * @generated from field: optional string profile_picture = 3; + */ + profilePicture?: string; +}; + +/** + * Describes the message user.v1.User. + * Use `create(UserSchema)` to create a new message. + */ +export const UserSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_user_v1_user, 0); + +/** + * @generated from message user.v1.GetUserRequest + */ +export type GetUserRequest = Message<"user.v1.GetUserRequest"> & { +}; + +/** + * Describes the message user.v1.GetUserRequest. + * Use `create(GetUserRequestSchema)` to create a new message. + */ +export const GetUserRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_user_v1_user, 1); + +/** + * @generated from message user.v1.GetUserResponse + */ +export type GetUserResponse = Message<"user.v1.GetUserResponse"> & { + /** + * @generated from field: user.v1.User user = 1; + */ + user?: User; +}; + +/** + * Describes the message user.v1.GetUserResponse. + * Use `create(GetUserResponseSchema)` to create a new message. + */ +export const GetUserResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_user_v1_user, 2); + +/** + * @generated from message user.v1.UpdatePasswordRequest + */ +export type UpdatePasswordRequest = Message<"user.v1.UpdatePasswordRequest"> & { /** * @generated from field: string old_password = 1; */ @@ -33,29 +90,33 @@ export type ChangePasswordRequest = Message<"user.v1.ChangePasswordRequest"> & { }; /** - * Describes the message user.v1.ChangePasswordRequest. - * Use `create(ChangePasswordRequestSchema)` to create a new message. + * Describes the message user.v1.UpdatePasswordRequest. + * Use `create(UpdatePasswordRequestSchema)` to create a new message. */ -export const ChangePasswordRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 0); +export const UpdatePasswordRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_user_v1_user, 3); /** - * @generated from message user.v1.ChangePasswordResponse + * @generated from message user.v1.UpdatePasswordResponse */ -export type ChangePasswordResponse = Message<"user.v1.ChangePasswordResponse"> & { +export type UpdatePasswordResponse = Message<"user.v1.UpdatePasswordResponse"> & { + /** + * @generated from field: user.v1.User user = 1; + */ + user?: User; }; /** - * Describes the message user.v1.ChangePasswordResponse. - * Use `create(ChangePasswordResponseSchema)` to create a new message. + * Describes the message user.v1.UpdatePasswordResponse. + * Use `create(UpdatePasswordResponseSchema)` to create a new message. */ -export const ChangePasswordResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 1); +export const UpdatePasswordResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_user_v1_user, 4); /** - * @generated from message user.v1.APIKeyRequest + * @generated from message user.v1.GetAPIKeyRequest */ -export type APIKeyRequest = Message<"user.v1.APIKeyRequest"> & { +export type GetAPIKeyRequest = Message<"user.v1.GetAPIKeyRequest"> & { /** * @generated from field: string password = 1; */ @@ -68,16 +129,16 @@ export type APIKeyRequest = Message<"user.v1.APIKeyRequest"> & { }; /** - * Describes the message user.v1.APIKeyRequest. - * Use `create(APIKeyRequestSchema)` to create a new message. + * Describes the message user.v1.GetAPIKeyRequest. + * Use `create(GetAPIKeyRequestSchema)` to create a new message. */ -export const APIKeyRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 2); +export const GetAPIKeyRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_user_v1_user, 5); /** - * @generated from message user.v1.APIKeyResponse + * @generated from message user.v1.GetAPIKeyResponse */ -export type APIKeyResponse = Message<"user.v1.APIKeyResponse"> & { +export type GetAPIKeyResponse = Message<"user.v1.GetAPIKeyResponse"> & { /** * @generated from field: string key = 1; */ @@ -85,31 +146,86 @@ export type APIKeyResponse = Message<"user.v1.APIKeyResponse"> & { }; /** - * Describes the message user.v1.APIKeyResponse. - * Use `create(APIKeyResponseSchema)` to create a new message. + * Describes the message user.v1.GetAPIKeyResponse. + * Use `create(GetAPIKeyResponseSchema)` to create a new message. */ -export const APIKeyResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_user_v1_user, 3); +export const GetAPIKeyResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_user_v1_user, 6); + +/** + * @generated from message user.v1.UpdateProfilePictureRequest + */ +export type UpdateProfilePictureRequest = Message<"user.v1.UpdateProfilePictureRequest"> & { + /** + * @generated from field: string file_name = 1; + */ + fileName: string; + + /** + * @generated from field: bytes data = 2; + */ + data: Uint8Array; +}; + +/** + * Describes the message user.v1.UpdateProfilePictureRequest. + * Use `create(UpdateProfilePictureRequestSchema)` to create a new message. + */ +export const UpdateProfilePictureRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_user_v1_user, 7); + +/** + * @generated from message user.v1.UpdateProfilePictureResponse + */ +export type UpdateProfilePictureResponse = Message<"user.v1.UpdateProfilePictureResponse"> & { + /** + * @generated from field: user.v1.User user = 1; + */ + user?: User; +}; + +/** + * Describes the message user.v1.UpdateProfilePictureResponse. + * Use `create(UpdateProfilePictureResponseSchema)` to create a new message. + */ +export const UpdateProfilePictureResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_user_v1_user, 8); /** * @generated from service user.v1.UserService */ export const UserService: GenService<{ /** - * @generated from rpc user.v1.UserService.ChangePassword + * @generated from rpc user.v1.UserService.GetUser */ - changePassword: { + getUser: { methodKind: "unary"; - input: typeof ChangePasswordRequestSchema; - output: typeof ChangePasswordResponseSchema; + input: typeof GetUserRequestSchema; + output: typeof GetUserResponseSchema; }, /** - * @generated from rpc user.v1.UserService.APIKey + * @generated from rpc user.v1.UserService.UpdatePassword */ - aPIKey: { + updatePassword: { methodKind: "unary"; - input: typeof APIKeyRequestSchema; - output: typeof APIKeyResponseSchema; + input: typeof UpdatePasswordRequestSchema; + output: typeof UpdatePasswordResponseSchema; + }, + /** + * @generated from rpc user.v1.UserService.GetAPIKey + */ + getAPIKey: { + methodKind: "unary"; + input: typeof GetAPIKeyRequestSchema; + output: typeof GetAPIKeyResponseSchema; + }, + /** + * @generated from rpc user.v1.UserService.UpdateProfilePicture + */ + updateProfilePicture: { + methodKind: "unary"; + input: typeof UpdateProfilePictureRequestSchema; + output: typeof UpdateProfilePictureResponseSchema; }, }> = /*@__PURE__*/ serviceDesc(file_user_v1_user, 0); diff --git a/client/src/lib/ui/Button.svelte b/client/src/lib/ui/Button.svelte new file mode 100644 index 0000000..e5cfc5e --- /dev/null +++ b/client/src/lib/ui/Button.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + diff --git a/client/src/lib/ui/Modal.svelte b/client/src/lib/ui/Modal.svelte index 0e543c8..3b6794b 100644 --- a/client/src/lib/ui/Modal.svelte +++ b/client/src/lib/ui/Modal.svelte @@ -7,7 +7,7 @@ trigger, content, open = $bindable(false) - }: { trigger: Snippet; content: Snippet; open: boolean } = $props(); + }: { trigger: Snippet; content: Snippet; open?: boolean } = $props(); diff --git a/client/src/routes/(app)/+layout.svelte b/client/src/routes/(app)/+layout.svelte index fe8dee2..73d936a 100644 --- a/client/src/routes/(app)/+layout.svelte +++ b/client/src/routes/(app)/+layout.svelte @@ -9,17 +9,21 @@ House, type Icon as IconType } from '@lucide/svelte'; - import { NavigationMenu, Popover, Separator, Dialog } from 'bits-ui'; - import { fly, slide } from 'svelte/transition'; + import { NavigationMenu, Popover, Separator, Dialog, Avatar } from 'bits-ui'; + import { fade, fly, slide } from 'svelte/transition'; import { toast } from 'svelte-sonner'; import { goto } from '$app/navigation'; - import { AuthClient } from '$lib/transport'; + import { AuthClient, UserClient } from '$lib/transport'; import { page } from '$app/state'; import { cn } from '$lib/utils'; let { children } = $props(); - const username = localStorage.getItem('username'); + let user = UserClient.getUser({}).then((res) => { + return res.user; + }); + let sidebarOpen = $state(false); + let popupOpen = $state(false); type MenuItem = { name: string; @@ -46,14 +50,17 @@ async function logout() { await AuthClient.logout({}); - localStorage.removeItem('username'); await goto('/auth'); toast.success('logged out successfully'); + + if (sidebarOpen) { + sidebarOpen = false; + } }
@@ -61,7 +68,20 @@ - + + {#snippet child({ props, open })} + {#if open} +
+
+
+ {/if} + {/snippet} +
{#snippet child({ props, open })} {#if open} @@ -73,7 +93,9 @@ }} > - + {#each menuItems as item} {@const Icon = item.icon} @@ -83,7 +105,7 @@ page.url.pathname === item.href && 'bg-surface-0' )} href={item.href} - onclick={() => { + onSelect={() => { if (sidebarOpen) { sidebarOpen = false; } @@ -101,6 +123,11 @@ { + if (sidebarOpen) { + sidebarOpen = false; + } + }} > Settings @@ -146,24 +173,36 @@ - + - {username} + {#await user then user} + + + {user?.username.substring(0, 2)} + + {/await} {#snippet child({ wrapperProps, props, open })} {#if open}
-
+
{@render children()}
diff --git a/client/src/routes/(app)/+page.svelte b/client/src/routes/(app)/+page.svelte index 1e6401c..970a930 100644 --- a/client/src/routes/(app)/+page.svelte +++ b/client/src/routes/(app)/+page.svelte @@ -1,2 +1,11 @@ -

Welcome to TrevStack

-

Visit github.com/spotdemo4/trevstack to read the documentation

\ No newline at end of file +
+
+

+ Welcome to TrevStack +

+

+ Visit github.com/spotdemo4/trevstack to read + the documentation +

+
+
diff --git a/client/src/routes/(app)/items/+page.svelte b/client/src/routes/(app)/items/+page.svelte index 739f90c..2ef2739 100644 --- a/client/src/routes/(app)/items/+page.svelte +++ b/client/src/routes/(app)/items/+page.svelte @@ -2,11 +2,10 @@ import { ItemClient } from '$lib/transport'; import { Plus, Trash, Pencil } from '@lucide/svelte'; import { timestampFromDate, timestampDate } from '@bufbuild/protobuf/wkt'; - import { Dialog, Button } from 'bits-ui'; - import { fade } from 'svelte/transition'; import { toast } from 'svelte-sonner'; import { ConnectError } from '@connectrpc/connect'; import Modal from '$lib/ui/Modal.svelte'; + import Button from '$lib/ui/Button.svelte'; import { SvelteMap } from 'svelte/reactivity'; // Config @@ -67,6 +66,7 @@
+ {:then items} {#each items as item} @@ -80,16 +80,19 @@ {item.quantity}
- editsOpen.has(item.id!) ? editsOpen.get(item.id!)! : editsOpen.set(item.id!, false) && editsOpen.get(item.id!)!, - (value) => editsOpen.set(item.id!, value) - }> + + editsOpen.has(item.id!) + ? editsOpen.get(item.id!)! + : editsOpen.set(item.id!, false) && editsOpen.get(item.id!)!, + (value) => editsOpen.set(item.id!, value) + } + > {#snippet trigger()} - + {/snippet} {#snippet content()} @@ -119,7 +122,7 @@ if (response.item && item.id) { toast.success(`item "${name}" saved`); - editsOpen.set(item.id, false) + editsOpen.set(item.id, false); await updateItems(); } } catch (err) { @@ -169,27 +172,25 @@ value={item.quantity} />
- - Submit - +
{/snippet} - deletesOpen.has(item.id!) ? deletesOpen.get(item.id!)! : deletesOpen.set(item.id!, false) && deletesOpen.get(item.id!)!, - (value) => deletesOpen.set(item.id!, value) - }> + + deletesOpen.has(item.id!) + ? deletesOpen.get(item.id!)! + : deletesOpen.set(item.id!, false) && deletesOpen.get(item.id!)!, + (value) => deletesOpen.set(item.id!, value) + } + > {#snippet trigger()} - + {/snippet} {#snippet content()} @@ -206,7 +207,7 @@ }); toast.success(`item "${item.name}" deleted`); - deletesOpen.set(item.id!, false); + deletesOpen.set(item.id!, false); await updateItems(); } catch (err) { const error = ConnectError.from(err); @@ -215,15 +216,11 @@ }} >
- Are you sure you want to delete "{item.name}"?Are you sure you want to delete "{item.name}"?
- - Confirm - +
@@ -241,11 +238,9 @@
{#snippet trigger()} - + {/snippet} {#snippet content()} @@ -319,12 +314,7 @@ class="border-surface-0 rounded border p-2 text-sm" />
- - Submit - + {/snippet} diff --git a/client/src/routes/(app)/settings/+page.svelte b/client/src/routes/(app)/settings/+page.svelte new file mode 100644 index 0000000..49e7ddb --- /dev/null +++ b/client/src/routes/(app)/settings/+page.svelte @@ -0,0 +1,210 @@ + + +
+
+ {#await user then user} +
+
+ + + {user?.username.substring(0, 2)} + +
+

{user?.username}

+
+ {/await} + + + +
+ + {#snippet trigger()} + + {/snippet} + + {#snippet content()} +

+ Generate API Key +

+ {#if key == ''} +
{ + e.preventDefault(); + const form = e.target as HTMLFormElement; + const formData = new FormData(form); + + try { + const response = await UserClient.getAPIKey({ + password: formData.get('password')?.toString(), + confirmPassword: formData.get('confirm-password')?.toString() + }); + + if (response.key) { + key = response.key; + form.reset(); + } + } catch (err) { + const error = ConnectError.from(err); + toast.error(error.rawMessage); + } + }} + > +
+
+ + +
+
+ + +
+ +
+
+ {:else} +
+ {key} +
+ {/if} + {/snippet} +
+ + + {#snippet trigger()} + + {/snippet} + + {#snippet content()} +

+ Change Profile Picture +

+
{ + e.preventDefault(); + const form = e.target as HTMLFormElement; + + let fileInput = document.getElementById('file') as HTMLInputElement; + let file = fileInput.files?.[0]; + + if (!file) { + toast.error('No file selected'); + return; + } + + const data = await file.bytes(); + + try { + const response = await UserClient.updateProfilePicture({ + fileName: file.name, + data: data, + }); + + if (response.user) { + toast.success('Profile picture updated'); + form.reset(); + + } + } catch (err) { + const error = ConnectError.from(err); + toast.error(error.rawMessage); + } + }} + > +
+
+ + +
+ +
+
+ {/snippet} +
+
+ +
{ + e.preventDefault(); + const form = e.target as HTMLFormElement; + const formData = new FormData(form); + + try { + await UserClient.updatePassword({ + oldPassword: formData.get('old-password')?.toString(), + newPassword: formData.get('new-password')?.toString(), + confirmPassword: formData.get('confirm-password')?.toString() + }); + + toast.success('password updated successfully'); + form.reset(); + } catch (err) { + const error = ConnectError.from(err); + toast.error(error.rawMessage); + } + }} + class="bg-mantle border-surface-0 rounded border p-4 drop-shadow-md" + > +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
diff --git a/client/src/routes/auth/+page.svelte b/client/src/routes/auth/+page.svelte index 96470bc..5a397ca 100644 --- a/client/src/routes/auth/+page.svelte +++ b/client/src/routes/auth/+page.svelte @@ -1,10 +1,11 @@ @@ -47,7 +48,6 @@ }); if (response.token && username) { - localStorage.setItem('username', username); goto('/'); } } catch (err) { @@ -75,12 +75,7 @@ class="border-surface-0 rounded border p-2 text-sm" /> - - Submit - + @@ -138,12 +133,7 @@ class="border-surface-0 rounded border p-2 text-sm" /> - - Submit - + diff --git a/client/static/openapi/openapi.yaml b/client/static/openapi/openapi.yaml index 9eef2ad..4940a54 100644 --- a/client/static/openapi/openapi.yaml +++ b/client/static/openapi/openapi.yaml @@ -337,7 +337,7 @@ components: type: object title: SignUpResponse additionalProperties: false - user.v1.APIKeyRequest: + user.v1.GetAPIKeyRequest: type: object properties: password: @@ -346,17 +346,29 @@ components: confirmPassword: type: string title: confirm_password - title: APIKeyRequest + title: GetAPIKeyRequest additionalProperties: false - user.v1.APIKeyResponse: + user.v1.GetAPIKeyResponse: type: object properties: key: type: string title: key - title: APIKeyResponse + title: GetAPIKeyResponse additionalProperties: false - user.v1.ChangePasswordRequest: + user.v1.GetUserRequest: + type: object + title: GetUserRequest + additionalProperties: false + user.v1.GetUserResponse: + type: object + properties: + user: + title: user + $ref: '#/components/schemas/user.v1.User' + title: GetUserResponse + additionalProperties: false + user.v1.UpdatePasswordRequest: type: object properties: oldPassword: @@ -368,11 +380,50 @@ components: confirmPassword: type: string title: confirm_password - title: ChangePasswordRequest + title: UpdatePasswordRequest additionalProperties: false - user.v1.ChangePasswordResponse: + user.v1.UpdatePasswordResponse: type: object - title: ChangePasswordResponse + properties: + user: + title: user + $ref: '#/components/schemas/user.v1.User' + title: UpdatePasswordResponse + additionalProperties: false + user.v1.UpdateProfilePictureRequest: + type: object + properties: + fileName: + type: string + title: file_name + data: + type: string + title: data + format: byte + title: UpdateProfilePictureRequest + additionalProperties: false + user.v1.UpdateProfilePictureResponse: + type: object + properties: + user: + title: user + $ref: '#/components/schemas/user.v1.User' + title: UpdateProfilePictureResponse + additionalProperties: false + user.v1.User: + type: object + properties: + id: + type: integer + title: id + username: + type: string + title: username + profilePicture: + type: string + title: profile_picture + nullable: true + title: User additionalProperties: false security: - bearerAuth: [] @@ -657,12 +708,12 @@ paths: application/json: schema: $ref: '#/components/schemas/user.v1.LogoutResponse' - /user.v1.UserService/ChangePassword: + /user.v1.UserService/GetUser: post: tags: - user.v1.UserService - summary: ChangePassword - operationId: user.v1.UserService.ChangePassword + summary: GetUser + operationId: user.v1.UserService.GetUser parameters: - name: Connect-Protocol-Version in: header @@ -677,7 +728,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/user.v1.ChangePasswordRequest' + $ref: '#/components/schemas/user.v1.GetUserRequest' required: true responses: default: @@ -691,13 +742,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/user.v1.ChangePasswordResponse' - /user.v1.UserService/APIKey: + $ref: '#/components/schemas/user.v1.GetUserResponse' + /user.v1.UserService/UpdatePassword: post: tags: - user.v1.UserService - summary: APIKey - operationId: user.v1.UserService.APIKey + summary: UpdatePassword + operationId: user.v1.UserService.UpdatePassword parameters: - name: Connect-Protocol-Version in: header @@ -712,7 +763,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/user.v1.APIKeyRequest' + $ref: '#/components/schemas/user.v1.UpdatePasswordRequest' required: true responses: default: @@ -726,7 +777,77 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/user.v1.APIKeyResponse' + $ref: '#/components/schemas/user.v1.UpdatePasswordResponse' + /user.v1.UserService/GetAPIKey: + post: + tags: + - user.v1.UserService + summary: GetAPIKey + operationId: user.v1.UserService.GetAPIKey + parameters: + - name: Connect-Protocol-Version + in: header + required: true + schema: + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/user.v1.GetAPIKeyRequest' + required: true + responses: + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/connect.error' + "200": + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/user.v1.GetAPIKeyResponse' + /user.v1.UserService/UpdateProfilePicture: + post: + tags: + - user.v1.UserService + summary: UpdateProfilePicture + operationId: user.v1.UserService.UpdateProfilePicture + parameters: + - name: Connect-Protocol-Version + in: header + required: true + schema: + $ref: '#/components/schemas/connect-protocol-version' + - name: Connect-Timeout-Ms + in: header + schema: + $ref: '#/components/schemas/connect-timeout-header' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/user.v1.UpdateProfilePictureRequest' + required: true + responses: + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/connect.error' + "200": + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/user.v1.UpdateProfilePictureResponse' tags: - name: item.v1.ItemService - name: user.v1.AuthService diff --git a/client/vite.config.ts b/client/vite.config.ts index ae1d5f1..6e67bfc 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -13,6 +13,10 @@ export default defineConfig({ target: 'http://localhost:8080', changeOrigin: true, }, + '/file': { + target: 'http://localhost:8080', + changeOrigin: true, + }, }, host: '0.0.0.0', } diff --git a/flake.nix b/flake.nix index b32faec..d4b73a4 100644 --- a/flake.nix +++ b/flake.nix @@ -100,8 +100,9 @@ inotifywait -mre close_write,moved_to,create proto | while read -r _ _ basename; do echo "file changed: $basename" - buf lint - buf generate + if buf lint ; then + buf generate + fi echo "regenerated proto services" done ''; diff --git a/proto/user/v1/user.proto b/proto/user/v1/user.proto index 10a5c43..ac20f3f 100644 --- a/proto/user/v1/user.proto +++ b/proto/user/v1/user.proto @@ -2,22 +2,45 @@ syntax = "proto3"; package user.v1; -service UserService { - rpc ChangePassword (ChangePasswordRequest) returns (ChangePasswordResponse) {} - rpc APIKey (APIKeyRequest) returns (APIKeyResponse) {} +message User { + uint32 id = 1; + string username = 2; + optional string profile_picture = 3; } -message ChangePasswordRequest { +service UserService { + rpc GetUser (GetUserRequest) returns (GetUserResponse) {} + rpc UpdatePassword (UpdatePasswordRequest) returns (UpdatePasswordResponse) {} + rpc GetAPIKey (GetAPIKeyRequest) returns (GetAPIKeyResponse) {} + rpc UpdateProfilePicture (UpdateProfilePictureRequest) returns (UpdateProfilePictureResponse) {} +} + +message GetUserRequest {} +message GetUserResponse { + User user = 1; +} + +message UpdatePasswordRequest { string old_password = 1; string new_password = 2; string confirm_password = 3; } -message ChangePasswordResponse {} +message UpdatePasswordResponse { + User user = 1; +} -message APIKeyRequest { +message GetAPIKeyRequest { string password = 1; string confirm_password = 2; } -message APIKeyResponse { +message GetAPIKeyResponse { string key = 1; } + +message UpdateProfilePictureRequest { + string file_name = 1; + bytes data = 2; +} +message UpdateProfilePictureResponse { + User user = 1; +} diff --git a/server/internal/database/migrate.go b/server/internal/database/migrate.go index 8ad3f9d..cd0a7c2 100644 --- a/server/internal/database/migrate.go +++ b/server/internal/database/migrate.go @@ -6,7 +6,7 @@ import ( ) func Migrate(db *gorm.DB) error { - err := db.AutoMigrate(&models.User{}, &models.Item{}) + err := db.AutoMigrate(&models.User{}, &models.Item{}, &models.File{}) if err != nil { return err } diff --git a/server/internal/handlers/auth.go b/server/internal/handlers/auth.go index 4cb11f2..af9f0bf 100644 --- a/server/internal/handlers/auth.go +++ b/server/internal/handlers/auth.go @@ -22,10 +22,10 @@ type AuthHandler struct { key []byte } -func (s *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.LoginRequest]) (*connect.Response[userv1.LoginResponse], error) { +func (h *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.LoginRequest]) (*connect.Response[userv1.LoginResponse], error) { // Validate user := models.User{} - if err := s.db.First(&user, "username = ?", req.Msg.Username).Error; err != nil { + if err := h.db.First(&user, "username = ?", req.Msg.Username).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid username or password")) } @@ -46,7 +46,7 @@ func (s *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.Log Time: time.Now().Add(time.Hour * 24), }, }) - ss, err := t.SignedString(s.key) + ss, err := t.SignedString(h.key) if err != nil { return nil, connect.NewError(connect.CodeInternal, err) } @@ -69,9 +69,9 @@ func (s *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.Log return res, nil } -func (s *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.SignUpRequest]) (*connect.Response[userv1.SignUpResponse], error) { +func (h *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.SignUpRequest]) (*connect.Response[userv1.SignUpResponse], error) { // Validate - if err := s.db.First(&models.User{}, "username = ?", req.Msg.Username).Error; err != nil { + if err := h.db.First(&models.User{}, "username = ?", req.Msg.Username).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, connect.NewError(connect.CodeInternal, err) } @@ -93,7 +93,7 @@ func (s *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.Si Username: req.Msg.Username, Password: string(hash), } - if err := s.db.Create(&user).Error; err != nil { + if err := h.db.Create(&user).Error; err != nil { return nil, connect.NewError(connect.CodeInternal, err) } @@ -101,7 +101,7 @@ func (s *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.Si return res, nil } -func (s *AuthHandler) Logout(ctx context.Context, req *connect.Request[userv1.LogoutRequest]) (*connect.Response[userv1.LogoutResponse], error) { +func (h *AuthHandler) Logout(ctx context.Context, req *connect.Request[userv1.LogoutRequest]) (*connect.Response[userv1.LogoutResponse], error) { // Clear cookie cookie := http.Cookie{ Name: "token", diff --git a/server/internal/handlers/client.go b/server/internal/handlers/client.go new file mode 100644 index 0000000..5c18a5d --- /dev/null +++ b/server/internal/handlers/client.go @@ -0,0 +1,19 @@ +package handlers + +import ( + "embed" + "io/fs" + "log" + "net/http" + + "github.com/spotdemo4/trevstack/server/internal/interceptors" +) + +func NewClientHandler(client embed.FS, key string) http.Handler { + clientFs, err := fs.Sub(client, "client") + if err != nil { + log.Fatalf("failed to get sub filesystem: %v", err) + } + + return interceptors.WithAuthRedirect(http.FileServer(http.FS(clientFs)), key) +} diff --git a/server/internal/handlers/file.go b/server/internal/handlers/file.go new file mode 100644 index 0000000..f835e46 --- /dev/null +++ b/server/internal/handlers/file.go @@ -0,0 +1,66 @@ +package handlers + +import ( + "errors" + "log" + "net/http" + "strings" + + "github.com/spotdemo4/trevstack/server/internal/interceptors" + "github.com/spotdemo4/trevstack/server/internal/models" + "gorm.io/gorm" +) + +type FileHandler struct { + db *gorm.DB + key []byte +} + +func (h *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + userid, ok := interceptors.GetUserContext(r.Context()) + if !ok { + http.Redirect(w, r, "/auth", http.StatusFound) + return + } + + // Get the file id from the path + pathItems := strings.Split(r.URL.Path, "/") + if len(pathItems) < 3 { + http.Redirect(w, r, "/auth", http.StatusFound) + return + } + id := pathItems[2] + + // Get the file from the database + file := models.File{} + if err := h.db.First(&file, "id = ? AND user_id = ?", id, userid).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + http.Error(w, "File not found", http.StatusNotFound) + return + } + + log.Println(err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Serve the file + if r.Method == http.MethodGet { + w.Header().Set("Content-Type", http.DetectContentType(file.Data)) + w.Write(file.Data) + return + } else { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } +} + +func NewFileHandler(db *gorm.DB, key string) http.Handler { + return interceptors.WithAuthRedirect( + &FileHandler{ + db: db, + key: []byte(key), + }, + key, + ) +} diff --git a/server/internal/handlers/item.go b/server/internal/handlers/item.go index 612a452..5d8acd7 100644 --- a/server/internal/handlers/item.go +++ b/server/internal/handlers/item.go @@ -21,7 +21,7 @@ type ItemHandler struct { } func (h *ItemHandler) GetItem(ctx context.Context, req *connect.Request[itemv1.GetItemRequest]) (*connect.Response[itemv1.GetItemResponse], error) { - userid, ok := interceptors.UserFromContext(ctx) + userid, ok := interceptors.GetUserContext(ctx) if !ok { return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) } @@ -39,7 +39,7 @@ func (h *ItemHandler) GetItem(ctx context.Context, req *connect.Request[itemv1.G } func (h *ItemHandler) GetItems(ctx context.Context, req *connect.Request[itemv1.GetItemsRequest]) (*connect.Response[itemv1.GetItemsResponse], error) { - userid, ok := interceptors.UserFromContext(ctx) + userid, ok := interceptors.GetUserContext(ctx) if !ok { return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) } @@ -89,7 +89,7 @@ func (h *ItemHandler) GetItems(ctx context.Context, req *connect.Request[itemv1. } func (h *ItemHandler) CreateItem(ctx context.Context, req *connect.Request[itemv1.CreateItemRequest]) (*connect.Response[itemv1.CreateItemResponse], error) { - userid, ok := interceptors.UserFromContext(ctx) + userid, ok := interceptors.GetUserContext(ctx) if !ok { return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) } @@ -114,7 +114,7 @@ func (h *ItemHandler) CreateItem(ctx context.Context, req *connect.Request[itemv } func (h *ItemHandler) UpdateItem(ctx context.Context, req *connect.Request[itemv1.UpdateItemRequest]) (*connect.Response[itemv1.UpdateItemResponse], error) { - userid, ok := interceptors.UserFromContext(ctx) + userid, ok := interceptors.GetUserContext(ctx) if !ok { return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) } @@ -144,7 +144,7 @@ func (h *ItemHandler) UpdateItem(ctx context.Context, req *connect.Request[itemv } func (h *ItemHandler) DeleteItem(ctx context.Context, req *connect.Request[itemv1.DeleteItemRequest]) (*connect.Response[itemv1.DeleteItemResponse], error) { - userid, ok := interceptors.UserFromContext(ctx) + userid, ok := interceptors.GetUserContext(ctx) if !ok { return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) } diff --git a/server/internal/handlers/user.go b/server/internal/handlers/user.go index d94bdc3..da83144 100644 --- a/server/internal/handlers/user.go +++ b/server/internal/handlers/user.go @@ -22,15 +22,33 @@ type UserHandler struct { key []byte } -func (s *UserHandler) ChangePassword(ctx context.Context, req *connect.Request[userv1.ChangePasswordRequest]) (*connect.Response[userv1.ChangePasswordResponse], error) { - userid, ok := interceptors.UserFromContext(ctx) +func (h *UserHandler) GetUser(ctx context.Context, req *connect.Request[userv1.GetUserRequest]) (*connect.Response[userv1.GetUserResponse], error) { + userid, ok := interceptors.GetUserContext(ctx) if !ok { return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) } // Get user user := models.User{} - if err := s.db.First(&user, "id = ?", userid).Error; err != nil { + if err := h.db.Preload("ProfilePicture").First(&user, "id = ?", userid).Error; err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + + res := connect.NewResponse(&userv1.GetUserResponse{ + User: user.ToConnectV1(), + }) + return res, nil +} + +func (h *UserHandler) UpdatePassword(ctx context.Context, req *connect.Request[userv1.UpdatePasswordRequest]) (*connect.Response[userv1.UpdatePasswordResponse], error) { + userid, ok := interceptors.GetUserContext(ctx) + if !ok { + return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) + } + + // Get user + user := models.User{} + if err := h.db.First(&user, "id = ?", userid).Error; err != nil { return nil, connect.NewError(connect.CodeInternal, err) } @@ -49,23 +67,23 @@ func (s *UserHandler) ChangePassword(ctx context.Context, req *connect.Request[u } // Update password - if err := s.db.Model(&user).Update("password", string(hash)).Error; err != nil { + if err := h.db.Model(&user).Update("password", string(hash)).Error; err != nil { return nil, connect.NewError(connect.CodeInternal, err) } - res := connect.NewResponse(&userv1.ChangePasswordResponse{}) + res := connect.NewResponse(&userv1.UpdatePasswordResponse{}) return res, nil } -func (s *UserHandler) APIKey(ctx context.Context, req *connect.Request[userv1.APIKeyRequest]) (*connect.Response[userv1.APIKeyResponse], error) { - userid, ok := interceptors.UserFromContext(ctx) +func (h *UserHandler) GetAPIKey(ctx context.Context, req *connect.Request[userv1.GetAPIKeyRequest]) (*connect.Response[userv1.GetAPIKeyResponse], error) { + userid, ok := interceptors.GetUserContext(ctx) if !ok { return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) } // Get user user := models.User{} - if err := s.db.First(&user, "id = ?", userid).Error; err != nil { + if err := h.db.First(&user, "id = ?", userid).Error; err != nil { return nil, connect.NewError(connect.CodeInternal, err) } @@ -85,17 +103,71 @@ func (s *UserHandler) APIKey(ctx context.Context, req *connect.Request[userv1.AP Time: time.Now(), }, }) - ss, err := t.SignedString(s.key) + ss, err := t.SignedString(h.key) if err != nil { return nil, connect.NewError(connect.CodeInternal, err) } - res := connect.NewResponse(&userv1.APIKeyResponse{ + res := connect.NewResponse(&userv1.GetAPIKeyResponse{ Key: ss, }) return res, nil } +func (h *UserHandler) UpdateProfilePicture(ctx context.Context, req *connect.Request[userv1.UpdateProfilePictureRequest]) (*connect.Response[userv1.UpdateProfilePictureResponse], error) { + userid, ok := interceptors.GetUserContext(ctx) + if !ok { + return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) + } + + // Validate file + fileType := http.DetectContentType(req.Msg.Data) + if fileType != "image/jpeg" && fileType != "image/png" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("invalid file type")) + } + + // Save bytes into file + file := models.File{ + Name: req.Msg.FileName, + Data: req.Msg.Data, + UserID: uint(userid), + } + if err := h.db.Create(&file).Error; err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + + // Get user info + user := models.User{} + if err := h.db.First(&user, "id = ?", userid).Error; err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + + // Get old profile picture ID + var ppid *uint32 + if user.ProfilePicture != nil { + ppid = &user.ProfilePicture.ID + } + + // Update user profile picture + fid := uint(file.ID) + user.ProfilePictureID = &fid + if err := h.db.Save(&user).Error; err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + + // Delete old profile picture if exists + if ppid != nil { + if err := h.db.Delete(models.File{}, "id = ?", *ppid).Error; err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + } + + res := connect.NewResponse(&userv1.UpdateProfilePictureResponse{ + User: user.ToConnectV1(), + }) + return res, nil +} + func NewUserHandler(db *gorm.DB, key string) (string, http.Handler) { interceptors := connect.WithInterceptors(interceptors.NewAuthInterceptor(key)) diff --git a/server/internal/interceptors/auth.go b/server/internal/interceptors/auth.go index 67d1db7..a4163f0 100644 --- a/server/internal/interceptors/auth.go +++ b/server/internal/interceptors/auth.go @@ -15,7 +15,6 @@ import ( func WithAuthRedirect(next http.Handler, key string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Println("start", "method", r.Method, "path", r.URL.Path) pathItems := strings.Split(r.URL.Path, "/") if len(pathItems) < 2 { @@ -38,10 +37,14 @@ func WithAuthRedirect(next http.Handler, key string) http.Handler { cookies := getCookies(r.Header.Get("Cookie")) for _, cookie := range cookies { if cookie.Name == "token" { - _, err := validateToken(cookie.Value, key) + subject, err := validateToken(cookie.Value, key) if err == nil { - next.ServeHTTP(w, r) - return + ctx, err := newUserContext(r.Context(), subject) + if err == nil { + r = r.WithContext(ctx) + next.ServeHTTP(w, r) + return + } } } } @@ -80,7 +83,7 @@ func (i *authInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { if cookie.Name == "token" { subject, err := validateToken(cookie.Value, i.key) if err == nil { - ctx, err = i.newContext(ctx, subject) + ctx, err = newUserContext(ctx, subject) if err == nil { return next(ctx, req) } @@ -93,7 +96,7 @@ func (i *authInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { if authorization != "" && len(authorization) > 7 { subject, err := validateToken(authorization[7:], i.key) if err == nil { - ctx, err = i.newContext(ctx, subject) + ctx, err = newUserContext(ctx, subject) if err == nil { return next(ctx, req) } @@ -127,7 +130,7 @@ func (i *authInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc if cookie.Name == "token" { subject, err := validateToken(cookie.Value, i.key) if err == nil { - ctx, err = i.newContext(ctx, subject) + ctx, err = newUserContext(ctx, subject) if err == nil { return next(ctx, conn) } @@ -140,7 +143,7 @@ func (i *authInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc if authorization != "" && len(authorization) > 7 { subject, err := validateToken(authorization[7:], i.key) if err == nil { - ctx, err = i.newContext(ctx, subject) + ctx, err = newUserContext(ctx, subject) if err == nil { return next(ctx, conn) } @@ -211,8 +214,8 @@ type key int // instead of using this key directly. var userKey key -// NewContext returns a new Context that carries value u. -func (i *authInterceptor) newContext(ctx context.Context, subject string) (context.Context, error) { +// newUserContext returns a new Context that carries value u. +func newUserContext(ctx context.Context, subject string) (context.Context, error) { id, err := strconv.Atoi(subject) if err != nil { return nil, err @@ -221,8 +224,8 @@ func (i *authInterceptor) newContext(ctx context.Context, subject string) (conte return context.WithValue(ctx, userKey, id), nil } -// FromContext returns the User value stored in ctx, if any. -func UserFromContext(ctx context.Context) (int, bool) { +// getUserContext returns the User value stored in ctx, if any. +func GetUserContext(ctx context.Context) (int, bool) { u, ok := ctx.Value(userKey).(int) return u, ok } diff --git a/server/internal/models/file.go b/server/internal/models/file.go new file mode 100644 index 0000000..06b48e0 --- /dev/null +++ b/server/internal/models/file.go @@ -0,0 +1,12 @@ +package models + +type File struct { + ID uint32 `gorm:"primaryKey"` + + Name string + Data []byte + + // User + UserID uint + User User +} diff --git a/server/internal/models/user.go b/server/internal/models/user.go index ef9903d..7daaac1 100644 --- a/server/internal/models/user.go +++ b/server/internal/models/user.go @@ -1,8 +1,32 @@ package models +import ( + "fmt" + + userv1 "github.com/spotdemo4/trevstack/server/internal/services/user/v1" +) + type User struct { ID uint32 `gorm:"primaryKey"` Username string Password string + + // Profile picture + ProfilePictureID *uint + ProfilePicture *File +} + +func (u User) ToConnectV1() *userv1.User { + var ppid *string + if u.ProfilePicture != nil { + id := fmt.Sprintf("/file/%d", u.ProfilePicture.ID) + ppid = &id + } + + return &userv1.User{ + Id: u.ID, + Username: u.Username, + ProfilePicture: ppid, + } } diff --git a/server/internal/services/user/v1/user.pb.go b/server/internal/services/user/v1/user.pb.go index b7286b6..846746c 100644 --- a/server/internal/services/user/v1/user.pb.go +++ b/server/internal/services/user/v1/user.pb.go @@ -21,7 +21,147 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type ChangePasswordRequest struct { +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + ProfilePicture *string `protobuf:"bytes,3,opt,name=profile_picture,json=profilePicture,proto3,oneof" json:"profile_picture,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_user_v1_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetId() uint32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *User) GetProfilePicture() string { + if x != nil && x.ProfilePicture != nil { + return *x.ProfilePicture + } + return "" +} + +type GetUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserRequest) Reset() { + *x = GetUserRequest{} + mi := &file_user_v1_user_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserRequest) ProtoMessage() {} + +func (x *GetUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead. +func (*GetUserRequest) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{1} +} + +type GetUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserResponse) Reset() { + *x = GetUserResponse{} + mi := &file_user_v1_user_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserResponse) ProtoMessage() {} + +func (x *GetUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserResponse.ProtoReflect.Descriptor instead. +func (*GetUserResponse) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{2} +} + +func (x *GetUserResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +type UpdatePasswordRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OldPassword string `protobuf:"bytes,1,opt,name=old_password,json=oldPassword,proto3" json:"old_password,omitempty"` NewPassword string `protobuf:"bytes,2,opt,name=new_password,json=newPassword,proto3" json:"new_password,omitempty"` @@ -30,21 +170,21 @@ type ChangePasswordRequest struct { sizeCache protoimpl.SizeCache } -func (x *ChangePasswordRequest) Reset() { - *x = ChangePasswordRequest{} - mi := &file_user_v1_user_proto_msgTypes[0] +func (x *UpdatePasswordRequest) Reset() { + *x = UpdatePasswordRequest{} + mi := &file_user_v1_user_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ChangePasswordRequest) String() string { +func (x *UpdatePasswordRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ChangePasswordRequest) ProtoMessage() {} +func (*UpdatePasswordRequest) ProtoMessage() {} -func (x *ChangePasswordRequest) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[0] +func (x *UpdatePasswordRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -55,53 +195,54 @@ func (x *ChangePasswordRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ChangePasswordRequest.ProtoReflect.Descriptor instead. -func (*ChangePasswordRequest) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{0} +// Deprecated: Use UpdatePasswordRequest.ProtoReflect.Descriptor instead. +func (*UpdatePasswordRequest) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{3} } -func (x *ChangePasswordRequest) GetOldPassword() string { +func (x *UpdatePasswordRequest) GetOldPassword() string { if x != nil { return x.OldPassword } return "" } -func (x *ChangePasswordRequest) GetNewPassword() string { +func (x *UpdatePasswordRequest) GetNewPassword() string { if x != nil { return x.NewPassword } return "" } -func (x *ChangePasswordRequest) GetConfirmPassword() string { +func (x *UpdatePasswordRequest) GetConfirmPassword() string { if x != nil { return x.ConfirmPassword } return "" } -type ChangePasswordResponse struct { +type UpdatePasswordResponse struct { state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ChangePasswordResponse) Reset() { - *x = ChangePasswordResponse{} - mi := &file_user_v1_user_proto_msgTypes[1] +func (x *UpdatePasswordResponse) Reset() { + *x = UpdatePasswordResponse{} + mi := &file_user_v1_user_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ChangePasswordResponse) String() string { +func (x *UpdatePasswordResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ChangePasswordResponse) ProtoMessage() {} +func (*UpdatePasswordResponse) ProtoMessage() {} -func (x *ChangePasswordResponse) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[1] +func (x *UpdatePasswordResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -112,12 +253,19 @@ func (x *ChangePasswordResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ChangePasswordResponse.ProtoReflect.Descriptor instead. -func (*ChangePasswordResponse) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{1} +// Deprecated: Use UpdatePasswordResponse.ProtoReflect.Descriptor instead. +func (*UpdatePasswordResponse) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{4} } -type APIKeyRequest struct { +func (x *UpdatePasswordResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +type GetAPIKeyRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` ConfirmPassword string `protobuf:"bytes,2,opt,name=confirm_password,json=confirmPassword,proto3" json:"confirm_password,omitempty"` @@ -125,21 +273,21 @@ type APIKeyRequest struct { sizeCache protoimpl.SizeCache } -func (x *APIKeyRequest) Reset() { - *x = APIKeyRequest{} - mi := &file_user_v1_user_proto_msgTypes[2] +func (x *GetAPIKeyRequest) Reset() { + *x = GetAPIKeyRequest{} + mi := &file_user_v1_user_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *APIKeyRequest) String() string { +func (x *GetAPIKeyRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*APIKeyRequest) ProtoMessage() {} +func (*GetAPIKeyRequest) ProtoMessage() {} -func (x *APIKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[2] +func (x *GetAPIKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -150,47 +298,47 @@ func (x *APIKeyRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use APIKeyRequest.ProtoReflect.Descriptor instead. -func (*APIKeyRequest) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{2} +// Deprecated: Use GetAPIKeyRequest.ProtoReflect.Descriptor instead. +func (*GetAPIKeyRequest) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{5} } -func (x *APIKeyRequest) GetPassword() string { +func (x *GetAPIKeyRequest) GetPassword() string { if x != nil { return x.Password } return "" } -func (x *APIKeyRequest) GetConfirmPassword() string { +func (x *GetAPIKeyRequest) GetConfirmPassword() string { if x != nil { return x.ConfirmPassword } return "" } -type APIKeyResponse struct { +type GetAPIKeyResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *APIKeyResponse) Reset() { - *x = APIKeyResponse{} - mi := &file_user_v1_user_proto_msgTypes[3] +func (x *GetAPIKeyResponse) Reset() { + *x = GetAPIKeyResponse{} + mi := &file_user_v1_user_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *APIKeyResponse) String() string { +func (x *GetAPIKeyResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*APIKeyResponse) ProtoMessage() {} +func (*GetAPIKeyResponse) ProtoMessage() {} -func (x *APIKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_user_v1_user_proto_msgTypes[3] +func (x *GetAPIKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -201,62 +349,193 @@ func (x *APIKeyResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use APIKeyResponse.ProtoReflect.Descriptor instead. -func (*APIKeyResponse) Descriptor() ([]byte, []int) { - return file_user_v1_user_proto_rawDescGZIP(), []int{3} +// Deprecated: Use GetAPIKeyResponse.ProtoReflect.Descriptor instead. +func (*GetAPIKeyResponse) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{6} } -func (x *APIKeyResponse) GetKey() string { +func (x *GetAPIKeyResponse) GetKey() string { if x != nil { return x.Key } return "" } +type UpdateProfilePictureRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + FileName string `protobuf:"bytes,1,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateProfilePictureRequest) Reset() { + *x = UpdateProfilePictureRequest{} + mi := &file_user_v1_user_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateProfilePictureRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateProfilePictureRequest) ProtoMessage() {} + +func (x *UpdateProfilePictureRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateProfilePictureRequest.ProtoReflect.Descriptor instead. +func (*UpdateProfilePictureRequest) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdateProfilePictureRequest) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *UpdateProfilePictureRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type UpdateProfilePictureResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateProfilePictureResponse) Reset() { + *x = UpdateProfilePictureResponse{} + mi := &file_user_v1_user_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateProfilePictureResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateProfilePictureResponse) ProtoMessage() {} + +func (x *UpdateProfilePictureResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateProfilePictureResponse.ProtoReflect.Descriptor instead. +func (*UpdateProfilePictureResponse) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{8} +} + +func (x *UpdateProfilePictureResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + var File_user_v1_user_proto protoreflect.FileDescriptor var file_user_v1_user_proto_rawDesc = string([]byte{ 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x22, 0x88, 0x01, - 0x0a, 0x15, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x6f, 0x6c, 0x64, 0x5f, 0x70, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6f, - 0x6c, 0x64, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x65, - 0x77, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x29, 0x0a, - 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, - 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x18, 0x0a, 0x16, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x56, 0x0a, 0x0d, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, - 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x72, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x22, 0x0a, 0x0e, 0x41, 0x50, - 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x32, 0x9f, - 0x01, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x53, - 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x12, 0x1e, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1f, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x2e, - 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x42, 0x9d, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x42, 0x09, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x46, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x6f, 0x74, 0x64, 0x65, - 0x6d, 0x6f, 0x34, 0x2f, 0x74, 0x72, 0x65, 0x76, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, - 0x73, 0x65, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x55, 0x73, - 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, - 0x02, 0x13, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x55, 0x73, 0x65, 0x72, 0x3a, 0x3a, 0x56, 0x31, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x22, 0x74, 0x0a, + 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x2c, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x69, 0x63, + 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x72, + 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x88, 0x01, 0x01, 0x42, + 0x12, 0x0a, 0x10, 0x5f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x69, 0x63, 0x74, + 0x75, 0x72, 0x65, 0x22, 0x10, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x34, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x88, 0x01, 0x0a, 0x15, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x6f, 0x6c, 0x64, 0x5f, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x6c, 0x64, + 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x6e, 0x65, 0x77, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x50, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x3b, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x22, 0x59, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x5f, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x25, + 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x4e, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, + 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x41, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, + 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, + 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x32, 0xcf, 0x02, 0x0a, 0x0b, 0x55, 0x73, 0x65, + 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, + 0x09, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, + 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x24, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x25, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x9d, 0x01, 0x0a, 0x0b, 0x63, + 0x6f, 0x6d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x55, 0x73, 0x65, 0x72, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x6f, 0x74, 0x64, 0x65, 0x6d, 0x6f, 0x34, 0x2f, 0x74, 0x72, + 0x65, 0x76, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0xa2, + 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, + 0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x55, 0x73, 0x65, 0x72, + 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, + 0x02, 0x08, 0x55, 0x73, 0x65, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, }) var ( @@ -271,23 +550,35 @@ func file_user_v1_user_proto_rawDescGZIP() []byte { return file_user_v1_user_proto_rawDescData } -var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_user_v1_user_proto_goTypes = []any{ - (*ChangePasswordRequest)(nil), // 0: user.v1.ChangePasswordRequest - (*ChangePasswordResponse)(nil), // 1: user.v1.ChangePasswordResponse - (*APIKeyRequest)(nil), // 2: user.v1.APIKeyRequest - (*APIKeyResponse)(nil), // 3: user.v1.APIKeyResponse + (*User)(nil), // 0: user.v1.User + (*GetUserRequest)(nil), // 1: user.v1.GetUserRequest + (*GetUserResponse)(nil), // 2: user.v1.GetUserResponse + (*UpdatePasswordRequest)(nil), // 3: user.v1.UpdatePasswordRequest + (*UpdatePasswordResponse)(nil), // 4: user.v1.UpdatePasswordResponse + (*GetAPIKeyRequest)(nil), // 5: user.v1.GetAPIKeyRequest + (*GetAPIKeyResponse)(nil), // 6: user.v1.GetAPIKeyResponse + (*UpdateProfilePictureRequest)(nil), // 7: user.v1.UpdateProfilePictureRequest + (*UpdateProfilePictureResponse)(nil), // 8: user.v1.UpdateProfilePictureResponse } var file_user_v1_user_proto_depIdxs = []int32{ - 0, // 0: user.v1.UserService.ChangePassword:input_type -> user.v1.ChangePasswordRequest - 2, // 1: user.v1.UserService.APIKey:input_type -> user.v1.APIKeyRequest - 1, // 2: user.v1.UserService.ChangePassword:output_type -> user.v1.ChangePasswordResponse - 3, // 3: user.v1.UserService.APIKey:output_type -> user.v1.APIKeyResponse - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 0, // 0: user.v1.GetUserResponse.user:type_name -> user.v1.User + 0, // 1: user.v1.UpdatePasswordResponse.user:type_name -> user.v1.User + 0, // 2: user.v1.UpdateProfilePictureResponse.user:type_name -> user.v1.User + 1, // 3: user.v1.UserService.GetUser:input_type -> user.v1.GetUserRequest + 3, // 4: user.v1.UserService.UpdatePassword:input_type -> user.v1.UpdatePasswordRequest + 5, // 5: user.v1.UserService.GetAPIKey:input_type -> user.v1.GetAPIKeyRequest + 7, // 6: user.v1.UserService.UpdateProfilePicture:input_type -> user.v1.UpdateProfilePictureRequest + 2, // 7: user.v1.UserService.GetUser:output_type -> user.v1.GetUserResponse + 4, // 8: user.v1.UserService.UpdatePassword:output_type -> user.v1.UpdatePasswordResponse + 6, // 9: user.v1.UserService.GetAPIKey:output_type -> user.v1.GetAPIKeyResponse + 8, // 10: user.v1.UserService.UpdateProfilePicture:output_type -> user.v1.UpdateProfilePictureResponse + 7, // [7:11] is the sub-list for method output_type + 3, // [3:7] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_user_v1_user_proto_init() } @@ -295,13 +586,14 @@ func file_user_v1_user_proto_init() { if File_user_v1_user_proto != nil { return } + file_user_v1_user_proto_msgTypes[0].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_user_v1_user_proto_rawDesc), len(file_user_v1_user_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 9, NumExtensions: 0, NumServices: 1, }, diff --git a/server/internal/services/user/v1/userv1connect/user.connect.go b/server/internal/services/user/v1/userv1connect/user.connect.go index ca6376b..7f25e60 100644 --- a/server/internal/services/user/v1/userv1connect/user.connect.go +++ b/server/internal/services/user/v1/userv1connect/user.connect.go @@ -33,17 +33,24 @@ const ( // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( - // UserServiceChangePasswordProcedure is the fully-qualified name of the UserService's - // ChangePassword RPC. - UserServiceChangePasswordProcedure = "/user.v1.UserService/ChangePassword" - // UserServiceAPIKeyProcedure is the fully-qualified name of the UserService's APIKey RPC. - UserServiceAPIKeyProcedure = "/user.v1.UserService/APIKey" + // UserServiceGetUserProcedure is the fully-qualified name of the UserService's GetUser RPC. + UserServiceGetUserProcedure = "/user.v1.UserService/GetUser" + // UserServiceUpdatePasswordProcedure is the fully-qualified name of the UserService's + // UpdatePassword RPC. + UserServiceUpdatePasswordProcedure = "/user.v1.UserService/UpdatePassword" + // UserServiceGetAPIKeyProcedure is the fully-qualified name of the UserService's GetAPIKey RPC. + UserServiceGetAPIKeyProcedure = "/user.v1.UserService/GetAPIKey" + // UserServiceUpdateProfilePictureProcedure is the fully-qualified name of the UserService's + // UpdateProfilePicture RPC. + UserServiceUpdateProfilePictureProcedure = "/user.v1.UserService/UpdateProfilePicture" ) // UserServiceClient is a client for the user.v1.UserService service. type UserServiceClient interface { - ChangePassword(context.Context, *connect.Request[v1.ChangePasswordRequest]) (*connect.Response[v1.ChangePasswordResponse], error) - APIKey(context.Context, *connect.Request[v1.APIKeyRequest]) (*connect.Response[v1.APIKeyResponse], error) + GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.GetUserResponse], error) + UpdatePassword(context.Context, *connect.Request[v1.UpdatePasswordRequest]) (*connect.Response[v1.UpdatePasswordResponse], error) + GetAPIKey(context.Context, *connect.Request[v1.GetAPIKeyRequest]) (*connect.Response[v1.GetAPIKeyResponse], error) + UpdateProfilePicture(context.Context, *connect.Request[v1.UpdateProfilePictureRequest]) (*connect.Response[v1.UpdateProfilePictureResponse], error) } // NewUserServiceClient constructs a client for the user.v1.UserService service. By default, it uses @@ -57,16 +64,28 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts .. baseURL = strings.TrimRight(baseURL, "/") userServiceMethods := v1.File_user_v1_user_proto.Services().ByName("UserService").Methods() return &userServiceClient{ - changePassword: connect.NewClient[v1.ChangePasswordRequest, v1.ChangePasswordResponse]( + getUser: connect.NewClient[v1.GetUserRequest, v1.GetUserResponse]( httpClient, - baseURL+UserServiceChangePasswordProcedure, - connect.WithSchema(userServiceMethods.ByName("ChangePassword")), + baseURL+UserServiceGetUserProcedure, + connect.WithSchema(userServiceMethods.ByName("GetUser")), connect.WithClientOptions(opts...), ), - aPIKey: connect.NewClient[v1.APIKeyRequest, v1.APIKeyResponse]( + updatePassword: connect.NewClient[v1.UpdatePasswordRequest, v1.UpdatePasswordResponse]( httpClient, - baseURL+UserServiceAPIKeyProcedure, - connect.WithSchema(userServiceMethods.ByName("APIKey")), + baseURL+UserServiceUpdatePasswordProcedure, + connect.WithSchema(userServiceMethods.ByName("UpdatePassword")), + connect.WithClientOptions(opts...), + ), + getAPIKey: connect.NewClient[v1.GetAPIKeyRequest, v1.GetAPIKeyResponse]( + httpClient, + baseURL+UserServiceGetAPIKeyProcedure, + connect.WithSchema(userServiceMethods.ByName("GetAPIKey")), + connect.WithClientOptions(opts...), + ), + updateProfilePicture: connect.NewClient[v1.UpdateProfilePictureRequest, v1.UpdateProfilePictureResponse]( + httpClient, + baseURL+UserServiceUpdateProfilePictureProcedure, + connect.WithSchema(userServiceMethods.ByName("UpdateProfilePicture")), connect.WithClientOptions(opts...), ), } @@ -74,24 +93,38 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts .. // userServiceClient implements UserServiceClient. type userServiceClient struct { - changePassword *connect.Client[v1.ChangePasswordRequest, v1.ChangePasswordResponse] - aPIKey *connect.Client[v1.APIKeyRequest, v1.APIKeyResponse] + getUser *connect.Client[v1.GetUserRequest, v1.GetUserResponse] + updatePassword *connect.Client[v1.UpdatePasswordRequest, v1.UpdatePasswordResponse] + getAPIKey *connect.Client[v1.GetAPIKeyRequest, v1.GetAPIKeyResponse] + updateProfilePicture *connect.Client[v1.UpdateProfilePictureRequest, v1.UpdateProfilePictureResponse] } -// ChangePassword calls user.v1.UserService.ChangePassword. -func (c *userServiceClient) ChangePassword(ctx context.Context, req *connect.Request[v1.ChangePasswordRequest]) (*connect.Response[v1.ChangePasswordResponse], error) { - return c.changePassword.CallUnary(ctx, req) +// GetUser calls user.v1.UserService.GetUser. +func (c *userServiceClient) GetUser(ctx context.Context, req *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.GetUserResponse], error) { + return c.getUser.CallUnary(ctx, req) } -// APIKey calls user.v1.UserService.APIKey. -func (c *userServiceClient) APIKey(ctx context.Context, req *connect.Request[v1.APIKeyRequest]) (*connect.Response[v1.APIKeyResponse], error) { - return c.aPIKey.CallUnary(ctx, req) +// UpdatePassword calls user.v1.UserService.UpdatePassword. +func (c *userServiceClient) UpdatePassword(ctx context.Context, req *connect.Request[v1.UpdatePasswordRequest]) (*connect.Response[v1.UpdatePasswordResponse], error) { + return c.updatePassword.CallUnary(ctx, req) +} + +// GetAPIKey calls user.v1.UserService.GetAPIKey. +func (c *userServiceClient) GetAPIKey(ctx context.Context, req *connect.Request[v1.GetAPIKeyRequest]) (*connect.Response[v1.GetAPIKeyResponse], error) { + return c.getAPIKey.CallUnary(ctx, req) +} + +// UpdateProfilePicture calls user.v1.UserService.UpdateProfilePicture. +func (c *userServiceClient) UpdateProfilePicture(ctx context.Context, req *connect.Request[v1.UpdateProfilePictureRequest]) (*connect.Response[v1.UpdateProfilePictureResponse], error) { + return c.updateProfilePicture.CallUnary(ctx, req) } // UserServiceHandler is an implementation of the user.v1.UserService service. type UserServiceHandler interface { - ChangePassword(context.Context, *connect.Request[v1.ChangePasswordRequest]) (*connect.Response[v1.ChangePasswordResponse], error) - APIKey(context.Context, *connect.Request[v1.APIKeyRequest]) (*connect.Response[v1.APIKeyResponse], error) + GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.GetUserResponse], error) + UpdatePassword(context.Context, *connect.Request[v1.UpdatePasswordRequest]) (*connect.Response[v1.UpdatePasswordResponse], error) + GetAPIKey(context.Context, *connect.Request[v1.GetAPIKeyRequest]) (*connect.Response[v1.GetAPIKeyResponse], error) + UpdateProfilePicture(context.Context, *connect.Request[v1.UpdateProfilePictureRequest]) (*connect.Response[v1.UpdateProfilePictureResponse], error) } // NewUserServiceHandler builds an HTTP handler from the service implementation. It returns the path @@ -101,24 +134,40 @@ type UserServiceHandler interface { // and JSON codecs. They also support gzip compression. func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { userServiceMethods := v1.File_user_v1_user_proto.Services().ByName("UserService").Methods() - userServiceChangePasswordHandler := connect.NewUnaryHandler( - UserServiceChangePasswordProcedure, - svc.ChangePassword, - connect.WithSchema(userServiceMethods.ByName("ChangePassword")), + userServiceGetUserHandler := connect.NewUnaryHandler( + UserServiceGetUserProcedure, + svc.GetUser, + connect.WithSchema(userServiceMethods.ByName("GetUser")), connect.WithHandlerOptions(opts...), ) - userServiceAPIKeyHandler := connect.NewUnaryHandler( - UserServiceAPIKeyProcedure, - svc.APIKey, - connect.WithSchema(userServiceMethods.ByName("APIKey")), + userServiceUpdatePasswordHandler := connect.NewUnaryHandler( + UserServiceUpdatePasswordProcedure, + svc.UpdatePassword, + connect.WithSchema(userServiceMethods.ByName("UpdatePassword")), + connect.WithHandlerOptions(opts...), + ) + userServiceGetAPIKeyHandler := connect.NewUnaryHandler( + UserServiceGetAPIKeyProcedure, + svc.GetAPIKey, + connect.WithSchema(userServiceMethods.ByName("GetAPIKey")), + connect.WithHandlerOptions(opts...), + ) + userServiceUpdateProfilePictureHandler := connect.NewUnaryHandler( + UserServiceUpdateProfilePictureProcedure, + svc.UpdateProfilePicture, + connect.WithSchema(userServiceMethods.ByName("UpdateProfilePicture")), connect.WithHandlerOptions(opts...), ) return "/user.v1.UserService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case UserServiceChangePasswordProcedure: - userServiceChangePasswordHandler.ServeHTTP(w, r) - case UserServiceAPIKeyProcedure: - userServiceAPIKeyHandler.ServeHTTP(w, r) + case UserServiceGetUserProcedure: + userServiceGetUserHandler.ServeHTTP(w, r) + case UserServiceUpdatePasswordProcedure: + userServiceUpdatePasswordHandler.ServeHTTP(w, r) + case UserServiceGetAPIKeyProcedure: + userServiceGetAPIKeyHandler.ServeHTTP(w, r) + case UserServiceUpdateProfilePictureProcedure: + userServiceUpdateProfilePictureHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -128,10 +177,18 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption // UnimplementedUserServiceHandler returns CodeUnimplemented from all methods. type UnimplementedUserServiceHandler struct{} -func (UnimplementedUserServiceHandler) ChangePassword(context.Context, *connect.Request[v1.ChangePasswordRequest]) (*connect.Response[v1.ChangePasswordResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.UserService.ChangePassword is not implemented")) +func (UnimplementedUserServiceHandler) GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.GetUserResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.UserService.GetUser is not implemented")) } -func (UnimplementedUserServiceHandler) APIKey(context.Context, *connect.Request[v1.APIKeyRequest]) (*connect.Response[v1.APIKeyResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.UserService.APIKey is not implemented")) +func (UnimplementedUserServiceHandler) UpdatePassword(context.Context, *connect.Request[v1.UpdatePasswordRequest]) (*connect.Response[v1.UpdatePasswordResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.UserService.UpdatePassword is not implemented")) +} + +func (UnimplementedUserServiceHandler) GetAPIKey(context.Context, *connect.Request[v1.GetAPIKeyRequest]) (*connect.Response[v1.GetAPIKeyResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.UserService.GetAPIKey is not implemented")) +} + +func (UnimplementedUserServiceHandler) UpdateProfilePicture(context.Context, *connect.Request[v1.UpdateProfilePictureRequest]) (*connect.Response[v1.UpdateProfilePictureResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.UserService.UpdateProfilePicture is not implemented")) } diff --git a/server/main.go b/server/main.go index e00c921..9973bee 100644 --- a/server/main.go +++ b/server/main.go @@ -4,7 +4,6 @@ import ( "context" "embed" "fmt" - "io/fs" "log" "net/http" "os" @@ -21,7 +20,6 @@ import ( "github.com/spotdemo4/trevstack/server/internal/database" "github.com/spotdemo4/trevstack/server/internal/handlers" - "github.com/spotdemo4/trevstack/server/internal/interceptors" ) //go:embed all:client @@ -118,11 +116,8 @@ func main() { // Serve web interface mux := http.NewServeMux() - clientFs, err := fs.Sub(client, "client") - if err != nil { - log.Fatalf("failed to get sub filesystem: %v", err) - } - mux.Handle("/", interceptors.WithAuthRedirect(http.FileServer(http.FS(clientFs)), env.Key)) + mux.Handle("/", handlers.NewClientHandler(client, env.Key)) + mux.Handle("/file/", handlers.NewFileHandler(db, env.Key)) mux.Handle("/grpc/", http.StripPrefix("/grpc", api)) // Start server