diff --git a/backend/cmd/clerk/sync.go b/backend/cmd/cli/users.go similarity index 52% rename from backend/cmd/clerk/sync.go rename to backend/cmd/cli/users.go index fecefbcad..4d57fcf7b 100644 --- a/backend/cmd/clerk/sync.go +++ b/backend/cmd/cli/users.go @@ -6,6 +6,7 @@ import ( "log" "github.com/generate/selfserve/config" + "github.com/generate/selfserve/internal/models" "github.com/generate/selfserve/internal/repository" "github.com/generate/selfserve/internal/service/clerk" storage "github.com/generate/selfserve/internal/service/storage/postgres" @@ -26,8 +27,7 @@ func main() { defer repo.Close() usersRepo := repository.NewUsersRepository(repo.DB) - path := "/users" - err = syncUsers(ctx, cfg.BaseURL+path, cfg.SecretKey, usersRepo) + err = syncUsers(ctx, cfg.BaseURL, cfg.SecretKey, usersRepo) if err != nil { log.Fatal(err) } @@ -37,19 +37,34 @@ func main() { func syncUsers(ctx context.Context, clerkBaseURL string, clerkSecret string, usersRepo storage.UsersRepository) error { - users, err := clerk.FetchUsersFromClerk(clerkBaseURL, clerkSecret) + users, err := clerk.FetchUsersFromClerk(clerkBaseURL+"/users", clerkSecret); if err != nil { - return err + return fmt.Errorf("failed to fetch users: %w", err); } - transformed, err := clerk.ValidateAndReformatUserData(users) - if err != nil { - return err - } + for _, u := range users { + if len(u.OrganizationMemberships) == 0 { + log.Printf("skipping user %s: no org membership found", u.ID); + continue; + } + + orgID := u.OrganizationMemberships[0].Organization.ID - if err := usersRepo.BulkInsertUsers(ctx, transformed); err != nil { - return fmt.Errorf("failed to insert users: %w", err) + createUser := &models.CreateUser{ + ID: u.ID, + FirstName: u.FirstName, + LastName: u.LastName, + HotelID: orgID, + ProfilePicture: u.ImageUrl, + } + + if _, err := usersRepo.InsertUser(ctx, createUser); err != nil { + log.Printf("failed to insert user %s: %v", u.ID, err); + continue; + } + + log.Printf("inserted user %s", u.ID); } return nil -} +} \ No newline at end of file diff --git a/backend/internal/models/users.go b/backend/internal/models/users.go index 6cfc94aa6..7ac4e6fbd 100644 --- a/backend/internal/models/users.go +++ b/backend/internal/models/users.go @@ -6,7 +6,7 @@ type CreateUser struct { ID string `json:"id" validate:"notblank" example:"user_123"` FirstName string `json:"first_name" validate:"notblank" example:"John"` LastName string `json:"last_name" validate:"notblank" example:"Doe"` - HotelID string `json:"hotel_id" validate:"notblank" example:"org_550e8400-e29b-41d4-a716-446655440000"` + HotelID string `json:"hotel_id" validate:"notblank" example:"550e8400-e29b-41d4-a716-446655440000"` EmployeeID *string `json:"employee_id,omitempty" validate:"omitempty" example:"EMP-1234"` ProfilePicture *string `json:"profile_picture,omitempty" validate:"omitempty,url" example:"https://example.com/john.jpg"` Role *string `json:"role,omitempty" validate:"omitempty" example:"Receptionist"` diff --git a/clients/mobile/app/_layout.tsx b/clients/mobile/app/_layout.tsx index 46fe72175..3df158fde 100644 --- a/clients/mobile/app/_layout.tsx +++ b/clients/mobile/app/_layout.tsx @@ -1,4 +1,4 @@ -import { Stack } from "expo-router"; +import { Stack, Redirect } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; diff --git a/clients/mobile/app/no-org.tsx b/clients/mobile/app/no-org.tsx new file mode 100644 index 000000000..137b7b7ac --- /dev/null +++ b/clients/mobile/app/no-org.tsx @@ -0,0 +1,11 @@ +import { View, Text } from "react-native"; + +export default function NoOrg() { + return ( + + + No Organization Found + + + ); +} diff --git a/clients/shared/src/api/client.ts b/clients/shared/src/api/client.ts index 648ae378f..4da8b4fbf 100644 --- a/clients/shared/src/api/client.ts +++ b/clients/shared/src/api/client.ts @@ -7,8 +7,8 @@ import { getConfig } from "./config"; export const createRequest = ( getToken: () => Promise, baseUrl: string, + hotelId: string, ) => { - const hardCodedHotelId = "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" return async (config: RequestConfig): Promise => { let fullUrl = `${baseUrl}${config.url}`; if (config.params && Object.keys(config.params).length > 0) { @@ -24,7 +24,7 @@ export const createRequest = ( headers: { "Content-Type": "application/json", ...(token && { Authorization: `Bearer ${token}` }), - "X-Hotel-ID": hardCodedHotelId, + "X-Hotel-ID": hotelId, ...config.headers, }, body: config.data ? JSON.stringify(config.data) : undefined, @@ -74,9 +74,9 @@ export const useAPIClient = (): HttpClient => { // can be called during app startup (e.g. in a useEffect) // before any API calls are executed. const request = async (config: RequestConfig): Promise => { - const { getToken } = getConfig(); + const { getToken, hotelId } = getConfig(); const baseUrl = getBaseUrl(); - const doRequest = createRequest(getToken, baseUrl); + const doRequest = createRequest(getToken, baseUrl, hotelId); return doRequest(config); }; diff --git a/clients/shared/src/api/orval-mutator.ts b/clients/shared/src/api/orval-mutator.ts index 927ca8e5e..ad0d90a55 100755 --- a/clients/shared/src/api/orval-mutator.ts +++ b/clients/shared/src/api/orval-mutator.ts @@ -10,8 +10,8 @@ import { RequestConfig } from "../types/api.types"; export const useCustomInstance = (): (( config: RequestConfig, ) => Promise) => { - const { getToken } = getConfig(); - const request = createRequest(getToken, getBaseUrl()); + const { getToken, hotelId } = getConfig(); + const request = createRequest(getToken, getBaseUrl(), hotelId); return async (config: RequestConfig): Promise => { const response = await request(config); diff --git a/clients/web/src/routeTree.gen.ts b/clients/web/src/routeTree.gen.ts index 385331118..d03f3cd37 100644 --- a/clients/web/src/routeTree.gen.ts +++ b/clients/web/src/routeTree.gen.ts @@ -11,6 +11,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as SignUpRouteImport } from './routes/sign-up' import { Route as SignInRouteImport } from './routes/sign-in' +import { Route as NoOrgRouteImport } from './routes/no-org' import { Route as ProtectedRouteImport } from './routes/_protected' import { Route as IndexRouteImport } from './routes/index' import { Route as ProtectedTestApiRouteImport } from './routes/_protected/test-api' @@ -32,6 +33,11 @@ const SignInRoute = SignInRouteImport.update({ path: '/sign-in', getParentRoute: () => rootRouteImport, } as any) +const NoOrgRoute = NoOrgRouteImport.update({ + id: '/no-org', + path: '/no-org', + getParentRoute: () => rootRouteImport, +} as any) const ProtectedRoute = ProtectedRouteImport.update({ id: '/_protected', getParentRoute: () => rootRouteImport, @@ -84,6 +90,7 @@ const ProtectedGuestsGuestIdRoute = ProtectedGuestsGuestIdRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/no-org': typeof NoOrgRoute '/sign-in': typeof SignInRoute '/sign-up': typeof SignUpRoute '/home': typeof ProtectedHomeRoute @@ -97,6 +104,7 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/': typeof IndexRoute + '/no-org': typeof NoOrgRoute '/sign-in': typeof SignInRoute '/sign-up': typeof SignUpRoute '/home': typeof ProtectedHomeRoute @@ -111,6 +119,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/_protected': typeof ProtectedRouteWithChildren + '/no-org': typeof NoOrgRoute '/sign-in': typeof SignInRoute '/sign-up': typeof SignUpRoute '/_protected/home': typeof ProtectedHomeRoute @@ -126,6 +135,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/no-org' | '/sign-in' | '/sign-up' | '/home' @@ -139,6 +149,7 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/' + | '/no-org' | '/sign-in' | '/sign-up' | '/home' @@ -152,6 +163,7 @@ export interface FileRouteTypes { | '__root__' | '/' | '/_protected' + | '/no-org' | '/sign-in' | '/sign-up' | '/_protected/home' @@ -167,6 +179,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute ProtectedRoute: typeof ProtectedRouteWithChildren + NoOrgRoute: typeof NoOrgRoute SignInRoute: typeof SignInRoute SignUpRoute: typeof SignUpRoute } @@ -187,6 +200,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SignInRouteImport parentRoute: typeof rootRouteImport } + '/no-org': { + id: '/no-org' + path: '/no-org' + fullPath: '/no-org' + preLoaderRoute: typeof NoOrgRouteImport + parentRoute: typeof rootRouteImport + } '/_protected': { id: '/_protected' path: '' @@ -299,6 +319,7 @@ const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ProtectedRoute: ProtectedRouteWithChildren, + NoOrgRoute: NoOrgRoute, SignInRoute: SignInRoute, SignUpRoute: SignUpRoute, } diff --git a/clients/web/src/routes/__root.tsx b/clients/web/src/routes/__root.tsx index 502af6834..0d22603db 100644 --- a/clients/web/src/routes/__root.tsx +++ b/clients/web/src/routes/__root.tsx @@ -1,4 +1,8 @@ -import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router"; +import { + HeadContent, + Scripts, + createRootRoute +} from "@tanstack/react-router"; import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import { TanStackDevtools } from "@tanstack/react-devtools"; import { ClerkProvider } from "@clerk/clerk-react"; diff --git a/clients/web/src/routes/no-org.tsx b/clients/web/src/routes/no-org.tsx new file mode 100644 index 000000000..6b91bdc58 --- /dev/null +++ b/clients/web/src/routes/no-org.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/no-org")({ + component: NoOrgPage, +}); + +function NoOrgPage() { + return ( + + No Organization Found + + You're not part of a hotel organization yet. Please contact your manager + for an invitation. + + + ); +}
+ You're not part of a hotel organization yet. Please contact your manager + for an invitation. +