mirror of
https://github.com/seerr-team/seerr.git
synced 2026-10-11 22:49:57 +00:00
feat: add a demo feature (#3017)
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@ export default defineConfig({
|
||||
env: {
|
||||
ADMIN_EMAIL: '[email protected]',
|
||||
ADMIN_PASSWORD: 'test1234',
|
||||
USER_EMAIL: 'friend@seerr.dev',
|
||||
USER_EMAIL: 'demo@seerr.dev',
|
||||
USER_PASSWORD: 'test1234',
|
||||
},
|
||||
retries: {
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('Discover', () => {
|
||||
requestedBy: {
|
||||
permissions: 4194336,
|
||||
id: 18,
|
||||
email: 'friend@seerr.dev',
|
||||
email: 'demo@seerr.dev',
|
||||
plexUsername: null,
|
||||
username: '',
|
||||
recoveryLinkExpirationDate: null,
|
||||
@@ -159,7 +159,7 @@ describe('Discover', () => {
|
||||
createdAt: '2022-08-17T04:55:28.000Z',
|
||||
updatedAt: '2022-08-17T04:55:28.000Z',
|
||||
requestCount: 1,
|
||||
displayName: 'friend@seerr.dev',
|
||||
displayName: 'demo@seerr.dev',
|
||||
},
|
||||
seasonCount: 0,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { NextConfig } from 'next';
|
||||
const nextConfig: NextConfig = {
|
||||
env: {
|
||||
commitTag: process.env.COMMIT_TAG || 'local',
|
||||
unsafeDoNotUseDemo: process.env.UNSAFE_DO_NOT_USE_DEMO,
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
|
||||
@@ -46,7 +46,7 @@ async function seedRequester(movieQuotaLimit: number): Promise<User> {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const requester = await userRepository.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
requester.movieQuotaLimit = movieQuotaLimit;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import createCustomProxyAgent, {
|
||||
setForceIpv4First,
|
||||
} from '@server/utils/customProxyAgent';
|
||||
import { isPgsql } from '@server/utils/dbType';
|
||||
import { initDemoData } from '@server/utils/demoMode';
|
||||
import { initializeDnsCache } from '@server/utils/dnsCache';
|
||||
import restartFlag from '@server/utils/restartFlag';
|
||||
import '@server/utils/userAgent';
|
||||
@@ -253,6 +254,13 @@ app
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
// Init demo mode and catch some API routes
|
||||
if (process.env.UNSAFE_DO_NOT_USE_DEMO === 'true') {
|
||||
logger.info('Demo mode enabled, seeding database with demo data');
|
||||
await initDemoData(server);
|
||||
}
|
||||
|
||||
server.use('/api/v1', routes);
|
||||
|
||||
// Do not set cookies so CDNs can cache them
|
||||
|
||||
@@ -692,7 +692,7 @@ describe('POST /auth/local', () => {
|
||||
it('allows the non-admin user to log in', async () => {
|
||||
const res = await request(app)
|
||||
.post('/auth/local')
|
||||
.send({ email: 'friend@seerr.dev', password: 'test1234' });
|
||||
.send({ email: 'demo@seerr.dev', password: 'test1234' });
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.ok('id' in res.body);
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('POST /issue', () => {
|
||||
const userRepo = getRepository(User);
|
||||
const media = await seedMedia();
|
||||
const friend = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
|
||||
const agent = await loginAs('[email protected]', 'test1234');
|
||||
@@ -123,8 +123,8 @@ describe('POST /issue', () => {
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 201);
|
||||
assert.strictEqual(res.body.createdBy.email, 'friend@seerr.dev');
|
||||
assert.strictEqual(res.body.comments[0].user.email, 'friend@seerr.dev');
|
||||
assert.strictEqual(res.body.createdBy.email, 'demo@seerr.dev');
|
||||
assert.strictEqual(res.body.comments[0].user.email, 'demo@seerr.dev');
|
||||
|
||||
const persisted = await issueRepo.findOneOrFail({
|
||||
where: { id: res.body.id },
|
||||
@@ -153,13 +153,13 @@ describe('POST /issue', () => {
|
||||
const userRepo = getRepository(User);
|
||||
const media = await seedMedia();
|
||||
const friend = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
|
||||
friend.permissions = Permission.CREATE_ISSUES;
|
||||
await userRepo.save(friend);
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.post('/issue').send({
|
||||
issueType: IssueType.SUBTITLES,
|
||||
message: 'Subtitles are missing.',
|
||||
@@ -168,15 +168,15 @@ describe('POST /issue', () => {
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 201);
|
||||
assert.strictEqual(res.body.createdBy.email, 'friend@seerr.dev');
|
||||
assert.strictEqual(res.body.comments[0].user.email, 'friend@seerr.dev');
|
||||
assert.strictEqual(res.body.createdBy.email, 'demo@seerr.dev');
|
||||
assert.strictEqual(res.body.comments[0].user.email, 'demo@seerr.dev');
|
||||
});
|
||||
|
||||
it('prevents non-managers from supplying another userId', async () => {
|
||||
const userRepo = getRepository(User);
|
||||
const media = await seedMedia();
|
||||
const friend = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
const admin = await userRepo.findOneOrFail({
|
||||
where: { email: '[email protected]' },
|
||||
@@ -185,7 +185,7 @@ describe('POST /issue', () => {
|
||||
friend.permissions = Permission.CREATE_ISSUES;
|
||||
await userRepo.save(friend);
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.post('/issue').send({
|
||||
issueType: IssueType.OTHER,
|
||||
message: 'Something else is wrong.',
|
||||
|
||||
@@ -193,7 +193,7 @@ async function seedRequest(status = MediaRequestStatus.PENDING) {
|
||||
const requestRepo = getRepository(MediaRequest);
|
||||
|
||||
const requestedBy = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
|
||||
const media = await mediaRepo.save(
|
||||
@@ -226,7 +226,7 @@ describe('DELETE /request/:requestId', () => {
|
||||
it('allows the owner to delete their own pending request', async () => {
|
||||
const mediaRequest = await seedRequest();
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.delete(`/request/${mediaRequest.id}`);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
@@ -270,7 +270,7 @@ describe('DELETE /request/:requestId', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.delete(`/request/${mediaRequest.id}`);
|
||||
|
||||
assert.strictEqual(res.status, 401);
|
||||
@@ -279,7 +279,7 @@ describe('DELETE /request/:requestId', () => {
|
||||
it('prevents the owner from deleting an approved request', async () => {
|
||||
const mediaRequest = await seedRequest(MediaRequestStatus.APPROVED);
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.delete(`/request/${mediaRequest.id}`);
|
||||
|
||||
assert.strictEqual(res.status, 401);
|
||||
@@ -349,7 +349,7 @@ describe('PUT /request/:requestId (tv)', () => {
|
||||
where: { email: '[email protected]' },
|
||||
});
|
||||
const otherUser = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
|
||||
const media = await mediaRepo.save(
|
||||
@@ -798,7 +798,7 @@ describe('POST /request (movie), override rules', () => {
|
||||
|
||||
const userRepo = getRepository(User);
|
||||
const friend = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
|
||||
const overrideRuleRepo = getRepository(OverrideRule);
|
||||
@@ -810,7 +810,7 @@ describe('POST /request (movie), override rules', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.post('/request').send({
|
||||
mediaType: MediaType.MOVIE,
|
||||
mediaId: 88001,
|
||||
@@ -826,7 +826,7 @@ describe('POST /request (movie), override rules', () => {
|
||||
|
||||
const userRepo = getRepository(User);
|
||||
const friend = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
|
||||
const overrideRuleRepo = getRepository(OverrideRule);
|
||||
@@ -838,7 +838,7 @@ describe('POST /request (movie), override rules', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.post('/request').send({
|
||||
mediaType: MediaType.MOVIE,
|
||||
mediaId: 88002,
|
||||
@@ -854,7 +854,7 @@ describe('POST /request (movie), override rules', () => {
|
||||
|
||||
const userRepo = getRepository(User);
|
||||
const friend = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
|
||||
const overrideRuleRepo = getRepository(OverrideRule);
|
||||
@@ -866,7 +866,7 @@ describe('POST /request (movie), override rules', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.post('/request').send({
|
||||
mediaType: MediaType.MOVIE,
|
||||
mediaId: 88005,
|
||||
@@ -884,7 +884,7 @@ describe('POST /request (tv), override rules', () => {
|
||||
|
||||
const userRepo = getRepository(User);
|
||||
const friend = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
|
||||
const overrideRuleRepo = getRepository(OverrideRule);
|
||||
@@ -896,7 +896,7 @@ describe('POST /request (tv), override rules', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.post('/request').send({
|
||||
mediaType: MediaType.TV,
|
||||
mediaId: 88003,
|
||||
@@ -913,7 +913,7 @@ describe('POST /request (tv), override rules', () => {
|
||||
|
||||
const userRepo = getRepository(User);
|
||||
const friend = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
|
||||
const overrideRuleRepo = getRepository(OverrideRule);
|
||||
@@ -925,7 +925,7 @@ describe('POST /request (tv), override rules', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.post('/request').send({
|
||||
mediaType: MediaType.TV,
|
||||
mediaId: 88004,
|
||||
@@ -942,7 +942,7 @@ describe('POST /request (tv), override rules', () => {
|
||||
|
||||
const userRepo = getRepository(User);
|
||||
const friend = await userRepo.findOneOrFail({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
});
|
||||
|
||||
const overrideRuleRepo = getRepository(OverrideRule);
|
||||
@@ -954,7 +954,7 @@ describe('POST /request (tv), override rules', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const agent = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const agent = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const res = await agent.post('/request').send({
|
||||
mediaType: MediaType.TV,
|
||||
mediaId: 88006,
|
||||
@@ -1029,7 +1029,7 @@ describe('DELETE /request/:requestId, orphaned season status reset', () => {
|
||||
});
|
||||
assert.strictEqual(updated.seasons[0].status, MediaStatus.UNKNOWN);
|
||||
|
||||
const friend = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const friend = await loginAs('demo@seerr.dev', 'test1234');
|
||||
const reRequest = await friend.post('/request').send({
|
||||
mediaType: MediaType.TV,
|
||||
mediaId: 99101,
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('POST /user/:id/settings/linked-accounts/jellyfin/quickconnect', () =>
|
||||
});
|
||||
|
||||
it('links the account when the media server is Jellyfin', async () => {
|
||||
const { agent, userId } = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const { agent, userId } = await loginAs('demo@seerr.dev', 'test1234');
|
||||
|
||||
const res = await agent
|
||||
.post(`/user/${userId}/settings/linked-accounts/jellyfin/quickconnect`)
|
||||
@@ -116,7 +116,7 @@ describe('POST /user/:id/settings/linked-accounts/jellyfin/quickconnect', () =>
|
||||
});
|
||||
|
||||
it('returns 403 when the media server is Emby', async () => {
|
||||
const { agent, userId } = await loginAs('friend@seerr.dev', 'test1234');
|
||||
const { agent, userId } = await loginAs('demo@seerr.dev', 'test1234');
|
||||
getSettings().main.mediaServerType = MediaServerType.EMBY;
|
||||
|
||||
const res = await agent
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import dataSource from '@server/datasource';
|
||||
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { seedTestUsers } from '@server/utils/seedTestDb';
|
||||
import type { Express } from 'express';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function resetDemoData(): Promise<void> {
|
||||
try {
|
||||
// Drop all tables except users and sessions
|
||||
const entities = dataSource.entityMetadatas;
|
||||
for (const entity of entities) {
|
||||
if (['session', 'user'].includes(entity.tableName.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
await dataSource.getRepository(entity.name).deleteAll();
|
||||
}
|
||||
// Add the default users
|
||||
await seedTestUsers();
|
||||
// Remove admin user from the database
|
||||
await dataSource.getRepository('user').delete({ email: '[email protected]' });
|
||||
// Re-seed discover sliders
|
||||
await DiscoverSlider.bootstrapSliders();
|
||||
} catch (error) {
|
||||
logger.error('Failed to reset demo data', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function initDemoData(server: Express): Promise<void> {
|
||||
// Check that no other data than the demo user exists in the database before seeding demo data
|
||||
const userCount = await dataSource.getRepository('user').count();
|
||||
if (
|
||||
userCount > 1 ||
|
||||
(userCount === 1 &&
|
||||
!(await dataSource
|
||||
.getRepository('user')
|
||||
.findOneBy({ email: '[email protected]' })))
|
||||
) {
|
||||
logger.error('Database is not empty, cannot seed demo data. Aborting.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Retrieve settings from Cypress tests
|
||||
try {
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, '../../cypress/config/settings.cypress.json'),
|
||||
path.join(__dirname, '../../config/settings.json')
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Failed to copy settings from Cypress tests', { error });
|
||||
process.exit(1);
|
||||
}
|
||||
const settings = getSettings();
|
||||
await settings.load();
|
||||
settings.main.mediaServerLogin = false;
|
||||
await settings.save();
|
||||
// Seed the database with demo data
|
||||
await resetDemoData();
|
||||
// Resets the database every hour
|
||||
setInterval(resetDemoData, 60 * 60 * 1000);
|
||||
|
||||
// Disable password sign-in for non-demo users
|
||||
server.post('/api/v1/auth/local', async (req, res, next) => {
|
||||
if (req.body?.email !== '[email protected]') {
|
||||
return res.status(403).json({ error: 'Password sign-in is disabled.' });
|
||||
}
|
||||
next();
|
||||
});
|
||||
// Disable password change
|
||||
server.post('/api/v1/user/:id/settings/password', async (_, res) => {
|
||||
return res.status(204).send();
|
||||
});
|
||||
// Disable test user modification
|
||||
server.post('/api/v1/user/:id/settings/main', async (_, res) => {
|
||||
return res.status(200).json({});
|
||||
});
|
||||
// Disable unlinking media servers
|
||||
server.post(
|
||||
'/api/v1/user/:id/settings/linked-accounts/plex',
|
||||
async (_, res) => {
|
||||
return res.status(204).send();
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ function assertTestDatabase(operation: string, allowOutsideTest = false): void {
|
||||
* Seeds test users into the database.
|
||||
* Assumes the database schema is already set up.
|
||||
*/
|
||||
async function seedTestUsers(): Promise<void> {
|
||||
export async function seedTestUsers(): Promise<void> {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const admin = await userRepository.findOne({
|
||||
@@ -58,17 +58,17 @@ async function seedTestUsers(): Promise<void> {
|
||||
// Create the other user
|
||||
const otherUser =
|
||||
(await userRepository.findOne({
|
||||
where: { email: 'friend@seerr.dev' },
|
||||
where: { email: 'demo@seerr.dev' },
|
||||
})) ?? new User();
|
||||
otherUser.plexId = admin?.plexId ?? 1;
|
||||
otherUser.plexToken = '1234';
|
||||
otherUser.plexUsername = 'friend';
|
||||
otherUser.username = 'friend';
|
||||
otherUser.email = 'friend@seerr.dev';
|
||||
otherUser.plexUsername = 'demo';
|
||||
otherUser.username = 'demo';
|
||||
otherUser.email = 'demo@seerr.dev';
|
||||
otherUser.userType = UserType.PLEX;
|
||||
otherUser.password = TEST_USER_PASSWORD_HASH;
|
||||
otherUser.permissions = 32;
|
||||
otherUser.avatar = gravatarUrl('friend@seerr.dev', {
|
||||
otherUser.avatar = gravatarUrl('demo@seerr.dev', {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Alert from '@app/components/Common/Alert';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
@@ -27,6 +28,7 @@ const messages = defineMessages('components.Login', {
|
||||
signingin: 'Signing In…',
|
||||
signin: 'Sign In',
|
||||
forgotpassword: 'Forgot Password?',
|
||||
demoModeInfo: 'Demo mode is enabled. Use the demo credentials to sign in.',
|
||||
});
|
||||
|
||||
interface LocalLoginProps {
|
||||
@@ -54,8 +56,9 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
password: '',
|
||||
email:
|
||||
process.env.unsafeDoNotUseDemo === 'true' ? '[email protected]' : '',
|
||||
password: process.env.unsafeDoNotUseDemo === 'true' ? 'test1234' : '',
|
||||
}}
|
||||
validationSchema={LoginSchema}
|
||||
validateOnBlur={false}
|
||||
@@ -89,6 +92,12 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
||||
})}
|
||||
</h2>
|
||||
|
||||
{process.env.unsafeDoNotUseDemo === 'true' && (
|
||||
<Alert type="info">
|
||||
{intl.formatMessage(messages.demoModeInfo)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="mb-4 mt-1">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
"components.Login.adminerror": "You must use an admin account to sign in.",
|
||||
"components.Login.back": "Go back",
|
||||
"components.Login.credentialerror": "The username or password is incorrect.",
|
||||
"components.Login.demoModeInfo": "Demo mode is enabled. Use the demo credentials to sign in.",
|
||||
"components.Login.description": "Add a valid email address to complete your profile. This will be used for notifications and local sign-in.",
|
||||
"components.Login.email": "Email Address",
|
||||
"components.Login.emailAlreadyTaken": "This email is already in use.",
|
||||
|
||||
Reference in New Issue
Block a user