mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
feat(uesrprofile): email requirement and validation
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
67
.idea/codeStyles/Project.xml
generated
Normal file
67
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,67 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="OTHER_INDENT_OPTIONS">
|
||||
<value>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</value>
|
||||
</option>
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
<option name="HTML_QUOTE_STYLE" value="Single" />
|
||||
<option name="HTML_ENFORCE_QUOTES" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="db" uuid="099ff2a1-e5b1-4a7b-b016-b41730e917c1">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/config/db/db.sqlite3</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
15
.idea/git_toolbox_prj.xml
generated
Normal file
15
.idea/git_toolbox_prj.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxProjectSettings">
|
||||
<option name="commitMessageIssueKeyValidationOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
<option name="commitMessageValidationEnabledOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/jellyseerr.iml
generated
Normal file
12
.idea/jellyseerr.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/jellyseerr.iml" filepath="$PROJECT_DIR$/.idea/jellyseerr.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -37,6 +37,7 @@
|
||||
"country-flag-icons": "^1.4.21",
|
||||
"csurf": "^1.11.0",
|
||||
"email-templates": "^8.0.10",
|
||||
"email-validator": "^2.0.4",
|
||||
"express": "^4.17.3",
|
||||
"express-openapi-validator": "^4.13.6",
|
||||
"express-rate-limit": "^6.3.0",
|
||||
@@ -84,6 +85,7 @@
|
||||
"@babel/cli": "^7.17.6",
|
||||
"@commitlint/cli": "^16.2.1",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@next/eslint-plugin-next": "^12.1.6",
|
||||
"@semantic-release/changelog": "^6.0.1",
|
||||
"@semantic-release/commit-analyzer": "^9.0.2",
|
||||
"@semantic-release/exec": "^6.0.3",
|
||||
|
||||
@@ -137,6 +137,8 @@ export class User {
|
||||
@UpdateDateColumn()
|
||||
public updatedAt: Date;
|
||||
|
||||
public warnings: string[] = [];
|
||||
|
||||
constructor(init?: Partial<User>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ interface FullPublicSettings extends PublicSettings {
|
||||
enablePushRegistration: boolean;
|
||||
locale: string;
|
||||
emailEnabled: boolean;
|
||||
userEmailRequired: boolean;
|
||||
newPlexLogin: boolean;
|
||||
}
|
||||
|
||||
@@ -159,6 +160,7 @@ export interface NotificationAgentSlack extends NotificationAgentConfig {
|
||||
|
||||
export interface NotificationAgentEmail extends NotificationAgentConfig {
|
||||
options: {
|
||||
userEmailRequired: boolean;
|
||||
emailFrom: string;
|
||||
smtpHost: string;
|
||||
smtpPort: number;
|
||||
@@ -335,6 +337,7 @@ class Settings {
|
||||
email: {
|
||||
enabled: false,
|
||||
options: {
|
||||
userEmailRequired: false,
|
||||
emailFrom: '',
|
||||
smtpHost: '',
|
||||
smtpPort: 587,
|
||||
@@ -529,6 +532,8 @@ class Settings {
|
||||
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
|
||||
locale: this.data.main.locale,
|
||||
emailEnabled: this.data.notifications.agents.email.enabled,
|
||||
userEmailRequired:
|
||||
this.data.notifications.agents.email.options.userEmailRequired,
|
||||
newPlexLogin: this.data.main.newPlexLogin,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Permission } from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
|
||||
const authRoutes = Router();
|
||||
|
||||
@@ -24,6 +25,16 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
|
||||
where: { id: req.user.id },
|
||||
});
|
||||
|
||||
// check if email is required in settings and if user has an valid email
|
||||
const settings = await getSettings();
|
||||
if (
|
||||
settings.notifications.agents.email.options.userEmailRequired &&
|
||||
!EmailValidator.validate(user.email)
|
||||
) {
|
||||
user.warnings.push('userEmailRequired');
|
||||
logger.warn(`User ${user.username} has no valid email address`);
|
||||
}
|
||||
|
||||
return res.status(200).json(user);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import useClickOutside from '../../../hooks/useClickOutside';
|
||||
import { Permission, useUser } from '../../../hooks/useUser';
|
||||
import Transition from '../../Transition';
|
||||
import VersionStatus from '../VersionStatus';
|
||||
import UserWarnings from '../UserWarnings';
|
||||
|
||||
const messages = defineMessages({
|
||||
dashboard: 'Discover',
|
||||
@@ -177,6 +178,10 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="px-2">
|
||||
<UserWarnings onClick={() => setClosed()} />
|
||||
</div>
|
||||
|
||||
{hasPermission(Permission.ADMIN) && (
|
||||
<div className="px-2">
|
||||
<VersionStatus onClick={() => setClosed()} />
|
||||
@@ -236,6 +241,9 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="px-2">
|
||||
<UserWarnings />
|
||||
</div>
|
||||
{hasPermission(Permission.ADMIN) && (
|
||||
<div className="px-2">
|
||||
<VersionStatus />
|
||||
|
||||
66
src/components/Layout/UserWarnings/index.tsx
Normal file
66
src/components/Layout/UserWarnings/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ExclamationIcon } from '@heroicons/react/outline';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useUser } from '../../../hooks/useUser';
|
||||
|
||||
const messages = defineMessages({
|
||||
emailRequired: 'An email address is required.',
|
||||
emailInvalid: 'Email address is invalid.',
|
||||
passwordRequired: 'A password is required.',
|
||||
});
|
||||
|
||||
interface UserWarningsProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const UserWarnings: React.FC<UserWarningsProps> = ({ onClick }) => {
|
||||
const intl = useIntl();
|
||||
const { user } = useUser();
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let res = null;
|
||||
|
||||
//check if a user has warnings
|
||||
if (user.warnings.length > 0) {
|
||||
user.warnings.forEach((warning) => {
|
||||
let link = '';
|
||||
let warningText = '';
|
||||
let warningTitle = '';
|
||||
switch (warning) {
|
||||
case 'userEmailRequired':
|
||||
link = '/profile/settings/';
|
||||
warningTitle = 'Profile is incomplete';
|
||||
warningText = intl.formatMessage(messages.emailRequired);
|
||||
}
|
||||
|
||||
res = (
|
||||
<Link href={link}>
|
||||
<a
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && onClick) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="mx-2 mb-2 flex items-center rounded-lg bg-yellow-500 p-2 text-xs text-white ring-1 ring-gray-700 transition duration-300 hover:bg-yellow-400"
|
||||
>
|
||||
<ExclamationIcon className="h-6 w-6" />
|
||||
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
|
||||
<span className="font-bold">{warningTitle}</span>
|
||||
<span className="truncate">{warningText}</span>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export default UserWarnings;
|
||||
@@ -50,6 +50,7 @@ const Layout: React.FC = ({ children }) => {
|
||||
<div className="absolute top-0 h-64 w-full bg-gradient-to-bl from-gray-800 to-gray-900">
|
||||
<div className="relative inset-0 h-full w-full bg-gradient-to-t from-gray-900 to-transparent" />
|
||||
</div>
|
||||
|
||||
<Sidebar open={isSidebarOpen} setClosed={() => setSidebarOpen(false)} />
|
||||
|
||||
<div className="relative mb-16 flex w-0 min-w-0 flex-1 flex-col lg:ml-64">
|
||||
|
||||
@@ -16,6 +16,7 @@ const messages = defineMessages({
|
||||
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
|
||||
validationSmtpPortRequired: 'You must provide a valid port number',
|
||||
agentenabled: 'Enable Agent',
|
||||
userEmailRequired: 'Require user email',
|
||||
emailsender: 'Sender Address',
|
||||
smtpHost: 'SMTP Host',
|
||||
smtpPort: 'SMTP Port',
|
||||
@@ -125,6 +126,7 @@ const NotificationsEmail: React.FC = () => {
|
||||
<Formik
|
||||
initialValues={{
|
||||
enabled: data.enabled,
|
||||
userEmailRequired: data.options.userEmailRequired,
|
||||
emailFrom: data.options.emailFrom,
|
||||
smtpHost: data.options.smtpHost,
|
||||
smtpPort: data.options.smtpPort ?? 587,
|
||||
@@ -148,6 +150,7 @@ const NotificationsEmail: React.FC = () => {
|
||||
await axios.post('/api/v1/settings/notifications/email', {
|
||||
enabled: values.enabled,
|
||||
options: {
|
||||
userEmailRequired: values.userEmailRequired,
|
||||
emailFrom: values.emailFrom,
|
||||
smtpHost: values.smtpHost,
|
||||
smtpPort: Number(values.smtpPort),
|
||||
@@ -241,6 +244,18 @@ const NotificationsEmail: React.FC = () => {
|
||||
<Field type="checkbox" id="enabled" name="enabled" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="userEmailRequired" className="checkbox-label">
|
||||
{intl.formatMessage(messages.userEmailRequired)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="userEmailRequired"
|
||||
name="userEmailRequired"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="senderName" className="text-label">
|
||||
{intl.formatMessage(messages.senderName)}
|
||||
|
||||
@@ -121,9 +121,9 @@ const UserGeneralSettings: React.FC = () => {
|
||||
</div>
|
||||
<Formik
|
||||
initialValues={{
|
||||
displayName: data?.username,
|
||||
email: data?.email,
|
||||
discordId: data?.discordId,
|
||||
displayName: data?.username ?? '',
|
||||
email: data?.email ?? '',
|
||||
discordId: data?.discordId ?? '',
|
||||
locale: data?.locale,
|
||||
region: data?.region,
|
||||
originalLanguage: data?.originalLanguage,
|
||||
@@ -251,6 +251,9 @@ const UserGeneralSettings: React.FC = () => {
|
||||
<div className="form-row">
|
||||
<label htmlFor="email" className="text-label">
|
||||
{intl.formatMessage(messages.email)}
|
||||
{user?.warnings.find((w) => w === 'userEmailRequired') && (
|
||||
<span className="label-required">*</span>
|
||||
)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
@@ -258,7 +261,12 @@ const UserGeneralSettings: React.FC = () => {
|
||||
id="email"
|
||||
name="email"
|
||||
type="text"
|
||||
placeholder={user?.email}
|
||||
placeholder="example@domain.com"
|
||||
className={
|
||||
user?.warnings.find((w) => w === 'userEmailRequired')
|
||||
? 'border-2 border-red-400 focus:border-blue-600'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{errors.email && touched.email && (
|
||||
|
||||
@@ -13,6 +13,7 @@ export type { PermissionCheckOptions };
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
warnings: string[];
|
||||
plexUsername?: string;
|
||||
username?: string;
|
||||
displayName: string;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||
|
||||
module.exports = {
|
||||
important: true,
|
||||
mode: 'jit',
|
||||
content: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -1622,6 +1622,13 @@
|
||||
dependencies:
|
||||
glob "7.1.7"
|
||||
|
||||
"@next/eslint-plugin-next@^12.1.6":
|
||||
version "12.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.1.6.tgz#dde3f98831f15923b25244588d924c716956292e"
|
||||
integrity sha512-yNUtJ90NEiYFT6TJnNyofKMPYqirKDwpahcbxBgSIuABwYOdkGwzos1ZkYD51Qf0diYwpQZBeVqElTk7Q2WNqw==
|
||||
dependencies:
|
||||
glob "7.1.7"
|
||||
|
||||
"@next/swc-android-arm64@12.1.0":
|
||||
version "12.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.0.tgz#865ba3a9afc204ff2bdeea49dd64d58705007a39"
|
||||
@@ -4780,6 +4787,11 @@ email-templates@^8.0.10:
|
||||
nodemailer "^6.7.2"
|
||||
preview-email "^3.0.5"
|
||||
|
||||
email-validator@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed"
|
||||
integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==
|
||||
|
||||
emoji-regex@^10.0.0:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.1.tgz#77180edb279b99510a21b79b19e1dc283d8f3991"
|
||||
|
||||
Reference in New Issue
Block a user