mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-06-30 10:35:19 +02:00
Merge pull request #365 from wasp-lang/update-updateUser-info
remove user lastActiveTimestamp property
This commit is contained in:
@ -104,7 +104,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -212,9 +208,9 @@
|
@@ -207,9 +203,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
api paymentsWebhook {
|
api paymentsWebhook {
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
--- template/app/migrations/20250220095333_remove_last_active_timestamp/migration.sql
|
||||||
|
+++ opensaas-sh/app/migrations/20250220095333_remove_last_active_timestamp/migration.sql
|
||||||
|
@@ -0,0 +1,8 @@
|
||||||
|
+/*
|
||||||
|
+ Warnings:
|
||||||
|
+
|
||||||
|
+ - You are about to drop the column `lastActiveTimestamp` on the `User` table. All the data in the column will be lost.
|
||||||
|
+
|
||||||
|
+*/
|
||||||
|
+-- AlterTable
|
||||||
|
+ALTER TABLE "User" DROP COLUMN "lastActiveTimestamp";
|
@ -1,9 +1,9 @@
|
|||||||
--- template/app/schema.prisma
|
--- template/app/schema.prisma
|
||||||
+++ opensaas-sh/app/schema.prisma
|
+++ opensaas-sh/app/schema.prisma
|
||||||
@@ -14,10 +14,12 @@
|
@@ -13,10 +13,12 @@
|
||||||
|
|
||||||
email String? @unique
|
email String? @unique
|
||||||
username String? @unique
|
username String? @unique
|
||||||
lastActiveTimestamp DateTime @default(now())
|
|
||||||
- isAdmin Boolean @default(false)
|
- isAdmin Boolean @default(false)
|
||||||
+ isAdmin Boolean @default(true)
|
+ isAdmin Boolean @default(true)
|
||||||
+ // isMockUser is an extra property for the demo app ensuring that all users can access
|
+ // isMockUser is an extra property for the demo app ensuring that all users can access
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
|
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
|
||||||
const [statusOptions, setStatusOptions] = useState<SubscriptionStatus[]>([]);
|
const [statusOptions, setStatusOptions] = useState<SubscriptionStatus[]>([]);
|
||||||
const { data, isLoading, error } = useQuery(getPaginatedUsers, {
|
const { data, isLoading, error } = useQuery(getPaginatedUsers, {
|
||||||
@@ -222,7 +223,7 @@
|
@@ -211,7 +212,7 @@
|
||||||
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>
|
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2 flex items-center'>
|
<div className='col-span-2 flex items-center'>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
--- template/app/src/server/scripts/dbSeeds.ts
|
--- template/app/src/server/scripts/dbSeeds.ts
|
||||||
+++ opensaas-sh/app/src/server/scripts/dbSeeds.ts
|
+++ opensaas-sh/app/src/server/scripts/dbSeeds.ts
|
||||||
@@ -38,9 +38,11 @@
|
@@ -37,9 +37,11 @@
|
||||||
sendNewsletter: false,
|
sendNewsletter: false,
|
||||||
credits,
|
credits,
|
||||||
subscriptionStatus,
|
subscriptionStatus,
|
||||||
- lemonSqueezyCustomerPortalUrl: null,
|
- lemonSqueezyCustomerPortalUrl: null,
|
||||||
- paymentProcessorUserId: hasUserPaidOnStripe ? `cus_test_${faker.string.uuid()}` : null,
|
- paymentProcessorUserId: hasUserPaidOnStripe ? `cus_test_${faker.string.uuid()}` : null,
|
||||||
+ stripeId: hasUserPaidOnStripe ? `cus_test_${faker.string.uuid()}` : null,
|
+ stripeId: hasUserPaidOnStripe ? `cus_test_${faker.string.uuid()}` : null,
|
||||||
datePaid: hasUserPaidOnStripe ? faker.date.between({ from: createdAt, to: lastActiveTimestamp }) : null,
|
datePaid: hasUserPaidOnStripe ? faker.date.between({ from: createdAt, to: timePaid }) : null,
|
||||||
subscriptionPlan: subscriptionStatus ? faker.helpers.arrayElement(getSubscriptionPaymentPlanIds()) : null,
|
subscriptionPlan: subscriptionStatus ? faker.helpers.arrayElement(getSubscriptionPaymentPlanIds()) : null,
|
||||||
+ // For the demo app, we want to default isMockUser to true so that our admin dash only shows mock users
|
+ // For the demo app, we want to default isMockUser to true so that our admin dash only shows mock users
|
||||||
+ // and not real users signing up to test the app
|
+ // and not real users signing up to test the app
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
--- template/app/src/user/operations.ts
|
--- template/app/src/user/operations.ts
|
||||||
+++ opensaas-sh/app/src/user/operations.ts
|
+++ opensaas-sh/app/src/user/operations.ts
|
||||||
@@ -52,7 +52,10 @@
|
@@ -38,7 +38,10 @@
|
||||||
subscriptionStatus?: SubscriptionStatus[];
|
subscriptionStatus?: SubscriptionStatus[];
|
||||||
};
|
};
|
||||||
type GetPaginatedUsersOutput = {
|
type GetPaginatedUsersOutput = {
|
||||||
- users: Pick<User, 'id' | 'email' | 'username' | 'lastActiveTimestamp' | 'subscriptionStatus' | 'paymentProcessorUserId'>[];
|
- users: Pick<User, 'id' | 'email' | 'username' | 'subscriptionStatus' | 'paymentProcessorUserId'>[];
|
||||||
+ users: Pick<
|
+ users: Pick<
|
||||||
+ User,
|
+ User,
|
||||||
+ 'id' | 'email' | 'username' | 'lastActiveTimestamp' | 'subscriptionStatus' | 'stripeId'
|
+ 'id' | 'email' | 'username' | 'subscriptionStatus' | 'stripeId'
|
||||||
+ >[];
|
+ >[];
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,8 +68,10 @@
|
@@ -51,8 +54,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const allSubscriptionStatusOptions = args.subscriptionStatus as Array<string | null> | undefined;
|
const allSubscriptionStatusOptions = args.subscriptionStatus as Array<string | null> | undefined;
|
||||||
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
const queryResults = await context.entities.User.findMany({
|
const queryResults = await context.entities.User.findMany({
|
||||||
skip: args.skip,
|
skip: args.skip,
|
||||||
@@ -79,6 +84,7 @@
|
@@ -65,6 +70,7 @@
|
||||||
mode: 'insensitive',
|
mode: 'insensitive',
|
||||||
},
|
},
|
||||||
isAdmin: args.isAdmin,
|
isAdmin: args.isAdmin,
|
||||||
@ -33,16 +33,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
OR: [
|
OR: [
|
||||||
@@ -103,7 +109,7 @@
|
@@ -88,7 +94,7 @@
|
||||||
|
username: true,
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
lastActiveTimestamp: true,
|
|
||||||
subscriptionStatus: true,
|
subscriptionStatus: true,
|
||||||
- paymentProcessorUserId: true,
|
- paymentProcessorUserId: true,
|
||||||
+ stripeId: true,
|
+ stripeId: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: 'desc',
|
id: 'desc',
|
||||||
@@ -119,6 +125,7 @@
|
@@ -104,6 +110,7 @@
|
||||||
mode: 'insensitive',
|
mode: 'insensitive',
|
||||||
},
|
},
|
||||||
isAdmin: args.isAdmin,
|
isAdmin: args.isAdmin,
|
||||||
|
@ -18,7 +18,6 @@ entity User {=psl
|
|||||||
email String? @unique
|
email String? @unique
|
||||||
username String?
|
username String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
lastActiveTimestamp DateTime @default(now())
|
|
||||||
isAdmin Boolean @default(false)
|
isAdmin Boolean @default(false)
|
||||||
paymentProcessorUserId String? @unique
|
paymentProcessorUserId String? @unique
|
||||||
lemonSqueezyCustomerPortalUrl String? // You can delete this if you're not using Lemon Squeezy as your payments processor.
|
lemonSqueezyCustomerPortalUrl String? // You can delete this if you're not using Lemon Squeezy as your payments processor.
|
||||||
@ -116,7 +115,6 @@ entity User {=psl
|
|||||||
email String? @unique
|
email String? @unique
|
||||||
username String?
|
username String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
lastActiveTimestamp DateTime @default(now())
|
|
||||||
isAdmin Boolean @default(false)
|
isAdmin Boolean @default(false)
|
||||||
//...
|
//...
|
||||||
psl=}
|
psl=}
|
||||||
|
@ -79,7 +79,7 @@ Authorization on the server-side is the core of your access control logic, and d
|
|||||||
You can authorize access to server-side operations by adding a check for a logged-in user on the `context.user` object which is passed to all operations in Wasp:
|
You can authorize access to server-side operations by adding a check for a logged-in user on the `context.user` object which is passed to all operations in Wasp:
|
||||||
|
|
||||||
```tsx title="src/server/actions.ts"
|
```tsx title="src/server/actions.ts"
|
||||||
export const updateCurrentUser: UpdateCurrentUser<...> = async (args, context) => {
|
export const someServerAction: SomeServerAction<...> = async (args, context) => {
|
||||||
if (!context.user) {
|
if (!context.user) {
|
||||||
throw new HttpError(401); // throw an error if user is not logged in
|
throw new HttpError(401); // throw an error if user is not logged in
|
||||||
}
|
}
|
||||||
|
@ -140,11 +140,6 @@ query getPaginatedUsers {
|
|||||||
entities: [User]
|
entities: [User]
|
||||||
}
|
}
|
||||||
|
|
||||||
action updateCurrentUserLastActiveTimestamp {
|
|
||||||
fn: import { updateCurrentUserLastActiveTimestamp } from "@src/user/operations",
|
|
||||||
entities: [User]
|
|
||||||
}
|
|
||||||
|
|
||||||
action updateIsUserAdminById {
|
action updateIsUserAdminById {
|
||||||
fn: import { updateIsUserAdminById } from "@src/user/operations",
|
fn: import { updateIsUserAdminById } from "@src/user/operations",
|
||||||
entities: [User]
|
entities: [User]
|
||||||
|
@ -13,7 +13,6 @@ model User {
|
|||||||
|
|
||||||
email String? @unique
|
email String? @unique
|
||||||
username String? @unique
|
username String? @unique
|
||||||
lastActiveTimestamp DateTime @default(now())
|
|
||||||
isAdmin Boolean @default(false)
|
isAdmin Boolean @default(false)
|
||||||
|
|
||||||
paymentProcessorUserId String? @unique
|
paymentProcessorUserId String? @unique
|
||||||
|
@ -172,13 +172,10 @@ const UsersTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-12 border-t-4 border-stroke py-4.5 px-4 dark:border-strokedark md:px-6 '>
|
<div className='grid grid-cols-9 border-t-4 border-stroke py-4.5 px-4 dark:border-strokedark md:px-6 '>
|
||||||
<div className='col-span-3 flex items-center'>
|
<div className='col-span-3 flex items-center'>
|
||||||
<p className='font-medium'>Email / Username</p>
|
<p className='font-medium'>Email / Username</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-3 hidden items-center sm:flex'>
|
|
||||||
<p className='font-medium'>Last Active</p>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-2 flex items-center'>
|
<div className='col-span-2 flex items-center'>
|
||||||
<p className='font-medium'>Subscription Status</p>
|
<p className='font-medium'>Subscription Status</p>
|
||||||
</div>
|
</div>
|
||||||
@ -202,7 +199,7 @@ const UsersTable = () => {
|
|||||||
data.users.map((user) => (
|
data.users.map((user) => (
|
||||||
<div
|
<div
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className='grid grid-cols-12 gap-4 border-t border-stroke py-4.5 px-4 dark:border-strokedark md:px-6 '
|
className='grid grid-cols-9 gap-4 border-t border-stroke py-4.5 px-4 dark:border-strokedark md:px-6 '
|
||||||
>
|
>
|
||||||
<div className='col-span-3 flex items-center'>
|
<div className='col-span-3 flex items-center'>
|
||||||
<div className='flex flex-col gap-1 '>
|
<div className='flex flex-col gap-1 '>
|
||||||
@ -210,14 +207,6 @@ const UsersTable = () => {
|
|||||||
<p className='text-sm text-black dark:text-white'>{user.username}</p>
|
<p className='text-sm text-black dark:text-white'>{user.username}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='col-span-3 hidden items-center sm:flex'>
|
|
||||||
<p className='text-sm text-black dark:text-white'>
|
|
||||||
{user.lastActiveTimestamp.toLocaleDateString() +
|
|
||||||
' ' +
|
|
||||||
user.lastActiveTimestamp.toLocaleTimeString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-2 flex items-center'>
|
<div className='col-span-2 flex items-center'>
|
||||||
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>
|
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,6 @@ import { routes } from 'wasp/client/router';
|
|||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
import { useAuth } from 'wasp/client/auth';
|
import { useAuth } from 'wasp/client/auth';
|
||||||
import { useIsLandingPage } from './hooks/useIsLandingPage';
|
import { useIsLandingPage } from './hooks/useIsLandingPage';
|
||||||
import { updateCurrentUserLastActiveTimestamp } from 'wasp/client/operations';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* use this component to wrap all child components
|
* use this component to wrap all child components
|
||||||
@ -28,16 +27,6 @@ export default function App() {
|
|||||||
return location.pathname.startsWith('/admin');
|
return location.pathname.startsWith('/admin');
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
const lastSeenAt = new Date(user.lastActiveTimestamp);
|
|
||||||
const today = new Date();
|
|
||||||
if (today.getTime() - lastSeenAt.getTime() > 5 * 60 * 1000) {
|
|
||||||
updateCurrentUserLastActiveTimestamp({ lastActiveTimestamp: today });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.hash) {
|
if (location.hash) {
|
||||||
const id = location.hash.replace('#', '');
|
const id = location.hash.replace('#', '');
|
||||||
|
@ -26,21 +26,20 @@ function generateMockUserData(): MockUserData {
|
|||||||
const subscriptionStatus = faker.helpers.arrayElement<SubscriptionStatus | null>(['active', 'cancel_at_period_end', 'past_due', 'deleted', null]);
|
const subscriptionStatus = faker.helpers.arrayElement<SubscriptionStatus | null>(['active', 'cancel_at_period_end', 'past_due', 'deleted', null]);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const createdAt = faker.date.past({ refDate: now });
|
const createdAt = faker.date.past({ refDate: now });
|
||||||
const lastActiveTimestamp = faker.date.between({ from: createdAt, to: now });
|
const timePaid = faker.date.between({ from: createdAt, to: now });
|
||||||
const credits = subscriptionStatus ? 0 : faker.number.int({ min: 0, max: 10 });
|
const credits = subscriptionStatus ? 0 : faker.number.int({ min: 0, max: 10 });
|
||||||
const hasUserPaidOnStripe = !!subscriptionStatus || credits > 3
|
const hasUserPaidOnStripe = !!subscriptionStatus || credits > 3
|
||||||
return {
|
return {
|
||||||
email: faker.internet.email({ firstName, lastName }),
|
email: faker.internet.email({ firstName, lastName }),
|
||||||
username: faker.internet.userName({ firstName, lastName }),
|
username: faker.internet.userName({ firstName, lastName }),
|
||||||
createdAt,
|
createdAt,
|
||||||
lastActiveTimestamp,
|
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
sendNewsletter: false,
|
sendNewsletter: false,
|
||||||
credits,
|
credits,
|
||||||
subscriptionStatus,
|
subscriptionStatus,
|
||||||
lemonSqueezyCustomerPortalUrl: null,
|
lemonSqueezyCustomerPortalUrl: null,
|
||||||
paymentProcessorUserId: hasUserPaidOnStripe ? `cus_test_${faker.string.uuid()}` : null,
|
paymentProcessorUserId: hasUserPaidOnStripe ? `cus_test_${faker.string.uuid()}` : null,
|
||||||
datePaid: hasUserPaidOnStripe ? faker.date.between({ from: createdAt, to: lastActiveTimestamp }) : null,
|
datePaid: hasUserPaidOnStripe ? faker.date.between({ from: createdAt, to: timePaid }) : null,
|
||||||
subscriptionPlan: subscriptionStatus ? faker.helpers.arrayElement(getSubscriptionPaymentPlanIds()) : null,
|
subscriptionPlan: subscriptionStatus ? faker.helpers.arrayElement(getSubscriptionPaymentPlanIds()) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
type UpdateCurrentUserLastActiveTimestamp,
|
|
||||||
type UpdateIsUserAdminById,
|
type UpdateIsUserAdminById,
|
||||||
type GetPaginatedUsers,
|
type GetPaginatedUsers,
|
||||||
} from 'wasp/server/operations';
|
} from 'wasp/server/operations';
|
||||||
@ -31,19 +30,6 @@ export const updateIsUserAdminById: UpdateIsUserAdminById<{ id: string; data: Pi
|
|||||||
return updatedUser;
|
return updatedUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateCurrentUserLastActiveTimestamp: UpdateCurrentUserLastActiveTimestamp<Pick<User, 'lastActiveTimestamp'>, User> = async ({ lastActiveTimestamp }, context) => {
|
|
||||||
if (!context.user) {
|
|
||||||
throw new HttpError(401);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.entities.User.update({
|
|
||||||
where: {
|
|
||||||
id: context.user.id,
|
|
||||||
},
|
|
||||||
data: {lastActiveTimestamp},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
type GetPaginatedUsersInput = {
|
type GetPaginatedUsersInput = {
|
||||||
skip: number;
|
skip: number;
|
||||||
cursor?: number | undefined;
|
cursor?: number | undefined;
|
||||||
@ -52,7 +38,7 @@ type GetPaginatedUsersInput = {
|
|||||||
subscriptionStatus?: SubscriptionStatus[];
|
subscriptionStatus?: SubscriptionStatus[];
|
||||||
};
|
};
|
||||||
type GetPaginatedUsersOutput = {
|
type GetPaginatedUsersOutput = {
|
||||||
users: Pick<User, 'id' | 'email' | 'username' | 'lastActiveTimestamp' | 'subscriptionStatus' | 'paymentProcessorUserId'>[];
|
users: Pick<User, 'id' | 'email' | 'username' | 'subscriptionStatus' | 'paymentProcessorUserId'>[];
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,7 +87,6 @@ export const getPaginatedUsers: GetPaginatedUsers<GetPaginatedUsersInput, GetPag
|
|||||||
email: true,
|
email: true,
|
||||||
username: true,
|
username: true,
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
lastActiveTimestamp: true,
|
|
||||||
subscriptionStatus: true,
|
subscriptionStatus: true,
|
||||||
paymentProcessorUserId: true,
|
paymentProcessorUserId: true,
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user