diff --git a/build.gradle b/build.gradle
index 8d5421d..c4b06dd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ plugins {
}
group = 'pl.databucket'
-version = '3.3.3'
+version = '3.4.0'
targetCompatibility = 8
sourceCompatibility = 8
@@ -34,6 +34,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.5.7'
implementation 'org.springframework.boot:spring-boot-starter-security:2.5.7'
implementation 'org.springframework.boot:spring-boot-starter-actuator:2.5.7'
+ implementation 'org.springframework.boot:spring-boot-starter-mail:2.5.7'
implementation 'org.springframework:spring-tx:5.3.9'
implementation 'org.postgresql:postgresql:42.2.23.jre7'
implementation 'com.fasterxml.jackson.core:jackson-core:2.13.1'
@@ -50,17 +51,17 @@ dependencies {
compileOnly 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok'
- testImplementation('org.springframework.boot:spring-boot-starter-test:2.5.4') {exclude group: 'junit', module: 'junit'}
+// testImplementation('org.springframework.boot:spring-boot-starter-test:2.5.4') {exclude group: 'junit', module: 'junit'}
// JUnit dependencies
- testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
- testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.7.2'
- testRuntimeOnly 'org.junit.platform:junit-platform-commons:1.7.2'
+// testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
+// testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.7.2'
+// testRuntimeOnly 'org.junit.platform:junit-platform-commons:1.7.2'
// Spock - Mandatory dependencies
- testImplementation 'org.codehaus.groovy:groovy-all:3.0.8'
- testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0'
- testImplementation 'org.spockframework:spock-spring:2.0-groovy-3.0'
+// testImplementation 'org.codehaus.groovy:groovy-all:3.0.8'
+// testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0'
+// testImplementation 'org.spockframework:spock-spring:2.0-groovy-3.0'
}
test {
diff --git a/frontend/package.json b/frontend/package.json
index 82ad140..6932f0f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"homepage": ".",
"name": "databucket",
- "version": "3.3.3",
+ "version": "3.4.0",
"private": true,
"dependencies": {
"@material-ui/core": "4.12.4",
diff --git a/frontend/src/components/dialogs/InfoDialog.jsx b/frontend/src/components/dialogs/InfoDialog.jsx
index 7f3abbf..ebe6229 100644
--- a/frontend/src/components/dialogs/InfoDialog.jsx
+++ b/frontend/src/components/dialogs/InfoDialog.jsx
@@ -62,7 +62,7 @@ export default function InfoDialog() {
>
-
Version: 3.3.3
+
Version: 3.4.0
www.databucket.pl
Source code
Documentation
diff --git a/frontend/src/components/login/ChangePasswordPage.jsx b/frontend/src/components/login/ChangePasswordPage.jsx
index b2184f9..1ae8f1f 100644
--- a/frontend/src/components/login/ChangePasswordPage.jsx
+++ b/frontend/src/components/login/ChangePasswordPage.jsx
@@ -13,6 +13,7 @@ import {fetchHelper, handleErrors} from "../../utils/FetchHelper";
import {Redirect} from "react-router-dom";
import {getProjectDataPath} from "../../route/AppRouter";
import {getBaseUrl} from "../../utils/UrlBuilder";
+import Link from "@material-ui/core/Link";
const initialState = {
password: "",
@@ -21,6 +22,7 @@ const initialState = {
};
export default function ChangePasswordPage() {
+ const [back, setBack] = useState(false);
const [{password, newPassword, newPasswordConfirmation}, setState] = useState(initialState);
const [messageBox, setMessageBox] = useState({open: false, severity: 'error', title: '', message: ''});
const [redirect, setRedirect] = useState(false);
@@ -82,6 +84,9 @@ export default function ChangePasswordPage() {
getPasswordStrength(value);
};
+ if (back)
+ return (
);
+ else
return (
redirect === true ? (
@@ -91,6 +96,16 @@ export default function ChangePasswordPage() {
Change password
+ {/*// additional control to block setting default password by Chrome*/}
+
+
+
Current password
+
+ {
+ setBack(true);
+ }}
+ >
+ Back
+
+
{
+ let resultOk = true;
+ fetch(getConfirmationUrl(inputParams[0]), getGetOptions())
+ .then(handleErrors)
+ .catch(error => {
+ setMessageBox({open: true, severity: 'error', title: 'Error', message: error});
+ resultOk = false;
+ })
+ .then(response => {
+ if (resultOk) {
+ setDone(true);
+ setTimeout(() => {
+ setRedirect(true);
+ }, 6000)
+ }
+ });
+ }, [inputParams]);
+
+ if (redirect)
+ return ( );
+ else
+ return (
+
+ {
}
+
+
+ Confirmation
+
+
+
+ {!done ? "Processing..." : "Check your email inbox."}
+
+
+
setMessageBox({...messageBox, open: false})}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/login/ForgotPasswordPage.css b/frontend/src/components/login/ForgotPasswordPage.css
new file mode 100644
index 0000000..890156a
--- /dev/null
+++ b/frontend/src/components/login/ForgotPasswordPage.css
@@ -0,0 +1,57 @@
+.ContainerClass {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 10vh;
+}
+
+.PaperClass {
+ margin-top: 40px;
+ display: flex;
+ min-width: 370px;
+ max-width: 370px;
+ min-height: 500px;
+ max-height: 700px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 20px 0px 0px 0px;
+}
+
+.Title {
+ padding: 30px;
+}
+
+.Description {
+ padding: 30px;
+}
+
+.TitleGrid {
+ padding: 30px;
+ width: 100%;
+ /*overflow: auto;*/
+}
+
+.Button {
+ width: 100%;
+ margin-top: 10px;
+ padding: 20px;
+}
+
+.EmailInputText {
+ height: 70px;
+ width: 85%;
+ overflow: auto;
+}
+
+.BackLink {
+ width: 100%;
+ margin-top: 50px;
+ margin-left: 40px;
+ margin-bottom: 10px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: flex-start;
+}
\ No newline at end of file
diff --git a/frontend/src/components/login/ForgotPasswordPage.jsx b/frontend/src/components/login/ForgotPasswordPage.jsx
new file mode 100644
index 0000000..32bb223
--- /dev/null
+++ b/frontend/src/components/login/ForgotPasswordPage.jsx
@@ -0,0 +1,104 @@
+import React, {useState} from "react";
+import "./ForgotPasswordPage.css";
+import Button from "@material-ui/core/Button";
+import Logo from "../../images/databucket-logo.png";
+import {Input, InputLabel, Paper} from "@material-ui/core";
+import Typography from "@material-ui/core/Typography";
+import FormControl from "@material-ui/core/FormControl";
+import {MessageBox} from "../utils/MessageBox";
+import Link from "@material-ui/core/Link";
+import {Redirect} from "react-router-dom";
+import {getBaseUrl, getContextPath} from "../../utils/UrlBuilder";
+import {handleLoginErrors} from "../../utils/FetchHelper";
+import {validateEmail} from "../../utils/Misc";
+
+export default function ForgotPasswordPage() {
+
+ const [back, setBack] = useState(false);
+ const [email, setEmail] = useState("");
+ const [messageBox, setMessageBox] = useState({open: false, severity: 'error', title: '', message: ''});
+
+ const onChange = e => {
+ setEmail(e.target.value);
+ };
+
+ const handleReset = () => {
+ fetch(getBaseUrl('public/forgot-password'), {
+ method: 'POST',
+ body: JSON.stringify({email: email, url: window.location.origin + getContextPath() + "/confirmation/forgot-password/"}),
+ headers: {'Content-Type': 'application/json'}
+ })
+ .then(handleLoginErrors)
+ .then(response => {
+ setMessageBox({open: true, severity: 'info', title: 'Send confirmation email', message: null});
+ }).catch(error => {
+ setMessageBox({open: true, severity: 'error', title: 'Sending confirmation password failed', message: error});
+ }
+ );
+ }
+
+ const handleKeypress = e => {
+ if (e.key === 'Enter')
+ handleReset();
+ };
+
+ if (back)
+ return ( );
+ else
+ return (
+
+ {
}
+
+
+ Forgot your password?
+
+
+ Please enter the email address for your account.
+ A verification link will be sent to you.
+ Once you have received the verification link,
+ you will be able to choose a new password for your account.
+
+
+ Email
+ handleKeypress(event)}
+ />
+
+
+ {
+ handleReset();
+ }}
+ >
+ Submit
+
+
+
+ {
+ setBack(true);
+ }}
+ >
+ Back
+
+
+
+
setMessageBox({...messageBox, open: false})}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/login/LoginPage.css b/frontend/src/components/login/LoginPage.css
index 3e304f8..cc6a8da 100644
--- a/frontend/src/components/login/LoginPage.css
+++ b/frontend/src/components/login/LoginPage.css
@@ -7,16 +7,16 @@
}
.PaperClass {
- margin-top: 30px;
+ margin-top: 40px;
display: flex;
min-width: 370px;
max-width: 370px;
- min-height: 400px;
- max-height: 600px;
+ min-height: 500px;
+ max-height: 700px;
flex-direction: column;
align-items: center;
justify-content: flex-start;
- padding: 20px 10px 10px 10px;
+ padding: 20px 0px 0px 0px;
}
.Title {
@@ -29,8 +29,25 @@
/*overflow: auto;*/
}
-.Button {
- padding: 35px;
+.ButtonLogin {
+ width: 100%;
+ margin-top: 50px;
+ padding: 20px;
+}
+
+.ForgotPasswordLink {
+ width: 100%;
+ display: flex;
+ margin-top: -15px;
+ margin-right: 45px;
+ flex-direction: column;
+ align-items: flex-end;
+ justify-content: flex-end;
+ z-index: 1;
+}
+
+.RegistrationLink {
+ padding-top: 70px;
}
.ProjectsList {
@@ -40,6 +57,6 @@
.LoginInputText {
height: 70px;
- width: 70%;
+ width: 85%;
overflow: auto;
}
\ No newline at end of file
diff --git a/frontend/src/components/login/LoginPage.jsx b/frontend/src/components/login/LoginPage.jsx
index f183795..9dfda40 100644
--- a/frontend/src/components/login/LoginPage.jsx
+++ b/frontend/src/components/login/LoginPage.jsx
@@ -20,7 +20,7 @@ import {
setActiveProjectId,
setRoles, setUsername, hasSuperRole, hasMemberRole, hasAdminRole, hasToken, hasProject, logOut, getPathname, setPathname
} from '../../utils/ConfigurationStorage';
-import {Link, Redirect} from 'react-router-dom';
+import {Redirect} from 'react-router-dom';
import FormControl from "@material-ui/core/FormControl";
import IconButton from "@material-ui/core/IconButton";
import {Visibility, VisibilityOff} from "@material-ui/icons";
@@ -28,20 +28,24 @@ import Grid from "@material-ui/core/Grid";
import {MessageBox} from "../utils/MessageBox";
import {sortByKey} from "../../utils/JsonHelper";
import {getManagementProjectsPath, getProjectDataPath} from "../../route/AppRouter";
-import {getBaseUrl, getContextPath} from "../../utils/UrlBuilder";
+import {getBaseUrl} from "../../utils/UrlBuilder";
import ReactGA from 'react-ga';
+import MaterialLink from "@material-ui/core/Link";
+import {Link} from "react-router-dom";
const initialState = {
username: "",
password: "",
projects: null,
+ resetPassword: false,
changePassword: false,
+ register: false,
showPassword: false
};
export default function LoginPage() {
- const [{username, password, projects, changePassword, showPassword}, setState] = useState(initialState);
+ const [{username, password, projects, resetPassword, changePassword, register, showPassword}, setState] = useState(initialState);
const [messageBox, setMessageBox] = useState({open: false, severity: 'error', title: '', message: ''});
const onChange = e => {
@@ -67,7 +71,7 @@ export default function LoginPage() {
};
const signIn = (username, password, projectId) => {
- fetch(getBaseUrl('public/signin'), {
+ fetch(getBaseUrl('public/sign-in'), {
method: 'POST',
body: JSON.stringify(projectId == null ? {username, password} : {username, password, projectId}),
headers: {'Content-Type': 'application/json'}
@@ -133,6 +137,7 @@ export default function LoginPage() {
-
+
+ {
+ setState(prevState => ({...prevState, resetPassword: true}));
+ }}
+ >
+ Forgot your password?
+
+
+
- Submit
+ Login
+
+ {
+ setState(prevState => ({...prevState, register: true}));
+ }}
+ >
+ Don't have an account?
+
+
);
}
@@ -215,7 +244,11 @@ export default function LoginPage() {
}
const getSwitchParam = () => {
- if (changePassword === true) {
+ if (register === true) {
+ return 6;
+ } else if (resetPassword === true) {
+ return 5;
+ } else if (changePassword === true) {
return 4;
} else if (projects != null && projects.length > 0) {
return 3;
@@ -230,6 +263,10 @@ export default function LoginPage() {
const paper = () => {
switch (getSwitchParam()) {
+ case 6:
+ return redirectTo("/sign-up");
+ case 5:
+ return redirectTo("/forgot-password");
case 4:
return redirectTo("/change-password");
case 3:
@@ -254,7 +291,7 @@ export default function LoginPage() {
{
}
{paper()}
-
3.3.3
+
3.4.0
setMessageBox({...messageBox, open: false})}
diff --git a/frontend/src/components/login/SignUpPage.css b/frontend/src/components/login/SignUpPage.css
new file mode 100644
index 0000000..42d5d6c
--- /dev/null
+++ b/frontend/src/components/login/SignUpPage.css
@@ -0,0 +1,69 @@
+.ContainerClass {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 10vh;
+}
+
+.PaperClass {
+ margin-top: 40px;
+ display: flex;
+ min-width: 370px;
+ max-width: 370px;
+ min-height: 500px;
+ max-height: 700px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 20px 0px 0px 0px;
+}
+
+.Title {
+ padding: 30px;
+}
+
+.Description {
+ padding: 30px;
+}
+
+.TitleGrid {
+ padding: 30px;
+ width: 100%;
+ /*overflow: auto;*/
+}
+
+.Button {
+ width: 100%;
+ margin-top: 10px;
+ padding: 20px;
+}
+
+.EmailInputText {
+ height: 70px;
+ width: 85%;
+ overflow: auto;
+}
+
+.UsernameInputText {
+ height: 70px;
+ width: 85%;
+ overflow: auto;
+}
+
+.PasswordText {
+ height: 70px;
+ width: 85%;
+ overflow: auto;
+}
+
+.BackLink {
+ width: 100%;
+ margin-top: 50px;
+ margin-left: 40px;
+ margin-bottom: 10px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: flex-start;
+}
\ No newline at end of file
diff --git a/frontend/src/components/login/SignUpPage.jsx b/frontend/src/components/login/SignUpPage.jsx
new file mode 100644
index 0000000..baaf72d
--- /dev/null
+++ b/frontend/src/components/login/SignUpPage.jsx
@@ -0,0 +1,200 @@
+import React, {useState} from "react";
+import "./SignUpPage.css";
+import Button from "@material-ui/core/Button";
+import Logo from "../../images/databucket-logo.png";
+import {Input, InputLabel, Paper} from "@material-ui/core";
+import Typography from "@material-ui/core/Typography";
+import FormControl from "@material-ui/core/FormControl";
+import {MessageBox} from "../utils/MessageBox";
+import Link from "@material-ui/core/Link";
+import {Redirect} from "react-router-dom";
+import {validateEmail} from "../../utils/Misc";
+import {getBaseUrl, getContextPath} from "../../utils/UrlBuilder";
+import {handleLoginErrors} from "../../utils/FetchHelper";
+
+export default function SignUpPage() {
+
+ const [back, setBack] = useState(false);
+ const [username, setUsername] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [passwordConfirmation, setPasswordConfirmation] = useState("");
+ const [messageBox, setMessageBox] = useState({open: false, severity: 'error', title: '', message: ''});
+
+ const handleSetUsername = e => {
+ setUsername(e.target.value);
+ checkUsername(e.target.value);
+ };
+
+ const handleSetEmail = e => {
+ setEmail(e.target.value);
+ checkEmail(e.target.value);
+ };
+
+ const handlePassword = e => {
+ setPassword(e.target.value);
+ getPasswordStrength(e.target.value);
+ };
+
+ const handlePasswordConfirmation = e => {
+ setPasswordConfirmation(e.target.value);
+ checkPasswordConfirmation(e.target.value);
+ };
+
+ const handleSubmit = () => {
+ fetch(getBaseUrl('public/sign-up'), {
+ method: 'POST',
+ body: JSON.stringify({username: username, email: email, password: password, url: window.location.origin + getContextPath() + "/confirmation/sign-up/"}),
+ headers: {'Content-Type': 'application/json'}
+ })
+ .then(handleLoginErrors)
+ .then(response => {
+ setMessageBox({open: true, severity: 'info', title: 'Send confirmation email', message: null});
+ }).catch(error => {
+ setMessageBox({open: true, severity: 'error', title: 'Registration failed', message: error});
+ }
+ );
+ }
+
+ const getPasswordStrength = (pwd) => {
+ const strongRegex = new RegExp("^(?=.{14,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*\\W).*$", "g");
+ const mediumRegex = new RegExp("^(?=.{10,})(((?=.*[A-Z])(?=.*[a-z]))|((?=.*[A-Z])(?=.*[0-9]))|((?=.*[a-z])(?=.*[0-9]))).*$", "g");
+ const enoughRegex = new RegExp("(?=.{8,}).*", "g");
+
+ if (strongRegex.test(pwd)) {
+ setMessageBox({open: true, severity: 'success', title: 'Strong password.', message: ''});
+ } else if (mediumRegex.test(pwd)) {
+ setMessageBox({open: true, severity: 'info', title: 'Medium password.', message: ''});
+ } else if (enoughRegex.test(pwd)) {
+ setMessageBox({open: true, severity: 'warning', title: 'Weak password!', message: ''});
+ } else {
+ setMessageBox({open: true, severity: 'error', title: 'Very weak password!!!', message: ''});
+ }
+ }
+
+ const checkPasswordConfirmation = (passwordConfirmation) => {
+ if (passwordConfirmation !== password)
+ setMessageBox({open: true, severity: 'error', title: 'Your password and confirmation password do not match.', message: ''});
+ else
+ setMessageBox({open: true, severity: 'success', title: 'Your password and confirmation password match.', message: ''});
+ }
+
+ const checkUsername = (username) => {
+ if (!(username.length >=3 && username.length < 30))
+ setMessageBox({open: true, severity: 'info', title: 'Please enter a username between 3 and 30 characters.', message: ''});
+ }
+
+ const checkEmail = (email) => {
+ if (!validateEmail(email))
+ setMessageBox({open: true, severity: 'error', title: 'Invalid email address.', message: ''});
+ }
+
+ const handleKeypress = e => {
+ if (e.key === 'Enter')
+ handleSubmit();
+ };
+
+ if (back)
+ return ( );
+ else
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/management/ProjectsTab.jsx b/frontend/src/components/management/ProjectsTab.jsx
index f4e00c1..50135f5 100644
--- a/frontend/src/components/management/ProjectsTab.jsx
+++ b/frontend/src/components/management/ProjectsTab.jsx
@@ -54,7 +54,7 @@ export default function ProjectsTab() {
const {templates, fetchTemplates, notifyTemplates} = templatesContext;
const rolesContext = useContext(RolesContext);
const {roles, fetchRoles} = rolesContext;
- const changeableFields = ['id', 'enabled', 'name', 'description', 'usersIds', 'templatesIds', 'expirationDate'];
+ const changeableFields = ['id', 'publicVisible', 'enabled', 'name', 'description', 'usersIds', 'templatesIds', 'expirationDate'];
const projectSpecification = {
name: {title: 'Name', check: ['notEmpty', 'min1', 'max30']},
description: {title: 'Description', check: ['max250']}
@@ -116,6 +116,13 @@ export default function ProjectsTab() {
tableRef={tableRef}
columns={[
getColumnId(),
+ {
+ title: 'Public',
+ field: 'publicVisible',
+ type: 'boolean',
+ width: '1%',
+ cellStyle: {width: '1%'}
+ },
getColumnEnabled(),
getColumnName(),
getColumnDescription('20%'),
diff --git a/frontend/src/components/management/UsersTab.jsx b/frontend/src/components/management/UsersTab.jsx
index 764dbbd..2f7b9cd 100644
--- a/frontend/src/components/management/UsersTab.jsx
+++ b/frontend/src/components/management/UsersTab.jsx
@@ -50,9 +50,10 @@ export default function UsersTab() {
const {roles, fetchRoles} = rolesContext;
const projectsContext = useContext(ProjectsContext);
const {projects, fetchProjects, notifyProjects} = projectsContext;
- const changeableFields = ['id', 'username', 'description', 'enabled', 'expirationDate', 'rolesIds', 'projectsIds'];
+ const changeableFields = ['id', 'username', 'email', 'description', 'enabled', 'expirationDate', 'rolesIds', 'projectsIds'];
const userSpecification = {
username: {title: 'Username', check: ['notEmpty', 'min1', 'max30']},
+ email: {title: 'Email', check: ['email']},
description: {title: 'Description', check: ['max200']}
};
@@ -86,6 +87,7 @@ export default function UsersTab() {
{title: 'State', filtering: false, cellStyle: { width: '1%'}, editable: 'never', searchable: false, sorting: false, render: (rowData) => getUserIcon(rowData)},
getColumnEnabled(),
{title: 'Name', field: 'username', editable: 'onAdd', filtering: true},
+ {title: 'Email', field: 'email', filtering: true},
getColumnDescription('20%'),
{
title: 'Roles', field: 'rolesIds', filtering: false, sorting: false,
diff --git a/frontend/src/route/AppRouter.js b/frontend/src/route/AppRouter.js
index c978c71..0cc44d5 100644
--- a/frontend/src/route/AppRouter.js
+++ b/frontend/src/route/AppRouter.js
@@ -11,6 +11,9 @@ import {getActiveProjectId, hasProject, setPathname} from "../utils/Configuratio
import _ProjectRouteInternal from "../components/data/_ProjectRouteInternal";
import ChangePasswordRoute from "./ChangePasswordRoute";
import {getContextPath} from "../utils/UrlBuilder";
+import ForgotPasswordPage from "../components/login/ForgotPasswordPage";
+import SignUpPage from "../components/login/SignUpPage";
+import ConfirmationPage from "../components/login/ConfirmationPage";
export default function AppRouter() {
@@ -24,6 +27,9 @@ export default function AppRouter() {
+
+
+
diff --git a/frontend/src/utils/JsonHelper.js b/frontend/src/utils/JsonHelper.js
index bb8d18b..5aa5dd8 100644
--- a/frontend/src/utils/JsonHelper.js
+++ b/frontend/src/utils/JsonHelper.js
@@ -22,6 +22,14 @@ export const getSelectedValues = (data, keys) => {
return result;
}
+const validateEmail = (email) => {
+ return String(email)
+ .toLowerCase()
+ .match(
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+ );
+};
+
export const validateItem = (data, specification) => {
// exampleSpecification = {
// name: {title: 'Name', check: ['notEmpty', 'min3', 'max5']},
@@ -88,6 +96,11 @@ export const validateItem = (data, specification) => {
if (data[key] === 'select' && data['enumId'] == null)
message += `>>> Enum must not be empty for 'Enum' type! `;
}
+
+ if (validation === 'email') {
+ if (!validateEmail(data[key]))
+ message += `>>> Invalid email address! `;
+ }
}
}
}
diff --git a/frontend/src/utils/Misc.js b/frontend/src/utils/Misc.js
new file mode 100644
index 0000000..95711bb
--- /dev/null
+++ b/frontend/src/utils/Misc.js
@@ -0,0 +1,4 @@
+export const validateEmail = (email) => {
+ const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return re.test(email);
+}
\ No newline at end of file
diff --git a/frontend/src/utils/UrlBuilder.js b/frontend/src/utils/UrlBuilder.js
index 2d658c3..6d4a7b4 100644
--- a/frontend/src/utils/UrlBuilder.js
+++ b/frontend/src/utils/UrlBuilder.js
@@ -1,7 +1,6 @@
export const getOrigin = () => {
- const origin = window.location.origin;
- // const origin = 'http://localhost:8080';
-
+ // const origin = window.location.origin;
+ const origin = 'http://localhost:8080';
return origin + getContextPath();
}
@@ -14,6 +13,10 @@ export const getContextPath = () => {
return "";
}
+export const getConfirmationUrl = (secureLink) => {
+ return `${getOrigin()}/api/public/confirmation/${secureLink}`;
+}
+
export const getDataUrl = (bucket) => {
return `${getOrigin()}/api/bucket/${bucket.name}`;
}
diff --git a/src/main/java/pl/databucket/server/configuration/SwaggerConfiguration.java b/src/main/java/pl/databucket/server/configuration/SwaggerConfiguration.java
index 8a70017..9077795 100644
--- a/src/main/java/pl/databucket/server/configuration/SwaggerConfiguration.java
+++ b/src/main/java/pl/databucket/server/configuration/SwaggerConfiguration.java
@@ -49,7 +49,7 @@ public Docket confDataContext() {
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Databucket API")
- .version("3.3.3")
+ .version("3.4.0")
.build();
}
}
\ No newline at end of file
diff --git a/src/main/java/pl/databucket/server/controller/ManageUserController.java b/src/main/java/pl/databucket/server/controller/ManageUserController.java
index 440dc5a..4e498f9 100644
--- a/src/main/java/pl/databucket/server/controller/ManageUserController.java
+++ b/src/main/java/pl/databucket/server/controller/ManageUserController.java
@@ -75,7 +75,7 @@ public ResponseEntity> modifyUser(@Valid @RequestBody ManageUserDtoRequest use
@PostMapping(value = "/password/reset")
public ResponseEntity> resetPassword(@Valid @RequestBody AuthReqDTO authDtoRequest) {
try {
- manageUserService.resetPassword(authDtoRequest);
+ manageUserService.resetAndSendPassword(authDtoRequest);
return new ResponseEntity<>(null, HttpStatus.OK);
} catch (IllegalArgumentException e1) {
return exceptionFormatter.customException(e1, HttpStatus.NOT_ACCEPTABLE);
diff --git a/src/main/java/pl/databucket/server/controller/PublicController.java b/src/main/java/pl/databucket/server/controller/PublicController.java
index 7a6eb5c..5dc384c 100644
--- a/src/main/java/pl/databucket/server/controller/PublicController.java
+++ b/src/main/java/pl/databucket/server/controller/PublicController.java
@@ -6,30 +6,29 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
+import org.springframework.mail.MailSendException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.*;
-import pl.databucket.server.dto.AuthReqDTO;
-import pl.databucket.server.dto.AuthRespDTO;
-import pl.databucket.server.dto.AuthProjectDTO;
-import pl.databucket.server.dto.DataGetDTO;
+import pl.databucket.server.dto.*;
import pl.databucket.server.entity.Project;
import pl.databucket.server.entity.User;
import pl.databucket.server.exception.ExceptionFormatter;
+import pl.databucket.server.exception.ForbiddenRepetitionException;
import pl.databucket.server.repository.UserRepository;
-import pl.databucket.server.response.MessageResponse;
import pl.databucket.server.security.TokenProvider;
-import pl.databucket.server.service.data.DataService;
-import pl.databucket.server.service.data.QueryRule;
+import pl.databucket.server.service.ManageUserService;
+import javax.mail.AuthenticationFailedException;
+import javax.mail.MessagingException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
-@Api(tags="PUBLIC")
+@Api(tags = "PUBLIC")
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api/public")
@@ -42,7 +41,7 @@ public class PublicController {
private UserRepository userRepository;
@Autowired
- private DataService dataService;
+ private ManageUserService manageUserService;
@Autowired
private ModelMapper modelMapper;
@@ -60,9 +59,9 @@ public class PublicController {
@ApiResponse(code = 403, message = "Forbidden - user access has expired | the project is disabled | the project is expired | the user is not assign to given project | the user is not assign to any project"),
@ApiResponse(code = 500, message = "Internal server error"),
})
- @PostMapping(value = "/signin", produces = MediaType.APPLICATION_JSON_VALUE)
+ @PostMapping(value = {"/sign-in", "/signin"}, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity> signIn(
- @ApiParam(value="payload - username (required), password (required), projectId (optional)", required = true) @RequestBody AuthReqDTO authReqDTO) {
+ @ApiParam(value = "payload - username (required), password (required), projectId (optional)", required = true) @RequestBody AuthReqDTO authReqDTO) {
User user = userRepository.findByUsername(authReqDTO.getUsername());
if (user == null)
return exceptionFormatter.customException(new UsernameNotFoundException("Bad credentials"), HttpStatus.UNAUTHORIZED);
@@ -177,23 +176,66 @@ public ResponseEntity> signIn(
}
}
- @ApiOperation(value = "getQuery", hidden = true)
- @PostMapping(value = "/query")
- public ResponseEntity> getQuery(@RequestBody(required = false) DataGetDTO dataGetDto) {
+ @PostMapping(value = "/forgot-password", produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity> forgotPassword(@RequestBody ForgotPasswordReqDTO forgotPasswordReqDTO) {
try {
- return new ResponseEntity<>(new MessageResponse(dataService.getQuery(new QueryRule("fakeUserName", dataGetDto))), HttpStatus.OK);
+ manageUserService.forgotPasswordMessage(forgotPasswordReqDTO);
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ } catch (MessagingException | MailSendException e) {
+ return exceptionFormatter.customPublicException("Mail service exception!", HttpStatus.SERVICE_UNAVAILABLE);
+ } catch (UsernameNotFoundException e) {
+ return exceptionFormatter.customPublicException(e.getMessage(), HttpStatus.UNAUTHORIZED);
+ } catch (ForbiddenRepetitionException e) {
+ return exceptionFormatter.customPublicException(e.getMessage(), HttpStatus.FORBIDDEN);
} catch (Exception e) {
return exceptionFormatter.defaultException(e);
}
}
- @ApiOperation(value = "getQueryRules", hidden = true)
- @PostMapping(value = "/query-rules")
- public ResponseEntity> getQueryRules(@RequestBody(required = false) DataGetDTO dataGetDto) {
+ @GetMapping(value = {"/confirmation/forgot-password/{jwts}"}, produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity> forgotPasswordConfirmation(@PathVariable String jwts) {
try {
- return new ResponseEntity<>(new QueryRule("fakeUserName", dataGetDto), HttpStatus.OK);
+ manageUserService.resetAndSendPassword(jwts);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (MessagingException | MailSendException e) {
+ return exceptionFormatter.customException("Mail service exception!", HttpStatus.SERVICE_UNAVAILABLE);
+ } catch (ForbiddenRepetitionException e) {
+ return exceptionFormatter.customPublicException(e.getMessage(), HttpStatus.FORBIDDEN);
} catch (Exception e) {
return exceptionFormatter.defaultException(e);
}
}
+
+ @PostMapping(value = {"/sign-up"}, produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity> signUp(@RequestBody SignUpDtoRequest signUpDtoRequest) {
+ try {
+ if (userRepository.existsByUsername(signUpDtoRequest.getUsername()))
+ return exceptionFormatter.customPublicException("Given username already exists", HttpStatus.CONFLICT);
+
+ if (userRepository.existsByEmail(signUpDtoRequest.getEmail()))
+ return exceptionFormatter.customPublicException("Given email already exists", HttpStatus.CONFLICT);
+
+ manageUserService.signUpUser(signUpDtoRequest);
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ } catch (MessagingException | MailSendException e) {
+ return exceptionFormatter.customException("Mail service exception!", HttpStatus.SERVICE_UNAVAILABLE);
+ } catch (Exception e) {
+ return exceptionFormatter.defaultException(e);
+ }
+ }
+
+ @GetMapping(value = {"/confirmation/sign-up/{jwts}"}, produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity> signUpConfirmation(@PathVariable String jwts) {
+ try {
+ manageUserService.signUpUserConfirmation(jwts);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (MessagingException | MailSendException e) {
+ return exceptionFormatter.customException("Mail service exception!", HttpStatus.SERVICE_UNAVAILABLE);
+ } catch (ForbiddenRepetitionException e) {
+ return exceptionFormatter.customPublicException(e.getMessage(), HttpStatus.FORBIDDEN);
+ } catch (Exception e) {
+ return exceptionFormatter.defaultException(e);
+ }
+ }
+
}
diff --git a/src/main/java/pl/databucket/server/dto/ForgotPasswordReqDTO.java b/src/main/java/pl/databucket/server/dto/ForgotPasswordReqDTO.java
new file mode 100644
index 0000000..5de9c86
--- /dev/null
+++ b/src/main/java/pl/databucket/server/dto/ForgotPasswordReqDTO.java
@@ -0,0 +1,13 @@
+package pl.databucket.server.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class ForgotPasswordReqDTO {
+ private String email;
+ private String url;
+}
diff --git a/src/main/java/pl/databucket/server/dto/ManageProjectDto.java b/src/main/java/pl/databucket/server/dto/ManageProjectDto.java
index a6d67ac..49044e5 100644
--- a/src/main/java/pl/databucket/server/dto/ManageProjectDto.java
+++ b/src/main/java/pl/databucket/server/dto/ManageProjectDto.java
@@ -18,6 +18,7 @@ public class ManageProjectDto {
@Size(max = Constants.DESCRIPTION_MAX)
private String description;
private boolean enabled;
+ private boolean publicVisible;
private Date expirationDate;
private Set usersIds;
private Set templatesIds;
diff --git a/src/main/java/pl/databucket/server/dto/ManageUserDtoRequest.java b/src/main/java/pl/databucket/server/dto/ManageUserDtoRequest.java
index eaeb6df..d7b3b2b 100644
--- a/src/main/java/pl/databucket/server/dto/ManageUserDtoRequest.java
+++ b/src/main/java/pl/databucket/server/dto/ManageUserDtoRequest.java
@@ -17,6 +17,7 @@ public class ManageUserDtoRequest {
@Size(min = Constants.NAME_MIN, max = Constants.NAME_MAX)
private String username;
private String description;
+ private String email;
private String password;
private boolean enabled = false;
private Date expirationDate;
diff --git a/src/main/java/pl/databucket/server/dto/ManageUserDtoResponse.java b/src/main/java/pl/databucket/server/dto/ManageUserDtoResponse.java
index 663e6ff..9310b31 100644
--- a/src/main/java/pl/databucket/server/dto/ManageUserDtoResponse.java
+++ b/src/main/java/pl/databucket/server/dto/ManageUserDtoResponse.java
@@ -13,6 +13,7 @@ public class ManageUserDtoResponse {
private long id;
private String username;
private String description;
+ private String email;
private Boolean enabled;
private Date expirationDate;
private Set rolesIds;
diff --git a/src/main/java/pl/databucket/server/dto/SignUpDtoRequest.java b/src/main/java/pl/databucket/server/dto/SignUpDtoRequest.java
new file mode 100644
index 0000000..4a48ace
--- /dev/null
+++ b/src/main/java/pl/databucket/server/dto/SignUpDtoRequest.java
@@ -0,0 +1,21 @@
+package pl.databucket.server.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+import pl.databucket.server.configuration.Constants;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+
+@Getter
+@Setter
+public class SignUpDtoRequest {
+
+ @NotEmpty
+ @Size(min = Constants.NAME_MIN, max = Constants.NAME_MAX)
+ private String username;
+ private String email;
+ private String password;
+ private String url;
+
+}
diff --git a/src/main/java/pl/databucket/server/entity/Project.java b/src/main/java/pl/databucket/server/entity/Project.java
index f44b5ab..6a8703b 100644
--- a/src/main/java/pl/databucket/server/entity/Project.java
+++ b/src/main/java/pl/databucket/server/entity/Project.java
@@ -33,6 +33,9 @@ public class Project extends Auditable {
@Temporal(TemporalType.TIMESTAMP)
private Date expirationDate;
+ @Column
+ private Boolean publicVisible = false;
+
@Column
private Boolean enabled = true;
diff --git a/src/main/java/pl/databucket/server/entity/User.java b/src/main/java/pl/databucket/server/entity/User.java
index 5e2a754..e607c8a 100644
--- a/src/main/java/pl/databucket/server/entity/User.java
+++ b/src/main/java/pl/databucket/server/entity/User.java
@@ -5,6 +5,7 @@
import pl.databucket.server.configuration.Constants;
import javax.persistence.*;
+import javax.validation.constraints.Email;
import java.io.Serializable;
import java.util.Date;
import java.util.Set;
@@ -30,6 +31,10 @@ public class User extends Auditable implements Serializable {
@Column(length = Constants.DESCRIPTION_MAX)
private String description;
+ @Email
+ @Column
+ private String email;
+
@Column
private String password;
@@ -43,6 +48,13 @@ public class User extends Auditable implements Serializable {
@Temporal(TemporalType.TIMESTAMP)
private Date expirationDate;
+ @Column(name = "last_send_email_forgot_password_link_date")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date lastSendEmailForgotPasswordLinkDate;
+
+ @Column(name = "last_send_email_temp_password_date")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date lastSendEmailTempPasswordDate;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "users_roles",
diff --git a/src/main/java/pl/databucket/server/exception/ExceptionFormatter.java b/src/main/java/pl/databucket/server/exception/ExceptionFormatter.java
index fcda0f1..1c21743 100644
--- a/src/main/java/pl/databucket/server/exception/ExceptionFormatter.java
+++ b/src/main/java/pl/databucket/server/exception/ExceptionFormatter.java
@@ -47,4 +47,10 @@ public ResponseEntity> customException(String message, HttpS
return new ResponseEntity<>(response, status);
}
+ public ResponseEntity> customPublicException(String message, HttpStatus status) {
+ Map response = new HashMap<>();
+ response.put("message", message);
+ return new ResponseEntity<>(response, status);
+ }
+
}
diff --git a/src/main/java/pl/databucket/server/exception/ForbiddenRepetitionException.java b/src/main/java/pl/databucket/server/exception/ForbiddenRepetitionException.java
new file mode 100644
index 0000000..77446cd
--- /dev/null
+++ b/src/main/java/pl/databucket/server/exception/ForbiddenRepetitionException.java
@@ -0,0 +1,13 @@
+package pl.databucket.server.exception;
+
+public class ForbiddenRepetitionException extends Exception {
+
+ public ForbiddenRepetitionException(Exception e) {
+ super(e);
+ }
+
+ public ForbiddenRepetitionException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/main/java/pl/databucket/server/repository/UserRepository.java b/src/main/java/pl/databucket/server/repository/UserRepository.java
index d703cf3..33a2d3d 100644
--- a/src/main/java/pl/databucket/server/repository/UserRepository.java
+++ b/src/main/java/pl/databucket/server/repository/UserRepository.java
@@ -10,7 +10,9 @@
@Repository
public interface UserRepository extends JpaRepository {
User findByUsername(String name);
+ User findByEmail(String email);
boolean existsByUsername(String name);
+ boolean existsByEmail(String email);
List findAllByIdIn(Iterable ids);
List findAllByOrderById();
List findUsersByProjectsContainsOrderById(Project project);
diff --git a/src/main/java/pl/databucket/server/security/TokenProvider.java b/src/main/java/pl/databucket/server/security/TokenProvider.java
index 0e2189f..bb49b42 100644
--- a/src/main/java/pl/databucket/server/security/TokenProvider.java
+++ b/src/main/java/pl/databucket/server/security/TokenProvider.java
@@ -21,6 +21,7 @@ public class TokenProvider implements Serializable {
public static final String AUTHORITIES_KEY = "a-key";
public static final String PROJECT_ID = "p-id";
+ public static final String CONTENT = "content";
@Value("${jwt.secret}")
private String singingKey;
@@ -48,7 +49,7 @@ private Claims getAllClaimsFromToken(String token) {
.getBody();
}
- private Boolean isTokenExpired(String token) {
+ public Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
@@ -72,6 +73,22 @@ public Boolean validateToken(String token, UserDetails userDetails) {
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
+ public String packToJwts(String content) {
+ return Jwts.builder()
+ .claim(CONTENT, content)
+ .signWith(SignatureAlgorithm.HS256, singingKey)
+ .setIssuedAt(new Date(System.currentTimeMillis()))
+ .setExpiration(new Date(System.currentTimeMillis() + 48 * 60 * 60 * 1000))
+ .compact();
+ }
+
+ public String unpackFromJwts(String jwts) {
+ return Jwts.parser()
+ .setSigningKey(singingKey)
+ .parseClaimsJws(jwts)
+ .getBody().get(CONTENT).toString();
+ }
+
UsernamePasswordAuthenticationToken getAuthentication(final String token, final CustomUserDetails customUserDetails) {
final JwtParser jwtParser = Jwts.parser().setSigningKey(singingKey);
diff --git a/src/main/java/pl/databucket/server/security/WebSecurityConfig.java b/src/main/java/pl/databucket/server/security/WebSecurityConfig.java
index d41726a..dccb33f 100644
--- a/src/main/java/pl/databucket/server/security/WebSecurityConfig.java
+++ b/src/main/java/pl/databucket/server/security/WebSecurityConfig.java
@@ -53,7 +53,7 @@ protected void configure(HttpSecurity http) throws Exception {
.authorizeRequests()
.antMatchers(
"/",
- "/api/public/**", // public endpoint for authorization
+ "/api/public/**", // public endpoint
"/**/static/**",
"/actuator/**",
"/**/favicon.ico",
diff --git a/src/main/java/pl/databucket/server/service/ManageProjectService.java b/src/main/java/pl/databucket/server/service/ManageProjectService.java
index eed3199..2c1c867 100644
--- a/src/main/java/pl/databucket/server/service/ManageProjectService.java
+++ b/src/main/java/pl/databucket/server/service/ManageProjectService.java
@@ -34,6 +34,7 @@ public Project createProject(ManageProjectDto manageProjectDto) {
project.setDescription(manageProjectDto.getDescription());
project.setExpirationDate(manageProjectDto.getExpirationDate());
project.setEnabled(manageProjectDto.isEnabled());
+ project.setPublicVisible(manageProjectDto.isPublicVisible());
projectRepository.saveAndFlush(project);
if (manageProjectDto.getUsersIds() != null) {
@@ -77,6 +78,7 @@ public Project modifyProject(ManageProjectDto manageProjectDto) throws ItemNotFo
project.setName(manageProjectDto.getName());
project.setDescription(manageProjectDto.getDescription());
project.setEnabled(manageProjectDto.isEnabled());
+ project.setPublicVisible(manageProjectDto.isPublicVisible());
project.setExpirationDate(manageProjectDto.getExpirationDate());
if (manageProjectDto.getUsersIds() != null) {
diff --git a/src/main/java/pl/databucket/server/service/ManageUserService.java b/src/main/java/pl/databucket/server/service/ManageUserService.java
index 05f505a..d79306f 100644
--- a/src/main/java/pl/databucket/server/service/ManageUserService.java
+++ b/src/main/java/pl/databucket/server/service/ManageUserService.java
@@ -1,20 +1,32 @@
package pl.databucket.server.service;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.AccountExpiredException;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
+import pl.databucket.server.configuration.Constants;
import pl.databucket.server.dto.AuthReqDTO;
+import pl.databucket.server.dto.ForgotPasswordReqDTO;
import pl.databucket.server.dto.ManageUserDtoRequest;
+import pl.databucket.server.dto.SignUpDtoRequest;
import pl.databucket.server.entity.Project;
+import pl.databucket.server.entity.Role;
import pl.databucket.server.entity.User;
+import pl.databucket.server.exception.ForbiddenRepetitionException;
import pl.databucket.server.exception.ItemAlreadyExistsException;
import pl.databucket.server.exception.SomeItemsNotFoundException;
import pl.databucket.server.repository.ProjectRepository;
import pl.databucket.server.repository.RoleRepository;
import pl.databucket.server.repository.UserRepository;
+import pl.databucket.server.security.TokenProvider;
+import pl.databucket.server.service.mail.MailSenderService;
-import java.util.HashSet;
-import java.util.List;
+import javax.mail.AuthenticationFailedException;
+import javax.mail.MessagingException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
@Service(value = "manageUserService")
@@ -32,6 +44,12 @@ public class ManageUserService {
@Autowired
private BCryptPasswordEncoder bcryptEncoder;
+ @Autowired
+ private TokenProvider jwtTokenUtil;
+
+ @Autowired
+ private MailSenderService mailSenderService;
+
public List getUsers() {
return userRepository.findAllByOrderById();
@@ -43,6 +61,7 @@ public User createUser(ManageUserDtoRequest manageUserDtoRequest) throws ItemAlr
User newUser = new User();
newUser.setUsername(manageUserDtoRequest.getUsername());
+ newUser.setEmail(manageUserDtoRequest.getEmail());
newUser.setDescription(manageUserDtoRequest.getDescription());
newUser.setEnabled(manageUserDtoRequest.isEnabled());
newUser.setExpirationDate(manageUserDtoRequest.getExpirationDate());
@@ -58,9 +77,60 @@ public User createUser(ManageUserDtoRequest manageUserDtoRequest) throws ItemAlr
return userRepository.save(newUser);
}
+ public void signUpUser(SignUpDtoRequest signUpDtoRequest) throws MessagingException {
+ Set roles = new HashSet<>();
+ roles.add(roleRepository.findByName(Constants.ROLE_MEMBER));
+
+ User newUser = new User();
+ newUser.setUsername(signUpDtoRequest.getUsername());
+ newUser.setEmail(signUpDtoRequest.getEmail());
+ newUser.setPassword(bcryptEncoder.encode(signUpDtoRequest.getPassword()));
+ newUser.setEnabled(false);
+ newUser.setChangePassword(false);
+ newUser.setRoles(roles);
+ newUser = userRepository.save(newUser);
+
+ mailSenderService.sendConfirmationLink(newUser, signUpDtoRequest.getUrl() + jwtTokenUtil.packToJwts(signUpDtoRequest.getEmail()));
+ }
+
+ public void signUpUserConfirmation(String jwts) throws ForbiddenRepetitionException, MessagingException {
+ if (!jwtTokenUtil.isTokenExpired(jwts)) {
+ String email = jwtTokenUtil.unpackFromJwts(jwts);
+ User user = userRepository.findByEmail(email);
+
+ if (user.getEnabled())
+ throw new ForbiddenRepetitionException("The user has been already activated");
+
+ user.setEnabled(true);
+ userRepository.save(user);
+
+ mailSenderService.sendRegistrationConfirmation(user);
+ } else
+ throw new AccountExpiredException("The confirmation link is expired!");
+ }
+
+ public void forgotPasswordMessage(ForgotPasswordReqDTO forgotPasswordReqDTO) throws ForbiddenRepetitionException, MessagingException, UsernameNotFoundException {
+ if (!userRepository.existsByEmail(forgotPasswordReqDTO.getEmail()))
+ throw new UsernameNotFoundException("User not found! Make sure you have entered the correct email address.");
+
+ User user = userRepository.findByEmail(forgotPasswordReqDTO.getEmail());
+
+ if (user.getLastSendEmailForgotPasswordLinkDate() != null) {
+ Instant lastSendEmailTime = user.getLastSendEmailForgotPasswordLinkDate().toInstant();
+ Instant currentTime = Instant.now();
+
+ if (ChronoUnit.HOURS.between(lastSendEmailTime, currentTime) < 48)
+ throw new ForbiddenRepetitionException("The confirmation link has been send within last 48 hours. Search it in your email inbox.");
+ }
+
+ mailSenderService.sendForgotPasswordLink(user, forgotPasswordReqDTO.getUrl() + jwtTokenUtil.packToJwts(forgotPasswordReqDTO.getEmail()));
+ user.setLastSendEmailForgotPasswordLinkDate(new Date());
+ userRepository.save(user);
+ }
public User modifyUser(ManageUserDtoRequest manageUserDtoRequest) {
User user = userRepository.findByUsername(manageUserDtoRequest.getUsername());
+ user.setEmail(manageUserDtoRequest.getEmail());
user.setDescription(manageUserDtoRequest.getDescription());
user.setEnabled(manageUserDtoRequest.isEnabled());
user.setExpirationDate(manageUserDtoRequest.getExpirationDate());
@@ -78,7 +148,7 @@ public User modifyUser(ManageUserDtoRequest manageUserDtoRequest) {
return userRepository.save(user);
}
- public void resetPassword(AuthReqDTO userDto) {
+ public void resetAndSendPassword(AuthReqDTO userDto) {
User user = userRepository.findByUsername(userDto.getUsername());
if (user != null) {
user.setPassword(bcryptEncoder.encode(userDto.getPassword()));
@@ -87,4 +157,39 @@ public void resetPassword(AuthReqDTO userDto) {
} else
throw new IllegalArgumentException("The given user does not exist.");
}
+
+ public void resetAndSendPassword(String jwts) throws ForbiddenRepetitionException, MessagingException {
+ if (!jwtTokenUtil.isTokenExpired(jwts)) {
+ String email = jwtTokenUtil.unpackFromJwts(jwts);
+ User user = userRepository.findByEmail(email);
+
+ if (user.getLastSendEmailTempPasswordDate() != null) {
+ Instant lastSendEmailTime = user.getLastSendEmailTempPasswordDate().toInstant();
+ Instant currentTime = Instant.now();
+
+ if (ChronoUnit.HOURS.between(lastSendEmailTime, currentTime) < 48)
+ throw new ForbiddenRepetitionException("The temporary password has been send within last 48 hours. Search it in your email inbox.");
+ }
+
+ int length = 11;
+ String small_letter = "abcdefghijklmnopqrstuvwxyz";
+ String numbers = "0123456789";
+
+ String finalString = small_letter + numbers;
+ Random random = new Random();
+ char[] password = new char[length];
+ for (int i = 0; i < length; i++)
+ password[i] = finalString.charAt(random.nextInt(finalString.length()));
+
+ String newPassword = new String(password);
+
+ mailSenderService.sendNewPassword(user, newPassword);
+
+ user.setPassword(bcryptEncoder.encode(newPassword));
+ user.setChangePassword(true);
+ user.setLastSendEmailTempPasswordDate(new Date());
+ userRepository.save(user);
+ } else
+ throw new AccountExpiredException("The confirmation link is expired!");
+ }
}
diff --git a/src/main/java/pl/databucket/server/service/mail/Mail.java b/src/main/java/pl/databucket/server/service/mail/Mail.java
new file mode 100644
index 0000000..44d181d
--- /dev/null
+++ b/src/main/java/pl/databucket/server/service/mail/Mail.java
@@ -0,0 +1,18 @@
+package pl.databucket.server.service.mail;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.springframework.web.multipart.MultipartFile;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class Mail {
+ private String address;
+ private String title;
+ private String message;
+ private MultipartFile file;
+}
diff --git a/src/main/java/pl/databucket/server/service/mail/MailSenderHandler.java b/src/main/java/pl/databucket/server/service/mail/MailSenderHandler.java
new file mode 100644
index 0000000..f0868ee
--- /dev/null
+++ b/src/main/java/pl/databucket/server/service/mail/MailSenderHandler.java
@@ -0,0 +1,51 @@
+package pl.databucket.server.service.mail;
+
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import java.io.IOException;
+
+public class MailSenderHandler {
+
+ private JavaMailSender sender;
+ private MimeMessage message;
+ private MimeMessageHelper messageHelper;
+
+ public MailSenderHandler(JavaMailSender sender) throws MessagingException {
+ this.sender = sender;
+ message = sender.createMimeMessage();
+ messageHelper = new MimeMessageHelper(message, true, "UTF-8");
+ }
+
+ public void setFrom(String fromAddress) throws MessagingException {
+ messageHelper.setFrom(fromAddress);
+ }
+
+ public void setTo(String toAddress) throws MessagingException {
+ messageHelper.setTo(toAddress);
+ }
+
+ public void setSubject(String subject) throws MessagingException {
+ messageHelper.setSubject(subject);
+ }
+
+ public void setText(String text, boolean useHtml) throws MessagingException {
+ messageHelper.setText(text, useHtml);
+ }
+
+ public void setAttach(String displayFileName, MultipartFile file) throws MessagingException {
+ messageHelper.addAttachment(displayFileName, file);
+ }
+
+ public void setInline(String contentId, MultipartFile file) throws MessagingException, IOException {
+ messageHelper.addInline(contentId, new ByteArrayResource(file.getBytes()), "image/jpeg");
+ }
+
+ public void send() {
+ sender.send(message);
+ }
+}
diff --git a/src/main/java/pl/databucket/server/service/mail/MailSenderService.java b/src/main/java/pl/databucket/server/service/mail/MailSenderService.java
new file mode 100644
index 0000000..c660c3e
--- /dev/null
+++ b/src/main/java/pl/databucket/server/service/mail/MailSenderService.java
@@ -0,0 +1,79 @@
+package pl.databucket.server.service.mail;
+
+import com.sun.mail.util.MailConnectException;
+import lombok.AllArgsConstructor;
+import org.springframework.mail.MailSendException;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+import pl.databucket.server.entity.User;
+
+import javax.mail.MessagingException;
+
+@Service
+@AllArgsConstructor
+public class MailSenderService {
+
+ private JavaMailSender sender;
+
+ public void sendConfirmationLink(User user, String link) throws MessagingException {
+ String htmlContent = "Thanks for signing up to Databucket. ";
+
+ htmlContent += "To confirm your registration, copy and paste the URL into your browser:
";
+ htmlContent += "" + link + " ";
+
+ htmlContent += "Please note that the link will expire in 48 hours.
";
+ htmlContent += "If you didn't request a registration, don't worry. You can safely ignore this email.
";
+
+ MailSenderHandler mailHandler = new MailSenderHandler(sender);
+ mailHandler.setTo(user.getEmail());
+ mailHandler.setSubject("Databucket account - registration");
+ mailHandler.setText(htmlContent, true);
+ mailHandler.send();
+ }
+
+ public void sendRegistrationConfirmation(User user) throws MessagingException {
+ String htmlContent = "Databucket account ";
+
+ htmlContent += "Your account has been activated.
";
+ htmlContent += "Log in to the application and request to be assigned to the project.
";
+
+ MailSenderHandler mailHandler = new MailSenderHandler(sender);
+ mailHandler.setTo(user.getEmail());
+ mailHandler.setSubject("Databucket account - activated");
+ mailHandler.setText(htmlContent, true);
+ mailHandler.send();
+ }
+
+ public void sendForgotPasswordLink(User user, String link) throws MessagingException {
+ String htmlContent = "Databucket account ";
+ htmlContent += "Forgot your password? ";
+ htmlContent += "We received a request to reset the password for your account.
";
+
+ htmlContent += "To reset your password, copy and paste the URL into your browser:
";
+ htmlContent += "" + link + " ";
+
+ htmlContent += "Please note that the link will expire in 48 hours.
";
+ htmlContent += "If you didn't request a reset, don't worry. You can safely ignore this email.
";
+
+ MailSenderHandler mailHandler = new MailSenderHandler(sender);
+ mailHandler.setTo(user.getEmail());
+ mailHandler.setSubject("Databucket account - forgot your password?");
+ mailHandler.setText(htmlContent, true);
+ mailHandler.send();
+ }
+
+ public void sendNewPassword(User user, String password) throws MessagingException {
+ String htmlContent = "Databucket account ";
+ htmlContent += "Your password has been reset.
";
+ htmlContent += "Below you can find your temporary password:
";
+ htmlContent += "" + password + " ";
+
+ MailSenderHandler mailHandler = new MailSenderHandler(sender);
+ mailHandler.setTo(user.getEmail());
+ mailHandler.setSubject("Databucket account - temporary password");
+ mailHandler.setText(htmlContent, true);
+ mailHandler.send();
+ }
+
+
+}
diff --git a/src/main/resources/application-default.yaml b/src/main/resources/application-default.yaml
index ab00256..6f122b4 100644
--- a/src/main/resources/application-default.yaml
+++ b/src/main/resources/application-default.yaml
@@ -11,3 +11,10 @@ spring:
url: jdbc:postgresql://localhost:5432/databucket
username: databucket_user
password: databucket_user
+ mail:
+ host: smtp.gmail.com
+ port: 587
+ username: databucket.info@gmail.com
+ password: *
+ properties.mail.smtp.auth: true
+ properties.mail.smtp.starttls.enable: true
diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt
index 6bfe80a..61e2612 100644
--- a/src/main/resources/banner.txt
+++ b/src/main/resources/banner.txt
@@ -7,5 +7,5 @@
██████╔╝██║ ██║ ██║ ██║ ██║██████╔╝╚██████╔╝╚██████╗██║ ██╗███████╗ ██║ ██████╔╝
╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝
-Databucket version: (v3.3.3)
+Databucket version: (v3.4.0)
Spring Boot version:${spring-boot.formatted-version}