Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

main->toatu: show quota and usage under user profile #198

Merged
merged 20 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
45341b4
delete PORTAL_WEBSITE_URL
yinglj Apr 1, 2024
e7a9df0
Further Improvement of Payment via Stripe, without WordPress and Flex…
yinglj Apr 1, 2024
254da79
add some spacing between free and pro button
yinglj Apr 2, 2024
7d258d5
Update openAI.js
yinglj Apr 2, 2024
90b9796
Merge branch 'main' into change-the-billing-link-for-main
yinglj Apr 2, 2024
aa7a29b
Merge pull request #190 from aitok-ai/change-the-billing-link-for-main
jinzishuai Apr 2, 2024
509dc0f
Basic scalfolding for webhook implemntation within API for {{BASE_URL…
jinzishuai Apr 3, 2024
0ee6ab5
Implement the webhook that would renew subscription by 1 month
jinzishuai Apr 3, 2024
6064714
refactor stripe webhook to include secrets and keys
jinzishuai Apr 3, 2024
b77d805
Merge pull request #191 from aitok-ai/jinshi/webhook-paid
jinzishuai Apr 3, 2024
9ccdb74
Webhook Working: it has to set up before express.json (#192)
jinzishuai Apr 3, 2024
8cce7c0
Update release-toatu.yml: limit workflow dispatch branch (#194)
jinzishuai May 5, 2024
d027ec1
Update release-toatu.yml: limit dispath from one branch (#193)
jinzishuai May 5, 2024
1f9931a
Change quota from daily to monthly (#195)
jinzishuai May 11, 2024
26ae33c
initial commit: add monthlyQuotaConsumed field to GET /api/user API
jinzishuai May 12, 2024
c65a76f
Got the API to return additional object for quota consumption
jinzishuai May 13, 2024
96d6e1c
restructure output to include both consumption and quota
jinzishuai May 13, 2024
8e345dc
Merge pull request #196 from aitok-ai/jinshi/quota-status
jinzishuai May 15, 2024
2e15859
Client Changes to Show Quota Consumption under User Profile
jinzishuai May 20, 2024
4887aa8
Merge pull request #197 from aitok-ai/jinshi/quote-ui
jinzishuai May 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/release-toatu.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
name: Production Release to https://zhao.toatu.com via CloudFront/S3
on:
workflow_dispatch
workflow_dispatch:
branches:
- release/zhao.toatu.com

jobs:
build:
runs-on: ubuntu-latest
environment: release-zhao.toatu.com
steps:
# Check out the repository
- name: Checkout
Expand Down
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"passport-local": "^1.0.0",
"pino": "^8.12.1",
"sharp": "^0.32.6",
"stripe": "^14.23.0",
"tiktoken": "^1.0.10",
"ua-parser-js": "^1.0.36",
"winston": "^3.10.0",
Expand Down
16 changes: 14 additions & 2 deletions api/server/controllers/UserController.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
const { updateUserPluginsService } = require('../services/UserService');
const { updateUserPluginAuth, deleteUserPluginAuth } = require('../services/PluginService');
const { getUserMessageQuotaUsagePastDays } = require('../middleware/messageQuota');
const User = require('../../models/User');

const getUserController = async (req, res) => {
try {
const { userId } = req.params;
if (userId == undefined || userId === req.user.id) {res.status(200).send(req.user);}
else {
if (userId === undefined || userId === req.user.id) {
// information about the current user
const monthlyQuotaConsumed = await getUserMessageQuotaUsagePastDays(req.user, 30); // This value might be dynamic based on your application logic

// Extend req.user with the new field
const response = {
...req.user.toJSON(),
monthlyQuotaConsumed: monthlyQuotaConsumed,
};
res.status(200).send(response);
} else {
// information about another user, without even authentification.
// TODO: this might be a security issue
const user = await User.findById(userId).exec();
const id = user._id;
const name = user.name;
Expand Down
3 changes: 3 additions & 0 deletions api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ const startServer = async () => {
const app = express();
app.locals.config = config;

// Web Hooks, note that is has to be ahead of the express.json() call since webhooks uses RAW
app.use('/api/webhooks', routes.webhooks);

// Middleware
app.use(errorController);
app.use(express.json({ limit: '3mb' }));
Expand Down
2 changes: 2 additions & 0 deletions api/server/middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const concurrentLimiter = require('./concurrentLimiter');
const validateMessageReq = require('./validateMessageReq');
const buildEndpointOption = require('./buildEndpointOption');
const validateRegistration = require('./validateRegistration');
const messageQuota = require('./messageQuota');

module.exports = {
...abortMiddleware,
Expand All @@ -28,4 +29,5 @@ module.exports = {
validateMessageReq,
buildEndpointOption,
validateRegistration,
messageQuota,
};
34 changes: 34 additions & 0 deletions api/server/middleware/messageQuota.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { getMessagesCount } = require('../../models');

const getUserMessageQuotaUsagePastDays = async (user, days = 30) => {
let currentTime = new Date();
let quota = 0;
if ('proMemberExpiredAt' in user && user.proMemberExpiredAt > currentTime) {
// If not proMember, check quota
quota = JSON.parse(process.env['CHAT_QUOTA_PER_MONTH_PRO_MEMBER']);
} else {
quota = JSON.parse(process.env['CHAT_QUOTA_PER_MONTH']);
}

let someTimeAgo = currentTime;
someTimeAgo.setSeconds(currentTime.getSeconds() - 60 * 60 * 24 * days); // 30 days

let quotaUsage = {};

let promises = Object.keys(quota).map(async (model) => {
let messagesCount = await getMessagesCount({
$and: [{ senderId: user.id }, { model: model }, { updatedAt: { $gte: someTimeAgo } }],
});
quotaUsage[model] = {
consumed: messagesCount,
quota: quota[model],
};
console.log(model, quotaUsage[model]);
});
await Promise.all(promises);
return quotaUsage;
};

module.exports = {
getUserMessageQuotaUsagePastDays,
};
12 changes: 6 additions & 6 deletions api/server/routes/ask/openAI.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req,
let quota = 0;
if ('proMemberExpiredAt' in cur_user && cur_user.proMemberExpiredAt > currentTime) {
// If not proMember, check quota
quota = JSON.parse(process.env['CHAT_QUOTA_PER_DAY_PRO_MEMBER']);
quota = JSON.parse(process.env['CHAT_QUOTA_PER_MONTH_PRO_MEMBER']);
} else {
quota = JSON.parse(process.env['CHAT_QUOTA_PER_DAY']);
quota = JSON.parse(process.env['CHAT_QUOTA_PER_MONTH']);
}

let someTimeAgo = currentTime;
someTimeAgo.setSeconds(currentTime.getSeconds() - 60 * 60 * 24); // 24 hours
someTimeAgo.setSeconds(currentTime.getSeconds() - 60 * 60 * 24 * 30); // 30 days
if (endpointOption.modelOptions.model in quota) {
let messagesCount = await getMessagesCount({
$and: [
Expand All @@ -127,11 +127,11 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req,
{ updatedAt: { $gte: someTimeAgo } },
],
});
let dailyQuota = quota[endpointOption.modelOptions.model].toFixed(0);
if (messagesCount >= dailyQuota) {
let monthlyQuota = quota[endpointOption.modelOptions.model].toFixed(0);
if (messagesCount >= monthlyQuota) {
// TODO: this error message should be implemented by the client based on locale
throw new Error(
`超出了您的使用额度(${endpointOption.modelOptions.model}模型每天${dailyQuota}条消息)。由于需要支付越来越多、每月上万元的API费用,如果您经常使用我们的服务,请打开“我的主页”进行购买,支持我们持续提供GPT服务。`,
`超出了您的使用额度(${endpointOption.modelOptions.model}模型每30天${monthlyQuota}条消息)。由于需要支付越来越多、每月上万元的API费用,如果您经常使用我们的服务,请打开“我的主页”进行购买,支持我们持续提供GPT服务。`,
);
}
}
Expand Down
2 changes: 2 additions & 0 deletions api/server/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const config = require('./config');
const leaderboard = require('./leaderboard');
const assistants = require('./assistants');
const files = require('./files');
const webhooks = require('./webhooks');

module.exports = {
search,
Expand All @@ -40,4 +41,5 @@ module.exports = {
leaderboard,
assistants,
files,
webhooks,
};
8 changes: 8 additions & 0 deletions api/server/routes/webhooks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();

const paid = require('./paid');

router.use('/paid', paid);

module.exports = router;
76 changes: 76 additions & 0 deletions api/server/routes/webhooks/paid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const express = require('express');
const router = express.Router();
const User = require('../../../models/User');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET;

// Modified endpoint using async/await
router.post('/', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];

let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
console.error(`Webhook signature verification failed, Error: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}

if (event.type === 'payment_intent.succeeded') {
console.log('Processing /paid webhook on type=:', event.type);
const paymentIntent = event.data.object;

try {
const updatedUserEmail = await updateUserOnPaymentSuccess(paymentIntent);
if (updatedUserEmail) {
console.log(`User with email ${updatedUserEmail} successfully updated after payment.`);
return res
.status(200)
.send(`User with email ${updatedUserEmail} successfully updated after payment.`);
} else {
console.log('User could not be updated because email was not found.');
return res.status(404).send('User with given email not found.');
}
} catch (error) {
console.error(`Failed to update user after payment: ${error}`);
return res.status(500).send('Internal Server Error');
}
} else {
console.log('Skipping /paid webhook on type:', event.type);
return res.status(200).send(`Unhandled event type ${event.type}`);
}
});

async function updateUserOnPaymentSuccess(paymentIntent) {
if (paymentIntent.receipt_email) {
console.log('updateUserOnPaymentSuccess() for user', paymentIntent.receipt_email);
// Find a user by email
try {
const user = await User.findOne({ email: paymentIntent.receipt_email });
if (user) {
// update proMemberExpiredAt based on the max of current time and proMemberExpiredAt
user.proMemberExpiredAt =
Math.max(user.proMemberExpiredAt, Date.now()) + 31 * 24 * 60 * 60 * 1000;
await user.save(); // Assuming this also returns a Promise, so it should be awaited.
console.log(
'updating user _id',
user._id.toString(),
' with additional 30 days professional subscription:',
user.proMemberExpiredAt.toDateString(),
);
return user.email;
} else {
console.log('User not found for email:', paymentIntent.receipt_email);
return null;
}
} catch (error) {
console.error('An error occurred while updating the user:', error);
}
} else {
console.log('updateUserOnPaymentSuccess(): paymentIntent.receipt_email undefined, skip');
return null;
}
}

module.exports = router;
35 changes: 33 additions & 2 deletions client/src/components/Profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function ProfileContent() {
const [proMemberExpiredAt, setProMemberExpiredAt] = useState<Date>(new Date());
const [editMode, setEditMode] = useState<boolean>(false);
const [bio, setBio] = useState(initialBio || '');
const [quotaUsage, setQuotaUsage] = useState<object>({}); // Monthly quota usage
// const [profession, setProfession] = useState(initialProfession || '');
// new commit
const { userId = '' } = useParams();
Expand Down Expand Up @@ -262,6 +263,12 @@ function ProfileContent() {
} else {
setProMemberExpiredAt(new Date());
}

if (getUserByIdQuery.data.monthlyQuotaConsumed) {
setQuotaUsage(getUserByIdQuery.data.monthlyQuotaConsumed);
} else {
setQuotaUsage({});
}
}
}, [getUserByIdQuery.isSuccess, getUserByIdQuery.data, user]);

Expand Down Expand Up @@ -470,7 +477,7 @@ function ProfileContent() {
{proMemberExpiredAt.getMonth() + 1}-{proMemberExpiredAt.getDate()}
<button
type="submit"
className="rounded bg-green-500 px-4 py-1 text-white hover:bg-green-600"
className="ml-2 rounded bg-blue-500 px-4 py-1 text-white hover:bg-blue-600"
onClick={() =>
window.open(
`${startupConfig?.proMemberPaymentURL}?locale=${lang}&prefilled_email=${profileUser?.email}`,
Expand All @@ -487,7 +494,7 @@ function ProfileContent() {
{localize('com_ui_free_member')}
<button
type="submit"
className="rounded bg-green-500 px-4 py-1 text-white hover:bg-green-600"
className="ml-2 rounded bg-blue-500 px-4 py-1 text-white hover:bg-blue-600"
onClick={() =>
window.open(
`${startupConfig?.proMemberPaymentURL}?locale=${lang}&prefilled_email=${profileUser?.email}`,
Expand All @@ -503,6 +510,30 @@ function ProfileContent() {
<div></div>
)}

{/* Monthly Quota Usage: a table of quota and usage based on object quotaUsage */}
<div className="w-full rounded-lg p-6 dark:text-gray-200">
<div className="pl-7">
<table className="w-full border-collapse border-2 border-gray-500">
<thead>
<tr>
<th className="border-2 border-gray-500 text-left">{localize('com_ui_model')}</th>
<th className="border-2 border-gray-500 text-left">{localize('com_ui_usage')}</th>
<th className="border-2 border-gray-500 text-left">{localize('com_ui_quota')}</th>
</tr>
</thead>
<tbody>
{Object.entries(quotaUsage).map(([key, value]) => (
<tr key={key}>
<td className="border-2 border-gray-500">{key}</td>
<td className="border-2 border-gray-500">{value.consumed}</td>
<td className="border-2 border-gray-500">{value.quota}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>

{/* User bio */}
{userId === user?.id ? (
// Current user's profile view
Expand Down
3 changes: 3 additions & 0 deletions client/src/localization/languages/Eng.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export default {
com_ui_become_pro_member: 'become pro member',
com_ui_renewal_pro_member: 'renewal pro member',
com_ui_free_member: 'Free Member',
com_ui_model: 'Model',
com_ui_quota: '30-Day Quota',
com_ui_usage: 'Consumption (Last 30 Days)',
com_ui_about_yourself: 'About Yourself',
com_ui_profession: 'Profession',
com_ui_share_profile: 'Share Profile',
Expand Down
3 changes: 3 additions & 0 deletions client/src/localization/languages/Zh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default {
com_ui_become_pro_member: '成为专业会员',
com_ui_renewal_pro_member: '续期专业会员',
com_ui_free_member: '免费用户',
com_ui_model: '模型',
com_ui_quota: '30天配额',
com_ui_usage: '过去30天使用情况',
com_ui_about_yourself: '关于自己',
com_ui_profession: '职业',
com_ui_share_profile: '分享主页',
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/data-provider/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export type TUser = {
followers: object;
following: object;
proMemberExpiredAt: Date;
monthlyQuotaConsumed: object;
};

export type TGetConversationsResponse = {
Expand Down
Loading