Skip to content

Commit

Permalink
Merge pull request #61 from WaifuAPI/staging
Browse files Browse the repository at this point in the history
Staging
  • Loading branch information
kyrea authored Aug 5, 2024
2 parents c89a05e + 1d134ca commit a3b88a0
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 190 deletions.
221 changes: 91 additions & 130 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "waifu.it",
"version": "4.6.0",
"version": "4.7.0",
"description": "Random API Serving Anime stuff",
"author": "Aeryk",
"private": true,
Expand Down Expand Up @@ -50,4 +50,4 @@
"weeb",
"anime-girls"
]
}
}
33 changes: 33 additions & 0 deletions src/controllers/v4/internal/stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import createError from 'http-errors';
import Stats from '../../../models/schemas/Stat.js';

// Get Internal Status or statistics
const getStats = async (req, res, next) => {
const key = req.headers.key;
// Check for valid access key in headers
if (!key || key !== process.env.ACCESS_KEY) {
return res.status(401).json({
message: 'Unauthorized',
});
}
try {
const [result] = await Stats.aggregate([
// Select a random document from the results
{ $sample: { size: 1 } },
{ $project: { __v: 0, _id: 0 } },
]);

if (!result) {
return next(createError(404, 'Could not find any Stats'));
}

res.status(200).json(result);

await Stats.findOneAndUpdate({ _id: 'systemstats' }, { $inc: { stats: 1 } });
} catch (error) {
await Stats.findOneAndUpdate({ _id: 'systemstats' }, { $inc: { failed_requests: 1 } });
return next(error);
}
};

