diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 8236a1d6..7396fe47 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -70,7 +70,7 @@ var baseDockerName = "frikky/shuffle" var dbclient *datastore.Client type Userapi struct { - Username string `datastore:"Username"` + Username string `datastore:"username"` ApiKey string `datastore:"apikey"` } @@ -183,7 +183,7 @@ type session struct { } type loginStruct struct { - Username string `json:"Username"` + Username string `json:"username"` Password string `json:"password"` } @@ -810,9 +810,9 @@ func parseLoginParameters(resp http.ResponseWriter, request *http.Request) (logi // Removed for localhost func checkPasswordStrength(password string) error { // Check password strength here - //if len(password) < 10 { - // return errors.New("Minimum password length is 10.") - //} + if len(password) < 10 { + return errors.New("Minimum password length is 10.") + } //if len(password) > 128 { // return errors.New("Maximum password length is 128.") @@ -1334,7 +1334,7 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { log.Printf("Updated apikey for user %s", userInfo.Username) resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "Username": "%s", "verified": %t, "apikey": "%s"}`, userInfo.Username, userInfo.Verified, userInfo.ApiKey))) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "username": "%s", "verified": %t, "apikey": "%s"}`, userInfo.Username, userInfo.Verified, userInfo.ApiKey))) } func handleSettings(resp http.ResponseWriter, request *http.Request) { @@ -1380,7 +1380,7 @@ func handleSettings(resp http.ResponseWriter, request *http.Request) { } resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "Username": "%s", "verified": %t, "apikey": "%s"}`, UserInfo.Username, UserInfo.Verified, UserInfo.ApiKey))) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "username": "%s", "verified": %t, "apikey": "%s"}`, UserInfo.Username, UserInfo.Verified, UserInfo.ApiKey))) } func handleInfo(resp http.ResponseWriter, request *http.Request) { @@ -1402,9 +1402,10 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() sessionToken := c.Value + log.Printf("Found session %s", sessionToken) session, err := getSession(ctx, sessionToken) if err != nil { - //log.Printf("Session %#v doesn't exist: %s", session, err) + log.Printf("Session %#v doesn't exist: %s", session, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": "No session"}`)) return @@ -1501,9 +1502,10 @@ type passwordReset struct { } type passwordChange struct { - Password1 string `json:"newpassword"` - Password2 string `json:"newpassword2"` - Password3 string `json:"currentpassword"` + Username string `json:"username"` + Newpassword string `json:"newpassword"` + Newpassword2 string `json:"newpassword2"` + Currentpassword string `json:"currentpassword"` } func handlePasswordResetMail(resp http.ResponseWriter, request *http.Request) { @@ -1524,7 +1526,7 @@ func handlePasswordResetMail(resp http.ResponseWriter, request *http.Request) { } type passwordReset struct { - Username string `json:"Username"` + Username string `json:"username"` } var t passwordReset @@ -1679,13 +1681,12 @@ func handlePasswordReset(resp http.ResponseWriter, request *http.Request) { } func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { - log.Println("Handling password change") - cors := handleCors(resp, request) if cors { return } + log.Println("Handling password change") body, err := ioutil.ReadAll(request.Body) if err != nil { log.Println("Failed reading body") @@ -1694,6 +1695,7 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { return } + // Get the current user - check if they're admin or the "username" user. var t passwordChange err = json.Unmarshal(body, &t) if err != nil { @@ -1703,21 +1705,41 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { return } - if t.Password1 != t.Password2 { + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) - err := "Passwords don't match" - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + resp.Write([]byte(`{"success": false}`)) return } - if len(t.Password1) < 10 || len(t.Password2) < 10 { + curUserFound := false + if t.Username != user.Username && user.Role != "admin" { resp.WriteHeader(401) - err := "Passwords don't match - 2" - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + resp.Write([]byte(`{"success": false, "reason": "Admin required to change others' passwords"}`)) return + } else if t.Username == user.Username { + curUserFound = true } - err = checkPasswordStrength(t.Password3) + if user.Role != "admin" { + if t.Newpassword != t.Newpassword2 { + err := "Passwords don't match" + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + + if len(t.Newpassword) < 10 || len(t.Newpassword2) < 10 { + err := "Passwords too short - 2" + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + } + + // Current password + err = checkPasswordStrength(t.Newpassword) if err != nil { log.Printf("Bad password strength: %s", err) resp.WriteHeader(401) @@ -1725,56 +1747,52 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { return } - // Check cookie - c, err := request.Cookie("session_token") - if err != nil { - log.Printf("User doesn't have sessiontoken on pw change: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "You're not logged in."}`))) - return - } - ctx := context.Background() - // Validate with User - sessionToken := c.Value - session, err := getSession(ctx, sessionToken) - if err != nil { - log.Printf("Session %s doesn't exist (password change): %s", session.Session, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "You're not logged in"}`)) - return + if !curUserFound { + log.Printf("Have to find a different user") + q := datastore.NewQuery("Users").Filter("Username =", strings.ToLower(t.Username)) + var users []User + _, err = dbclient.GetAll(ctx, q, &users) + if err != nil { + log.Printf("Failed getting user %s", t.Username) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) + return + } + + if len(users) != 1 { + log.Printf(`Found multiple users with the same username: %s: %d`, t.Username, len(users)) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Found multiple users with the same username: %s"}`, t.Username))) + return + } + + user = users[0] + } else { + // Admins can re-generate others' passwords as well. + if user.Role != "admin" { + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(t.Newpassword)) + if err != nil { + log.Printf("Bad password for %s: %s", user.Username, err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) + return + } + } } - // Get session first - // Should basically never happen - Userdata, err := getUser(ctx, session.Id) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(t.Newpassword), 8) if err != nil { - log.Printf("Username %s doesn't exist (pw change): %s", session.Username, err) + log.Printf("New password failure for %s: %s", user.Username, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) return } - err = bcrypt.CompareHashAndPassword([]byte(Userdata.Password), []byte(t.Password1)) + user.Password = string(hashedPassword) + err = setUser(ctx, &user) if err != nil { - log.Printf("Bad password for %s: %s", session.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - return - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(t.Password3), 8) - if err != nil { - log.Printf("Wrong password for %s: %s", Userdata.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - return - } - - Userdata.Password = string(hashedPassword) - err = setUser(ctx, Userdata) - if err != nil { - log.Printf("Error adding User %s: %s", Userdata.Username, err) + log.Printf("Error fixing password for user %s: %s", user.Username, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) return @@ -2057,6 +2075,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { } Userdata := users[0] + err = bcrypt.CompareHashAndPassword([]byte(Userdata.Password), []byte(data.Password)) if err != nil { log.Printf("Password for %s is incorrect: %s", data.Username, err) @@ -2065,7 +2084,14 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("%s SUCCESSFULLY LOGGED IN", data.Username) + if !Userdata.Active { + log.Printf("%s is not active, but tried to login", data.Username, err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "This user is deactivated"}`)) + return + } + + log.Printf("%s SUCCESSFULLY LOGGED IN with session %s", data.Username, Userdata.Session) //if !Userdata.Verified { // log.Printf("User %s is not verified", data.Username) // resp.WriteHeader(403) @@ -2100,7 +2126,6 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { } sessionToken := uuid.NewV4() - http.SetCookie(resp, &http.Cookie{ Name: "session_token", Value: sessionToken.String(), diff --git a/frontend/src/Admin.js b/frontend/src/Admin.js index 7c07c0fd..8e25f77f 100644 --- a/frontend/src/Admin.js +++ b/frontend/src/Admin.js @@ -1,5 +1,6 @@ import React, { useEffect} from 'react'; +import {Link} from 'react-router-dom'; import Paper from '@material-ui/core/Paper'; import List from '@material-ui/core/List'; import Divider from '@material-ui/core/Divider'; @@ -32,6 +33,8 @@ const Admin = (props) => { const [environments, setEnvironments] = React.useState([]); const [schedules, setSchedules] = React.useState([]) const [selectedUser, setSelectedUser] = React.useState({}) + const [newPassword, setNewPassword] = React.useState(""); + const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false) const alert = useAlert() @@ -64,6 +67,34 @@ const Admin = (props) => { }); } + const onPasswordChange = () => { + const data = {"username": selectedUser.username, "newpassword": newPassword} + const url = globalUrl+'/api/v1/passwordchange'; + fetch(url, { + mode: 'cors', + method: 'POST', + body: JSON.stringify(data), + credentials: 'include', + crossDomain: true, + withCredentials: true, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + .then(response => + response.json().then(responseJson => { + if (responseJson["success"] === false) { + alert.error("Failed setting new password") + } else { + alert.success("Changed password!") + } + }), + ) + .catch(error => { + alert.error("Err: ", error.toString()) + }); + } + const deleteUser = (data) => { // Just use this one? const url = globalUrl+'/api/v1/users/'+data.id @@ -85,7 +116,7 @@ const Admin = (props) => { if (!responseJson.success && responseJson.reason !== undefined) { alert.error("Failed to deactivate user: "+responseJson.reason) } else { - alert.success("Deleted user "+data.id) + alert.success("Deactivated user "+data.id) } }) @@ -287,10 +318,41 @@ const Admin = (props) => { modalUser[field] = value } + const generateApikey = () => { + fetch(globalUrl+"/api/v1/generateapikey", { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!") + } else { + getUsers() + } + + return response.json() + }) + .then((responseJson) => { + console.log("RESP: ", responseJson) + if (!responseJson.success && responseJson.reason !== undefined) { + alert.error("Failed getting new: "+responseJson.reason) + } else { + alert.success("Got new API key") + } + }) + .catch(error => { + console.log(error) + }); + } + const editUserModal = {setModalOpen(false)}} + open={selectedUserModalOpen} + onClose={() => {setSelectedUserModalOpen(false)}} PaperProps={{ style: { backgroundColor: surfaceColor, @@ -300,7 +362,55 @@ const Admin = (props) => { }, }} > - Add user + Edit user + + + setNewPassword(e.target.value)} + /> + onPasswordChange()} + > + Submit + + + + deleteUser(selectedUser)} + > + {selectedUser.active ? "Deactivate" : "Activate"} + + generateApikey(selectedUser)} + > + Get new API key + + const modalView = @@ -447,7 +557,6 @@ const Admin = (props) => { /> {users === undefined ? null : users.map(data => { - console.log("USER: ", data) return ( { style={{}} variant="outlined" color="primary" - onClick={() => deleteUser(data)} + onClick={() => { + setSelectedUserModalOpen(true) + setSelectedUser(data) + }} > - {data.active ? "Deactivate" : "Activate"} - - deleteUser(data)} - > - Edit + Edit user diff --git a/frontend/src/SettingsPage.js b/frontend/src/SettingsPage.js index d03f928f..02575957 100644 --- a/frontend/src/SettingsPage.js +++ b/frontend/src/SettingsPage.js @@ -6,6 +6,7 @@ import Divider from '@material-ui/core/Divider'; import {Link} from 'react-router-dom'; import TextField from '@material-ui/core/TextField'; +import { useAlert } from "react-alert"; //const tmpdata = { @@ -23,6 +24,7 @@ import TextField from '@material-ui/core/TextField'; // FIXME: Use isLoggedIn :) const Settings = (props) => { const { globalUrl, isLoaded, userdata, surfaceColor, inputColor } = props; + const alert = useAlert() const [username, setUsername] = useState(""); const [firstname, setFirstname] = useState(""); @@ -65,7 +67,7 @@ const Settings = (props) => { } const onPasswordChange = () => { - const data = {"currentpassword": currentPassword, "newpassword": newPassword, "newpassword2": newPassword2} + const data = {"username": userSettings.username, "currentpassword": currentPassword, "newpassword": newPassword, "newpassword2": newPassword2} const url = globalUrl+'/api/v1/passwordchange'; fetch(url, { mode: 'cors', @@ -82,7 +84,10 @@ const Settings = (props) => { response.json().then(responseJson => { if (responseJson["success"] === false) { setPasswordFormMessage(responseJson["reason"]) - } + } else { + alert.success("Changed password!") + setPasswordFormMessage("") + } }), ) .catch(error => {