Skip to content
This repository was archived by the owner on Apr 19, 2023. It is now read-only.

Commit a1cab6d

Browse files
✨ Add security settings and events
1 parent e844efa commit a1cab6d

File tree

10 files changed

+177
-5
lines changed

10 files changed

+177
-5
lines changed

layouts/default.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ body {
4545
margin: 0;
4646
}
4747
48+
a {
49+
color: darkblue;
50+
}
51+
4852
.container {
4953
max-width: 960px;
5054
margin: 0 auto;

nuxt.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const config: NuxtConfiguration = {
2727
plugins: [
2828
"~/plugins/axios",
2929
"~/plugins/vue-notification",
30+
"~/plugins/vue-timeago",
3031
{ src: "~/plugins/vuex-persist", ssr: false }
3132
],
3233
modules: ["@nuxtjs/axios", "@nuxtjs/pwa"],

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
"countries-and-timezones": "^1.0.1",
2020
"jwt-decode": "^2.2.0",
2121
"nuxt": "^2.4.0",
22+
"ua-parser-js": "^0.7.19",
2223
"vue-notification": "^1.3.16",
24+
"vue-timeago": "^5.1.2",
2325
"vuex-persist": "^2.0.0"
2426
},
2527
"devDependencies": {

pages/settings/emails.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export default class AccountSettings extends Vue {
104104
loading = "";
105105
newEmail = "";
106106
notificationEmails = 0;
107+
emails!: Email[];
107108
notificationEmailsGetter!: number;
108109
notificationOptions = {
109110
0: "Only mandatory security-related emails",

pages/settings/security.vue

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<template>
2+
<main>
3+
<Settings>
4+
<h1>Password &amp; security</h1>
5+
<p>
6+
You can login to your account with any of the following verified emails.
7+
</p>
8+
<Loading v-if="loading" :message="loading" />
9+
<div v-else>
10+
<h2>Change password</h2>
11+
<form @submit.prevent="changePassword">
12+
<input hidden type="text" autocomplete="username" />
13+
<Input
14+
:value="newPassword"
15+
type="password"
16+
autocomplete="new-password"
17+
label="New password"
18+
placeholder="Enter a secure password"
19+
required
20+
@input="val => (newPassword = val)"
21+
/>
22+
<button class="button button--color-primary">
23+
Change password
24+
</button>
25+
</form>
26+
<h2>Two-factor authentication</h2>
27+
<p>
28+
Adding a second factor increases security while logging in to your
29+
account and doing sensitive operations. 2FA is currently
30+
<strong>{{ user.twoFactorEnabled ? "enabled" : "disabled" }}</strong
31+
>.
32+
</p>
33+
<form @submit.prevent="saveNotifications">
34+
<button class="button button--color-primary">
35+
Enable 2FA
36+
</button>
37+
</form>
38+
<h2>Security events</h2>
39+
<p>
40+
These are the most recent security-related events from your account.
41+
You can
42+
<nuxt-link to="/settings/data">export your data</nuxt-link> for the
43+
entire list.
44+
</p>
45+
<table class="table">
46+
<thead>
47+
<tr>
48+
<th>Event</th>
49+
<th>Browser</th>
50+
<th>OS</th>
51+
<th>Location</th>
52+
<th>Time</th>
53+
</tr>
54+
</thead>
55+
<tbody>
56+
<tr
57+
v-for="(event, index) in securityEvents"
58+
:key="`${event.id}_${index}`"
59+
>
60+
<td>{{ event.type }}</td>
61+
<td>
62+
{{ parse(event.userAgent).browser.name }}
63+
</td>
64+
<td>
65+
{{ parse(event.userAgent).os.name }}
66+
</td>
67+
<td v-if="event.location">
68+
{{ event.location.region_code }},
69+
{{ event.location.country_code }}
70+
</td>
71+
<td>
72+
<timeago :datetime="event.createdAt" :auto-update="60" />
73+
</td>
74+
</tr>
75+
</tbody>
76+
</table>
77+
<p>
78+
If you don't recognize an event, you should immediately reset your
79+
password.
80+
</p>
81+
</div>
82+
</Settings>
83+
</main>
84+
</template>
85+
86+
<script lang="ts">
87+
import { Component, Vue, Watch } from "vue-property-decorator";
88+
import { mapGetters } from "vuex";
89+
import Settings from "@/components/Settings.vue";
90+
import Loading from "@/components/Loading.vue";
91+
import Input from "@/components/form/Input.vue";
92+
import Select from "@/components/form/Select.vue";
93+
import UAParser from "ua-parser-js";
94+
import { Email, SecurityEvent } from "../../types/settings";
95+
import { User } from "../../types/auth";
96+
97+
@Component({
98+
components: {
99+
Settings,
100+
Loading,
101+
Select,
102+
Input
103+
},
104+
computed: mapGetters({
105+
securityEvents: "settings/securityEvents",
106+
user: "auth/user"
107+
})
108+
})
109+
export default class AccountSettings extends Vue {
110+
loading = "";
111+
newPassword = "";
112+
user!: User[];
113+
securityEvents!: SecurityEvent[];
114+
115+
private mounted() {
116+
this.$store.dispatch("settings/getEvents");
117+
}
118+
119+
private parse(userAgent: string) {
120+
const parser = new UAParser(userAgent);
121+
return parser.getResult();
122+
}
123+
124+
private changePassword() {
125+
this.loading = "Updating your password";
126+
this.$store
127+
.dispatch("settings/updateUser", { password: this.newPassword })
128+
.then(() => (this.loading = ""));
129+
this.newPassword = "";
130+
}
131+
}
132+
</script>
133+
134+
<style lang="scss" scoped></style>

plugins/vue-timeago.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Vue from "vue";
2+
import VueTimeago from "vue-timeago";
3+
4+
Vue.use(VueTimeago, {});

store/settings.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { MutationTree, ActionTree, GetterTree } from "vuex";
2-
import { RootState, User, Email } from "../types/settings";
2+
import { RootState, User, Email, SecurityEvent } from "../types/settings";
33

4-
export const state = (): RootState => ({});
4+
export const state = (): RootState => ({
5+
emails: [],
6+
securityEvents: []
7+
});
58

69
export const mutations: MutationTree<RootState> = {
710
setUser(state: RootState, user: User): void {
@@ -10,9 +13,13 @@ export const mutations: MutationTree<RootState> = {
1013
setEmails(state: RootState, emails: Email[]): void {
1114
state.emails = emails;
1215
},
16+
setSecurityEvents(state: RootState, securityEvents: SecurityEvent[]): void {
17+
state.securityEvents = securityEvents;
18+
},
1319
clearAll(state: RootState): void {
1420
delete state.user;
1521
delete state.emails;
22+
delete state.securityEvents;
1623
}
1724
};
1825

@@ -40,11 +47,18 @@ export const actions: ActionTree<RootState, RootState> = {
4047
async makeEmailPrimary({ dispatch }, context) {
4148
await this.$axios.patch("/users/me", { primaryEmail: context });
4249
return dispatch("getEmails");
50+
},
51+
async getEvents({ commit }, context) {
52+
const securityEvents: SecurityEvent[] = (await this.$axios.get(
53+
"/users/me/events"
54+
)).data;
55+
commit("setSecurityEvents", securityEvents);
4356
}
4457
};
4558

4659
export const getters: GetterTree<RootState, RootState> = {
4760
user: state => state.user,
4861
emails: state => state.emails,
62+
securityEvents: state => state.securityEvents,
4963
notificationEmails: state => (state.user ? state.user.notificationEmails : 0)
5064
};

store/tokens.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ActionTree } from "vuex";
22
import { RootState } from "../types/settings";
33

4-
export const state = (): RootState => ({});
4+
export const state = () => ({});
55

66
export const actions: ActionTree<RootState, RootState> = {
77
async verify({ commit }, context) {

types/settings.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ export interface Email {
1515
id: number;
1616
}
1717

18+
export interface SecurityEvent {
19+
id: number;
20+
}
21+
1822
export interface RootState {
1923
user?: User;
20-
emails?: Email[];
24+
emails: Email[];
25+
securityEvents: SecurityEvent[];
2126
}

yarn.lock

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3321,7 +3321,7 @@ dashdash@^1.12.0:
33213321
dependencies:
33223322
assert-plus "^1.0.0"
33233323

3324-
date-fns@^1.23.0:
3324+
date-fns@^1.23.0, date-fns@^1.29.0:
33253325
version "1.30.1"
33263326
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
33273327
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
@@ -9445,6 +9445,13 @@ vue-template-es2015-compiler@^1.9.0:
94459445
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
94469446
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
94479447

9448+
vue-timeago@^5.1.2:
9449+
version "5.1.2"
9450+
resolved "https://registry.yarnpkg.com/vue-timeago/-/vue-timeago-5.1.2.tgz#1fa625077a271cb1b7dddbbfb79e7647d2ea44c4"
9451+
integrity sha512-K74EdER1WO1XX+EIsf5ZeHucZjconfncOTlKxntC7s7q0QAAgiZt5BYenJ/GPHEEcxfmrGsiFWJPYmLHyNzDRg==
9452+
dependencies:
9453+
date-fns "^1.29.0"
9454+
94489455
vue@^2.6.10:
94499456
version "2.6.10"
94509457
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"

0 commit comments

Comments
 (0)