Skip to content

Commit c3f8197

Browse files
committed
feat(e2e): first e2e draft working
1 parent 44a879f commit c3f8197

File tree

19 files changed

+411
-165
lines changed

19 files changed

+411
-165
lines changed

api/createLink.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { insertRecord } from './db/index.js'
2+
3+
export const config = { runtime: 'nodejs' }
4+
5+
export default async function handler(req, res) {
6+
try {
7+
const result = await insertRecord({
8+
data: req.body.data,
9+
id: req.body.id,
10+
})
11+
if (result) {
12+
res.status(200).json({
13+
success: true,
14+
})
15+
} else {
16+
throw new Error('Failed to insert record')
17+
}
18+
} catch (error) {
19+
console.error(error)
20+
res.status(500).json({ message: error.message || 'Internal server error' })
21+
}
22+
}

api/db/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export async function getRecordById(id) {
2929
try {
3030
await client.connect()
3131
const res = await client.query(
32-
`SELECT data, "creationTimestamp" FROM "${DB_SCHEMA}".e2e_data WHERE id = ${id};`
32+
`SELECT data, "creationTimestamp" FROM "${DB_SCHEMA}".e2e_data WHERE id = $1;`,
33+
[id]
3334
)
3435
return res.rows
3536
} catch (error) {
@@ -44,7 +45,8 @@ export async function insertRecord({ data, id }) {
4445
try {
4546
await client.connect()
4647
await client.query(
47-
`INSERT INTO "${DB_SCHEMA}".e2e_data(data, id) VALUES ('${data}', '${id}');`
48+
`INSERT INTO "${DB_SCHEMA}".e2e_data(data, id) VALUES ($1, $2);`,
49+
[data, id]
4850
)
4951
return true
5052
} catch (error) {

api/getLink.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { getRecordById } from './db/index.js'
2+
3+
export const config = { runtime: 'nodejs' }
4+
5+
export default async function handler(req, res) {
6+
try {
7+
const records = await getRecordById(req.query.id)
8+
res.json(records)
9+
} catch (error) {
10+
console.error(error)
11+
res.status(500).json({ message: error.message || 'Internal server error' })
12+
}
13+
}

api/ok.js

Lines changed: 0 additions & 13 deletions
This file was deleted.

components/buttons/copyLink.vue

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
<button
33
id="copyLinkButton"
44
type="button"
5-
class="inline-flex items-center justify-center gap-1 p-2 text-sm transition-transform transform rounded-md shadow justify-self-end focus-visible:ring-4 active:scale-y-75 hover:scale-105 hover:shadow-lg copy-uri-button"
5+
class="inline-flex gap-1 justify-center justify-self-end items-center p-2 text-sm rounded-md shadow transition-transform transform focus-visible:ring-4 active:scale-y-75 hover:scale-105 hover:shadow-lg copy-uri-button"
66
aria-label="Click here to copy url to clipboard"
77
:class="{
8-
'bg-blue-500 text-white': !copied,
9-
'bg-green-500 text-gray-800': copied,
8+
'bg-blue-500 text-white': copied === false,
9+
'bg-green-500 text-gray-800': copied === true,
10+
'bg-blue-500 text-white animate-pulse': copied === null,
1011
}"
12+
:disabled="copied === null"
1113
@click="clickHandler"
1214
>
1315
<span
14-
class="inline-flex items-center justify-center gap-1"
16+
class="inline-flex gap-1 justify-center items-center"
1517
aria-live="assertive"
1618
role="status"
1719
>
@@ -22,14 +24,16 @@
2224
<span v-show="!copied" class="inline" aria-hidden="true">
2325
<Link />
2426
</span>
25-
<span v-show="!copied" class="hidden md:inline-block">Copy link</span>
27+
<span v-show="!copied" class="hidden md:inline-block">{{
28+
copied === null ? 'Generating...' : 'Copy link'
29+
}}</span>
2630
</span>
2731
</button>
2832
</template>
2933
<script lang="ts">
3034
import Vue from 'vue'
31-
import Link from '~/components/icons/link.vue'
3235
import Copied from '~/components/icons/copied.vue'
36+
import Link from '~/components/icons/link.vue'
3337
export default Vue.extend({
3438
components: { Link, Copied },
3539
props: {

components/diffActionBar.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default Vue.extend({
5151
comparator: null,
5252
comparer: null,
5353
treeWalker: null,
54+
e2eLink: null,
5455
}
5556
},
5657
mounted() {

components/v2/diffActionBar.vue

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<NextDiff :click-handler="goToNextDiff" />
2121
<PrevDiff :click-handler="goToPreviousDiff" />
2222
</div>
23-
<div class="flex items-center gap-4">
23+
<div class="flex gap-4 items-center">
2424
<DiffStyle :click-handler="toggleDiffFashion" />
2525
<CopyLink :click-handler="copyUrlToClipboard" :copied="copied"></CopyLink>
2626
</div>
@@ -29,12 +29,23 @@
2929

3030
<script lang="ts">
3131
import Vue from 'vue'
32-
import PrevDiff from '../buttons/prevDiff.vue'
33-
import NextDiff from '../buttons/nextDiff.vue'
3432
import CopyLink from '../buttons/copyLink.vue'
3533
import DiffStyle from '../buttons/diffStyle.vue'
36-
import { putToClipboard } from '~/helpers/utils'
34+
import NextDiff from '../buttons/nextDiff.vue'
35+
import PrevDiff from '../buttons/prevDiff.vue'
36+
import { SIMPLE_DIFF_CHARACTER_LIMIT } from '~/constants/constants'
37+
import {
38+
E2E_LINK_GENERATION_ERROR,
39+
E2E_LINK_GENERATION_SUCCESS,
40+
LINK_COPY_SUCCESS,
41+
} from '~/constants/messages'
42+
import {
43+
getEncryptedData,
44+
getEncryptionKey,
45+
getExtractedEncryptionKey,
46+
} from '~/helpers/encrypt'
3747
import { DiffActionBarData } from '~/helpers/types'
48+
import { getRandomDiffId, putToClipboard } from '~/helpers/utils'
3849
export default Vue.extend({
3950
components: {
4051
PrevDiff,
@@ -58,6 +69,7 @@ export default Vue.extend({
5869
comparator: null,
5970
comparer: null,
6071
treeWalker: null,
72+
e2eLink: null,
6173
}
6274
},
6375
mounted() {
@@ -81,15 +93,67 @@ export default Vue.extend({
8193
}
8294
},
8395
copyUrlToClipboard() {
84-
putToClipboard(
85-
window.location.href,
86-
'Link copied to your clipboard',
87-
this.$store
88-
)
89-
this.copied = true
90-
setTimeout(() => {
91-
this.copied = false
92-
}, 5000)
96+
const isCurrentUrlExceedsCharacterLimit =
97+
window.location.href.length > SIMPLE_DIFF_CHARACTER_LIMIT
98+
if (isCurrentUrlExceedsCharacterLimit && this.e2eLink) {
99+
putToClipboard(this.e2eLink, LINK_COPY_SUCCESS, this.$store)
100+
} else if (isCurrentUrlExceedsCharacterLimit) {
101+
this.copyE2eUrlToClipboard()
102+
} else {
103+
putToClipboard(window.location.href, LINK_COPY_SUCCESS, this.$store)
104+
this.copied = true
105+
setTimeout(() => {
106+
this.copied = false
107+
}, 5000)
108+
}
109+
},
110+
async copyE2eUrlToClipboard() {
111+
try {
112+
this.copied = null
113+
const id = getRandomDiffId()
114+
const keyBuffer = await getEncryptionKey()
115+
const encryptedDataText = await getEncryptedData(
116+
window.location.hash.replace(/^#/, ''),
117+
keyBuffer
118+
)
119+
const extractedEncryptionKey = await getExtractedEncryptionKey(
120+
keyBuffer
121+
)
122+
const response = await fetch('/api/createLink', {
123+
method: 'POST',
124+
headers: {
125+
'Content-Type': 'application/json',
126+
},
127+
body: JSON.stringify({
128+
data: encryptedDataText,
129+
id,
130+
}),
131+
})
132+
const data = await response.json()
133+
if (!data.success) {
134+
throw new Error(E2E_LINK_GENERATION_ERROR)
135+
}
136+
const newUrl = new URL(window.location.origin)
137+
newUrl.pathname = '/v2/diff'
138+
newUrl.hash = `#${extractedEncryptionKey}`
139+
newUrl.searchParams.set('id', id)
140+
putToClipboard(
141+
newUrl.toString(),
142+
E2E_LINK_GENERATION_SUCCESS,
143+
this.$store
144+
)
145+
this.e2eLink = newUrl.toString()
146+
this.copied = true
147+
setTimeout(() => {
148+
this.copied = false
149+
}, 5000)
150+
} catch (error: any) {
151+
this.showErrorToast(E2E_LINK_GENERATION_ERROR)
152+
} finally {
153+
setTimeout(() => {
154+
this.copied = false
155+
}, 5000)
156+
}
93157
},
94158
goToNextDiff() {
95159
this.diffNavigator.next()
@@ -100,6 +164,29 @@ export default Vue.extend({
100164
toggleDiffFashion(value: boolean) {
101165
this.onDiffFashion(value)
102166
},
167+
showErrorToast(content: string) {
168+
this.$store.commit('toast/show', {
169+
show: true,
170+
content,
171+
iconHTML: `
172+
<svg
173+
class="w-6 h-6"
174+
fill="none"
175+
stroke="currentColor"
176+
viewBox="0 0 24 24"
177+
xmlns="http://www.w3.org/2000/svg"
178+
>
179+
<path
180+
stroke-linecap="round"
181+
stroke-linejoin="round"
182+
stroke-width="2"
183+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
184+
></path>
185+
</svg>
186+
`,
187+
theme: 'error',
188+
})
189+
},
103190
},
104191
})
105192
</script>

constants/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const SIMPLE_DIFF_CHARACTER_LIMIT = 10000;

constants/messages.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export const E2E_LINK_GENERATION_ERROR =
2+
'Failed to generate E2E Link. Please try again later.'
3+
4+
export const E2E_LINK_GENERATION_SUCCESS = 'E2E Link copied to your clipboard'
5+
6+
export const E2E_DATA_LOADING_INFO = 'Loading your end to end encrypted diff...'
7+
8+
export const E2E_DATA_DECRYPTING_INFO = 'Decrypting your diff on your browser...'
9+
10+
export const E2E_DATA_FINALIZING_INFO = 'Finalizing your diff...'
11+
12+
export const E2E_DATA_ERROR =
13+
"We couldn't fetch/decrypt your diff. Please try again later."
14+
15+
export const E2E_DATA_NO_LONGER_AVAILABLE_ERROR =
16+
'Looks like your diff data is no longer available. Sorry for the inconvenience.'
17+
18+
export const LINK_COPY_SUCCESS = 'Link copied to your clipboard'
19+
20+
export const DIFF_USER_BLANK_SIDE_ERROR = 'Please enter some data on both sides to compare'

helpers/decrypt.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { base64ToArrayBuffer } from './utils'
2+
3+
export async function getDepryctionKey(key: string) {
4+
const decryptionKey = await window.crypto.subtle.importKey(
5+
'jwk',
6+
{
7+
k: key,
8+
alg: 'A128GCM',
9+
ext: true,
10+
key_ops: ['encrypt', 'decrypt'],
11+
kty: 'oct',
12+
},
13+
{ name: 'AES-GCM', length: 128 },
14+
false, // extractable
15+
['decrypt']
16+
)
17+
return decryptionKey
18+
}
19+
20+
export async function getDecryptedText(data: string, key: CryptoKey) {
21+
const decryptedContentBuffer = await window.crypto.subtle.decrypt(
22+
{ name: 'AES-GCM', iv: new Uint8Array(12) },
23+
key,
24+
base64ToArrayBuffer(data)
25+
)
26+
const plainText = new window.TextDecoder().decode(new Uint8Array(decryptedContentBuffer))
27+
return plainText
28+
}

0 commit comments

Comments
 (0)