Skip to content

Commit dcffb98

Browse files
committed
Profile & passwords work
1 parent d3328d6 commit dcffb98

File tree

16 files changed

+144
-35
lines changed

16 files changed

+144
-35
lines changed

apps/react-pouchdb-example/src/Notes.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export class Notes extends React.Component<Props, State> {
8282
<textarea
8383
className="textarea"
8484
onChange={this.updateNote}
85-
onKeyPress={this.addNoteWithEnter}
85+
onKeyDown={this.addNoteWithEnter}
8686
value={this.state.note}
8787
/>
8888
</div>

apps/react-pouchdb-example/src/Tasks.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export class Tasks extends React.Component<Props, State> {
100100
className="input"
101101
type="text"
102102
onChange={this.updateTask}
103-
onKeyPress={this.addTaskWithEnter}
103+
onKeyDown={this.addTaskWithEnter}
104104
value={this.state.name}
105105
/>
106106
</div>

apps/template/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"webpack:serve": "webpack serve",
1515
"webpack:build": "NODE_ENV=production webpack",
1616
"dev": "NODE_ENV=development concurrently \"npm run webpack:serve\" \"npm run start:dev\"",
17-
"test": "jest",
17+
"test": "jest --detectOpenHandles",
1818
"test:watch": "jest -w",
1919
"test:coverage": "jest --coverage",
2020
"lint": "eslint --ext js,jsx,ts,tsx ./src/",
@@ -36,4 +36,4 @@
3636
"concurrently": "^8.2.2",
3737
"supertest": "^6.3.3"
3838
}
39-
}
39+
}

apps/template/src/App.test.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import "@testing-library/jest-dom";
22
import { render, screen, fireEvent, act } from "@testing-library/react";
33
import userEvent from "@testing-library/user-event";
4-
import App, { ItemData } from "./App";
4+
import App from "./App";
5+
import { ItemData } from "./views";
56
import { SessionContext } from "./Session";
67

78
const output = [

apps/template/src/components/Input.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,27 @@ export function Input({
3030
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
3131
onChange(e.currentTarget.value);
3232
};
33-
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>): void => {
33+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
3434
if (e.key === "Enter") {
3535
e.preventDefault();
3636
onEnter();
3737
}
3838
};
3939

4040
return (
41-
<>
41+
<div style={{ marginBottom: ".75em" }}>
4242
{label && <label htmlFor={name}>{label}</label>}
4343
<input
4444
type={type}
4545
id={name}
4646
onChange={handleChange}
47-
onKeyPress={handleKeyPress}
47+
onKeyDown={handleKeyDown}
4848
placeholder={placeholder}
4949
value={value}
5050
{...attrs}
5151
/>
52-
{error && <div>{error}</div>}
53-
</>
52+
{error && <div style={{ color: "red" }}>{error}</div>}
53+
</div>
5454
);
5555
}
5656

apps/template/src/helpers/fetchApi.tsx

+40-6
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,46 @@ export function fetchApi<T, P>(
1212
"Content-Type": "application/json",
1313
},
1414
body: JSON.stringify(data),
15-
}).then((response) => {
16-
if (!response.ok) {
17-
throw new Error(response.statusText);
18-
}
19-
return response.json() as Promise<T>;
20-
});
15+
})
16+
.then((response) => {
17+
return Promise.all([
18+
new Promise<Response>((resolve, reject) => {
19+
resolve(response);
20+
}),
21+
response.json() as Promise<T>,
22+
]);
23+
})
24+
.then(([response, json]) => {
25+
if (!response.ok) {
26+
throw new ApiError(
27+
response.status,
28+
response.statusText,
29+
json as Record<string, unknown>
30+
);
31+
}
32+
33+
return json;
34+
});
2135
}
2236

2337
export default fetchApi;
38+
39+
export class ApiError extends Error {
40+
#code: number;
41+
42+
get code(): number {
43+
return this.#code;
44+
}
45+
46+
#body: Record<string, unknown>;
47+
48+
get body(): Record<string, unknown> {
49+
return this.#body;
50+
}
51+
52+
constructor(code: number, message: string, body: Record<string, unknown>) {
53+
super(message);
54+
this.#code = code;
55+
this.#body = body;
56+
}
57+
}

apps/template/src/views/Account.tsx