export { getStats };
89 changes: 89 additions & 0 deletions src/middlewares/authorize.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ import Stats from '../models/schemas/Stat.js';
*/
const authorize = requiredRole => async (req, res, next) => {
try {
/**
* Determine the endpoint based on the request URL.
*/
const endpoint = getEndpointFromUrl(req.originalUrl);

/**
* Check if the requested endpoint is disabled.
*/
const isEndpointEnabled = await isEndpointEnabledInStats(endpoint);

if (!isEndpointEnabled) {
return next(
createError(
403,
`The endpoint '${endpoint}' is currently disabled. Go to https://discord.gg/yyW389c for support.`,
),
);
}

/**
* Extract API key from request headers.
*
Expand Down Expand Up @@ -49,6 +68,7 @@ const authorize = requiredRole => async (req, res, next) => {
const updateData = {
$inc: {
req_quota: userData && userData.req_quota > 0 ? -1 : 0,
req_consumed: userData && userData.req_quota > 0 ? 1 : 0,
req_count: userData ? 1 : 0,
},
};
Expand Down Expand Up @@ -92,6 +112,11 @@ const authorize = requiredRole => async (req, res, next) => {
return next(createError(403, 'Insufficient privileges to access this endpoint.'));
}

/**
* Log the user request.
*/
await logUserRequest(userData._id, endpoint);

/**
* Increment system stats for successful requests.
*/
Expand All @@ -113,6 +138,46 @@ const authorize = requiredRole => async (req, res, next) => {
}
};

/**
* Helper function to extract endpoint from the request URL.
*
* @param {string} url - The request URL.
* @returns {string} - The extracted endpoint.
*/
const getEndpointFromUrl = url => {
const urlSegments = url.split('/');
return urlSegments[urlSegments.length - 1]; // Last segment is assumed to be the endpoint
};

/**
* Helper function to check if the endpoint is enabled in the Stats collection.
*
* @param {string} endpoint - The endpoint to check.
* @returns {Promise<boolean>} - Promise resolving to true if enabled, false otherwise.
*/
const isEndpointEnabledInStats = async endpoint => {
try {
// Assuming 'Stats' is the correct model for endpoint settings
const settings = await Stats.findOne();

// Handle case where settings are not found
if (!settings) {
return false;
}

// Check if endpoint exists in settings and isEnabled is defined
if (settings[endpoint] && typeof settings[endpoint].isEnabled !== 'undefined') {
return settings[endpoint].isEnabled;
}

// Default to true if isEnabled is not defined or endpoint doesn't exist
return true;
} catch (error) {
console.error('Error fetching endpoint settings:', error);
return true;
}
};

/**
* Increment the specified statistics in the system stats collection.
*
Expand All @@ -125,4 +190,28 @@ const incrementSystemStats = async stats => {
await Stats.findByIdAndUpdate({ _id: 'systemstats' }, { $inc: stats });
};

/**
* Log the number of requests made by a user to a specific endpoint.
*
* @param {string} userId - The ID of the user.
* @param {string} endpoint - The endpoint being accessed.
* @returns {Promise<void>} - Resolves when the log is updated.
*/
const logUserRequest = async (userId, endpoint) => {
try {
// Find the user and update the request count for the specific endpoint
await Users.findByIdAndUpdate(
userId,
{
$inc: {
[`statistics.requests.${endpoint}`]: 1,
},
},
{ new: true, upsert: true }, // Create a new document if it doesn't exist
);
} catch (error) {
console.error('Error logging user request:', error);
}
};

export default authorize;
58 changes: 1 addition & 57 deletions src/middlewares/rateLimit.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,11 @@
import rateLimit from 'express-rate-limit';
import Users from '../models/schemas/User.js';

/**
* @function createRateLimiter
* @description Create and return the rate limiter middleware.
* @returns {Function} Express middleware for rate limiting.
*
* @example
* // Basic usage
* const limiter = createRateLimiter();
* app.use('/api/route', limiter);
*
* @example
* // Customized options
* const customOptions = {
* windowMs: 15 * 60 * 1000, // 15 minutes
* max: 100, // limit each IP to 100 requests per windowMs
* message: 'Too many requests from this IP, please try again after a few minutes.',
* };
* const customLimiter = createRateLimiter(customOptions);
* app.use('/api/customRoute', customLimiter);
*/
const createRateLimiter = () => {
/**
* Default rate limiting options.
* @typedef {Object} RateLimitOptions
* @property {number} [windowMs=60000] - The time window for which the requests are checked/metered (in milliseconds).
* @property {number} [max=20] - The maximum number of allowed requests within the windowMs time frame.
* @property {Object} message - The message sent in the response when the limit is exceeded.
* @property {number} [message.status=429] - The HTTP status code to be set in the response.
* @property {string} [message.message='You've exhausted your ratelimit, please try again later.'] - The message to be sent in the response.
*/
const defaultOptions = {
windowMs: 60 * 1000, // 1 minute
max: 20, // Default rate limit
Expand All @@ -40,38 +15,7 @@ const createRateLimiter = () => {
},
};

// Create rate limiter middleware with default options
const limiter = rateLimit(defaultOptions);

/**
* Express middleware function for rate limiting.
* @param {Object} req - Express request object.
* @param {Object} res - Express response object.
* @param {Function} next - Express next function.
*/
return async (req, res, next) => {
try {
// Extract token from request headers
const token = req.headers.authorization;

// Find user data from the database based on token
const user = await Users.findOne({ token });

// Override default rate limit if user's rate limit is defined
if (user && user.rateLimit) {
limiter.options.max = user.rateLimit;
}

// Apply rate limiting
limiter(req, res, next);
} catch (error) {
// Handle errors when fetching user data
console.error('Error fetching user data:', error.message);

// Apply rate limiting as a fallback
limiter(req, res, next);
}
};
return rateLimit(defaultOptions);
};

export default createRateLimiter;
16 changes: 16 additions & 0 deletions src/models/schemas/Stat.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,28 @@ const { Schema, model } = mongoose;

const StatSchema = new Schema({
_id: { type: String, required: true, default: 'system' },
dashboard: {
isEnabled: { type: Boolean, default: true },
},
registrations: {
isEnabled: { type: Boolean, default: true },
},
login: {
isEnabled: { type: Boolean, default: true },
},
tokenReset: {
isEnabled: { type: Boolean, default: true },
},
quote: {
isEnabled: { type: Boolean, default: true },
},
total_requests: { type: Number, default: 0 },
endpoints_requests: { type: Number, default: 0 },
failed_requests: { type: Number, default: 0 },
success_requests: { type: Number, default: 0 },
banned_requests: { type: Number, default: 0 },
daily_requests: { type: Number, default: 0 },
stats: { type: Number, default: 0 },
run: { type: Number, default: 0 },
sad: { type: Number, default: 0 },
shoot: { type: Number, default: 0 },
Expand Down
57 changes: 56 additions & 1 deletion src/models/schemas/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const UserSchema = new mongoose.Schema({
* @type {number}
* @default 500
*/
req_quota: { type: Number, default: 500 },
req_quota: { type: Number, required: true, default: 500 },

/**
* Number of requests made by the user.
Expand All @@ -85,6 +85,13 @@ const UserSchema = new mongoose.Schema({
*/
req_count: { type: Number, default: 0 },

/**
* Number of requests consumed by the user.
* @type {number}
* @default 0
*/
req_consumed: { type: Number, default: 0 },

/**
* Date and time when the user account was created.
* @type {Date}
Expand All @@ -108,6 +115,54 @@ const UserSchema = new mongoose.Schema({
* @default ['user']
*/
roles: { type: [String], default: ['user'] },

/**
* Subscription or plan type.
* @type {string}
*/
planType: { type: String, default: 'free' },

/**
* Subscription start date.
* @type {Date}
*/
subscriptionStart: { type: Date },

/**
* Subscription end date.
* @type {Date}
*/
subscriptionEnd: { type: Date },

/**
* Subscription status.
* @type {string}
* @enum ['active', 'expired', 'canceled', 'pending', 'suspended', 'trial', 'renewal due', 'grace period']
* @default 'active'
*/
subscriptionStatus: {
type: String,
enum: ['active', 'expired', 'canceled', 'pending', 'suspended', 'trial', 'renewal due', 'grace period'],
default: 'active',
},

/**
* Metadata for subscription.
* @type {object}
*/
subscriptionMetadata: { type: Object },

/**
* Object to store the count of requests made to each endpoint by the user.
* @type {Object}
*/
statistics: {
requests: {
type: Map,
of: Number,
default: {},
},
},
});

/**
Expand Down
16 changes: 16 additions & 0 deletions src/routes/v4/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,22 @@ import yesRoutes from './interactions/yes.js';
*/
router.use('/yes', yesRoutes);

import statsRoutes from './internal/stats.js';

/**
* @api {use} v4/stats Use Stats Routes
* @apiDescription Mount the stats-related routes for handling interactions.
* @apiName UseStatsRoutes
* @apiGroup Routes
*
* @apiSuccess {Object} routes Stats-related routes mounted on the parent router.
*
* @function createStatsRoutes
* @description Creates and returns a set of routes for handling interactions related to Stats.
* @returns {Object} Stats-related routes.
*/
router.use('/stats', statsRoutes);

/**
* Exporting the router for use in other parts of the application.
* @exports {Router} router - Express Router instance with mounted routes.
Expand Down
Loading

0 comments on commit a3b88a0

Please sign in to comment.