+65-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { useContext, useState } from "react";
22
import { SessionContext } from "../Session";
33
import { Header, Input, Spacer } from "../components";
4+
import fetchApi, { ApiError } from "../helpers/fetchApi";
5+
6+
type Errors = {
7+
name?: string;
8+
email?: string;
9+
current_password?: string;
10+
password?: string;
11+
repeat_password?: string;
12+
};
413

514
export type ProfileData = {
615
name: string;
@@ -23,23 +32,68 @@ export const DEFAULT_PASSWORD_DATA: PasswordData = {
2332
new_password2: "",
2433
};
2534

35+
// eslint-disable-next-line max-lines-per-function
2636
export function Account() {
2737
const { session } = useContext(SessionContext);
28-
const [profile, setProfile] = useState<ProfileData>(DEFAULT_PROFILE_DATA);
38+
const [profile, setProfile] = useState<ProfileData>({
39+
...DEFAULT_PROFILE_DATA,
40+
name: session.user?.name ?? "",
41+
email: session.user?.email ?? "",
42+
});
2943
const [password, setPassword] = useState<PasswordData>(DEFAULT_PASSWORD_DATA);
44+
const [errors, setErrors] = useState<Errors>({});
3045

3146
const storeProfile = (key: keyof ProfileData, value: string) => {
3247
setProfile({ ...profile, [key]: value });
3348
};
3449
const saveProfile = () => {
35-
console.log(profile);
50+
setErrors({});
51+
fetchApi<ProfileData, ProfileData>(
52+
"/auth/user",
53+
session?.token || "",
54+
"put",
55+
profile
56+
)
57+
.then((user) => {
58+
setProfile(user);
59+
setErrors({});
60+
})
61+
.catch((err: ApiError) => {
62+
if (err.code === 400) {
63+
setErrors({ ...(err.body.errors as Errors) });
64+
}
65+
});
3666
};
3767

3868
const storePassword = (key: keyof PasswordData, value: string) => {
3969
setPassword({ ...password, [key]: value });
4070
};
4171
const savePassword = () => {
42-
console.log(password);
72+
setErrors({});
73+
74+
if (password.new_password1 !== password.new_password2) {
75+
setErrors({ repeat_password: "Passwords do not match" });
76+
return;
77+
}
78+
79+
fetchApi<PasswordData, { current_password: string; password: string }>(
80+
"/auth/password",
81+
session?.token || "",
82+
"post",
83+
{
84+
current_password: password.current_password,
85+
password: password.new_password1,
86+
}
87+
)
88+
.then(() => {
89+
setPassword(DEFAULT_PASSWORD_DATA);
90+
setErrors({});
91+
})
92+
.catch((err: ApiError) => {
93+
if (err.code === 400) {
94+
setErrors({ ...(err.body.errors as Errors) });
95+
}
96+
});
4397
};
4498

4599
return (
@@ -50,12 +104,14 @@ export function Account() {
50104
name="name"
51105
label="Name"
52106
value={profile.name}
107+
error={errors.name}
53108
onChange={(value) => storeProfile("name", value)}
54109
/>
55110
<Input
56111
name="email"
57112
label="Email"
58113
value={profile.email}
114+
error={errors.email}
59115
onChange={(value) => storeProfile("email", value)}
60116
/>
61117
<button onClick={saveProfile}>Save</button>
@@ -66,21 +122,24 @@ export function Account() {
66122
name="current_password"
67123
type="password"
68124
label="Current Password"
69-
value={password.current_password ?? ""}
125+
value={password.current_password}
126+
error={errors.current_password}
70127
onChange={(value) => storePassword("current_password", value)}
71128
/>
72129
<Input
73130
name="new_password1"
74131
type="password"
75132
label="New Password"
76-
value={password.new_password1 ?? ""}
133+
value={password.new_password1}
134+
error={errors.password}
77135
onChange={(value) => storePassword("new_password1", value)}
78136
/>
79137
<Input
80138
name="new_password2"
81139
type="password"
82140
label="Repeat New Password"
83-
value={password.new_password2 ?? ""}
141+
value={password.new_password2}
142+
error={errors.repeat_password}
84143
onChange={(value) => storePassword("new_password2", value)}
85144
/>
86145

jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "@stanlemon/webdev/jest.config.js";

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"type": "module",
23
"engines": {
34
"node": ">=18.0"
45
},
@@ -12,8 +13,9 @@
1213
"scripts": {
1314
"prepare": "husky install",
1415
"lint": "eslint --ext .js,.ts,.tsx --ignore-path .gitignore .",
15-
"test": "npm run test --workspaces --if-present",
16-
"test:coverage": "npm run test:coverage --workspaces --if-present",
16+
"test": "npm run build && jest --detectOpenHandles --silent",
17+
"test:watch": "npm run build && jest --silent -w",
18+
"test:coverage": "npm run build && jest --silent --coverage",
1719
"build": "npm run build --workspaces --if-present",
1820
"publish": "node publish.js"
1921
}

packages/react-couchdb-authentication/src/components/LoginView.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function LoginView(props: LoginViewProps): React.ReactElement {
3333
type="text"
3434
value={props.username}
3535
onChange={props.setUsername}
36-
onKeyPress={hitEnter}
36+
onKeyDown={hitEnter}
3737
/>
3838
</p>
3939
<p>
@@ -43,7 +43,7 @@ export function LoginView(props: LoginViewProps): React.ReactElement {
4343
type="password"
4444
value={props.password}
4545
onChange={props.setPassword}
46-
onKeyPress={hitEnter}
46+
onKeyDown={hitEnter}
4747
/>
4848
</p>
4949

packages/server-with-auth/src/routes/auth.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,17 @@ export default function authRoutes({
7878
return next(err);
7979
}
8080
if (!user) {
81-
res.status(401).json({
81+
return res.status(401).json({
8282
message: "Incorrect username or password.",
8383
});
8484
}
8585

86-
req.logIn(user, (err) => {
86+
return req.logIn(user, (err) => {
8787
if (err) {
8888
return next(err);
8989
}
9090

91-
dao
91+
return dao
9292
.updateUser(user.id, {
9393
last_logged_in: new Date(),
9494
})
@@ -97,7 +97,7 @@ export default function authRoutes({
9797

9898
eventEmitter.emit(EVENTS.USER_LOGIN, user);
9999

100-
res.json({
100+
return res.json({
101101
token,
102102
user: formatOutput(update, HIDDEN_FIELDS),
103103
});
@@ -251,7 +251,7 @@ export default function authRoutes({
251251
if (!user) {
252252
res.status(400).json({
253253
errors: {
254-
currentPassword: "Current password is incorrect",
254+
current_password: "Current password is incorrect",
255255
},
256256
});
257257
return;

packages/server-with-auth/src/routes/auth.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ describe("/auth", () => {
264264
const username = "test" + uuidv4();
265265
const password1 = "FirstP@ssw0rd";
266266
const password2 = "SecondP@ssw0rd";
267-
const data = { password: password2, currentPassword: password1 };
267+
const data = { password: password2, current_password: password1 };
268268

269269
const { token } = await signupAndLogin(username, password1);
270270

packages/server-with-auth/src/schema/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const RESET_PASSWORD = Joi.object({
2424

2525
export const CHANGE_PASSWORD = Joi.object({
2626
password: PASSWORD,
27-
currentPassword: PASSWORD,
27+
current_password: PASSWORD.label("Current Password"),
2828
});
2929

3030
export const USER = Joi.object({});
File renamed without changes.

packages/webdev/jest.setup.js

+12
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,15 @@ if (!global.setImmediate) {
44
// This is as gross as it looks. It's a workaround for using PouchDB in tests.
55
global.setImmediate = global.setTimeout; // ts: as unknown as typeof setImmediate;
66
}
7+
8+
if (process.argv.includes("--silent")) {
9+
global.console = {
10+
...console,
11+
// Comment to expose a specific log level
12+
log: jest.fn(),
13+
debug: jest.fn(),
14+
info: jest.fn(),
15+
warn: jest.fn(),
16+
error: jest.fn(),
17+
};
18+
}

packages/webdev/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
"type": "commonjs",
1717
"scripts": {
18-
"test": "WEBDEV_ENTRY=./webpack.test.tsx webpack build",
18+
"build": "WEBDEV_ENTRY=./example.tsx webpack build",
1919
"lint": "eslint --ext js,jsx,ts,tsx ./",
2020
"lint:format": "eslint --fix --ext js,jsx,ts,tsx ./"
2121
},
@@ -63,4 +63,4 @@
6363
"react": ">=17.0.0",
6464
"react-dom": ">=17.0.0"
6565
}
66-
}
66+
}

0 commit comments

Comments
 (0)