Skip to content

Commit 18f1137

Browse files
add electro test
1 parent 0a01e89 commit 18f1137

File tree

11 files changed

+330
-31
lines changed

11 files changed

+330
-31
lines changed

app/pages/electricity/index.vue

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<template>
2+
<div
3+
class="min-h-screen text-label-primary container flex flex-col items-center justify-start mx-auto px-4 py-8 max-w-6xl"
4+
>
5+
<h1 class="text-4xl font-bold text-accent-primary mb-8">Electricity</h1>
6+
7+
<div
8+
v-if="data?.trackers"
9+
class="space-y-4 w-full"
10+
>
11+
<div
12+
v-for="tracker in data.trackers"
13+
:key="tracker.slug"
14+
class="bg-fill-secondary p-4 rounded-xl border border-separator-primary"
15+
>
16+
<h3 class="text-lg font-semibold text-label-primary mb-2">
17+
{{ tracker.name }}
18+
</h3>
19+
<p class="text-sm text-label-secondary">
20+
Останній раз активний:
21+
{{ formatDate(tracker.lastAlive) }}
22+
</p>
23+
</div>
24+
</div>
25+
26+
<div
27+
class="my-8 bg-fill-secondary p-4 rounded-xl border border-separator-primary w-full"
28+
>
29+
<div class="flex gap-4">
30+
<input
31+
v-model="newTrackerName"
32+
type="text"
33+
placeholder="Enter tracker name"
34+
class="flex-1 px-4 py-2 bg-fill-primary border border-separator-primary rounded-lg text-label-primary focus:outline-none focus:border-accent-primary"
35+
@keyup.enter="addTracker"
36+
/>
37+
<MainButton
38+
@click="addTracker"
39+
:label="isAdding ? 'Adding...' : 'Add'"
40+
button-style="primary"
41+
/>
42+
</div>
43+
</div>
44+
45+
<div
46+
v-if="showTokenDialog"
47+
class="flex flex-col items-center justify-center w-full"
48+
>
49+
<div
50+
class="bg-fill-secondary rounded-xl border border-separator-primary p-6 max-w-2xl w-full mx-4 backdrop-blur-sm"
51+
@click.stop
52+
>
53+
<h3 class="text-xl font-bold text-label-primary mb-4">
54+
Tracker Token
55+
</h3>
56+
<div
57+
class="bg-red-500/10 border border-red-500/30 rounded-lg p-4 mb-4"
58+
>
59+
<p class="text-red-500 text-sm font-semibold">
60+
⚠️ Це остання можливість скопіювати токен, зробіть це
61+
перед тим як виконати будь яку іншу дію
62+
</p>
63+
</div>
64+
<div class="flex gap-2 mb-4">
65+
<input
66+
:value="newTrackerToken"
67+
readonly
68+
class="flex-1 px-4 py-2 bg-fill-primary border border-separator-primary rounded-lg text-label-primary text-sm font-mono"
69+
/>
70+
<MainButton
71+
@click="copyToken"
72+
label="Copy"
73+
button-style="primary"
74+
icon="mdi:content-copy"
75+
/>
76+
</div>
77+
</div>
78+
</div>
79+
</div>
80+
</template>
81+
82+
<script setup lang="ts">
83+
import { definePageMeta, useFetch, ref } from '#imports'
84+
import type { ElectricityTracker } from '~~/shared/types/electricity_device'
85+
import MainButton from '~/components/MainButton.vue'
86+
87+
definePageMeta({
88+
layout: 'default',
89+
})
90+
91+
const { data, refresh } = await useFetch<{
92+
trackers: ElectricityTracker[]
93+
}>('/api/electricty_tracker/all')
94+
95+
const newTrackerName = ref('')
96+
const isAdding = ref(false)
97+
const showTokenDialog = ref(false)
98+
const newTrackerToken = ref('')
99+
100+
const addTracker = async () => {
101+
if (!newTrackerName.value.trim() || isAdding.value) return
102+
103+
isAdding.value = true
104+
try {
105+
const response = await $fetch<{
106+
tracker: {
107+
name: string
108+
slug: string
109+
trackerJwtToken: string
110+
}
111+
}>('/api/electricty_tracker', {
112+
method: 'POST',
113+
body: {
114+
name: newTrackerName.value,
115+
},
116+
})
117+
118+
newTrackerToken.value = response.tracker.trackerJwtToken
119+
showTokenDialog.value = true
120+
newTrackerName.value = ''
121+
await refresh()
122+
} catch (error) {
123+
console.error('Error adding tracker:', error)
124+
} finally {
125+
isAdding.value = false
126+
}
127+
}
128+
129+
const copyToken = async () => {
130+
await navigator.clipboard.writeText(newTrackerToken.value)
131+
}
132+
133+
const formatDate = (date: Date | undefined) => {
134+
if (!date) return 'Невідомо'
135+
return new Date(date).toLocaleDateString('en-US', {
136+
year: 'numeric',
137+
month: 'long',
138+
day: 'numeric',
139+
hour: '2-digit',
140+
minute: '2-digit',
141+
})
142+
}
143+
</script>

app/pages/home/index.vue

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,53 +4,28 @@
44
<div class="max-w-4xl mx-auto">
55
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
66
<NuxtLink
7-
to="/karma"
7+
v-for="card in cards"
8+
:key="card.route"
9+
:to="card.route"
810
class="group bg-fill-secondary p-6 rounded-xl border border-separator-primary hover:border-accent-primary transition-all duration-200 cursor-pointer"
911
>
1012
<div class="flex flex-col gap-4">
1113
<div
1214
class="w-12 h-12 rounded-lg bg-accent-primary/10 flex items-center justify-center group-hover:bg-accent-primary/20 transition-all duration-200"
1315
>
1416
<Icon
15-
icon="mdi:chart-line"
17+
:icon="card.icon"
1618
class="text-2xl text-accent-primary"
1719
/>
1820
</div>
1921
<div>
2022
<h2
2123
class="text-xl font-bold text-label-primary mb-2"
2224
>
23-
Подивитися карму людей
25+
{{ card.title }}
2426
</h2>
2527
<p class="text-sm text-label-secondary">
26-
Переглянути рейтинг та карму всіх учасників
27-
спільноти
28-
</p>
29-
</div>
30-
</div>
31-
</NuxtLink>
32-
33-
<NuxtLink
34-
to="/design/buttons"
35-
class="group bg-fill-secondary p-6 rounded-xl border border-separator-primary hover:border-accent-primary transition-all duration-200 cursor-pointer"
36-
>
37-
<div class="flex flex-col gap-4">
38-
<div
39-
class="w-12 h-12 rounded-lg bg-accent-primary/10 flex items-center justify-center group-hover:bg-accent-primary/20 transition-all duration-200"
40-
>
41-
<Icon
42-
icon="mdi:palette"
43-
class="text-2xl text-accent-primary"
44-
/>
45-
</div>
46-
<div>
47-
<h2
48-
class="text-xl font-bold text-label-primary mb-2"
49-
>
50-
Подивитися на дизайн систему
51-
</h2>
52-
<p class="text-sm text-label-secondary">
53-
Вивчити компоненти та стилі дизайн системи
28+
{{ card.description }}
5429
</p>
5530
</div>
5631
</div>
@@ -71,6 +46,27 @@ definePageMeta({
7146
layout: 'default',
7247
})
7348
49+
const cards = [
50+
{
51+
route: '/karma',
52+
icon: 'mdi:chart-line',
53+
title: 'Подивитися карму людей',
54+
description: 'Переглянути рейтинг та карму всіх учасників спільноти',
55+
},
56+
{
57+
route: '/design/buttons',
58+
icon: 'mdi:palette',
59+
title: 'Подивитися на дизайн систему',
60+
description: 'Вивчити компоненти та стилі дизайн системи',
61+
},
62+
{
63+
route: '/electricity',
64+
icon: 'mdi:lightning-bolt',
65+
title: 'Подивитися електроенергію',
66+
description: 'Переглянути електроенергію всіх учасників спільноти',
67+
},
68+
]
69+
7470
onMounted(() => {
7571
trackEvent('page_view', { page: 'home' })
7672
})

server/api/auth/discord/auth.post.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ export default defineEventHandler(async (event): Promise<AuthResponse> => {
1414
const { code } = await readBody(event)
1515
const userAgent = event.node.req.headers['user-agent'] as string
1616
const country = event.node.req.headers[COUNTRY_HEADER_NAME] as string
17+
useNitroApp().logger.info('Discord auth code', { code: code })
1718
const accessToken = await discordAuth(code)
1819

20+
useNitroApp().logger.info('Discord access token getting successful', { accessToken: accessToken })
21+
1922
const userBody: any = await getUserByAuthToken(accessToken)
2023
useNitroApp().logger.info('Discord user getting successful', { user: userBody })
2124

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { defineEventHandler, getRouterParam, createError, getHeader, useRuntimeConfig, useNitroApp } from '#imports'
2+
import jwt from 'jsonwebtoken'
3+
4+
export default defineEventHandler(async (event) => {
5+
const authToken = getHeader(event, 'Authorization') as string
6+
if (!authToken) {
7+
throw createError({
8+
statusCode: 401,
9+
statusMessage: 'Unauthorized',
10+
})
11+
}
12+
13+
const deviceJWTToken = authToken.split(' ')[1]
14+
15+
const decoded = jwt.verify(deviceJWTToken, useRuntimeConfig().jwtSecret) as { trackerSlug: string }
16+
17+
const db = useNitroApp().db
18+
const device = await db.collection('electricity_trackers').findOne({ slug: decoded.trackerSlug })
19+
20+
if (!device) {
21+
throw createError({
22+
statusCode: 404,
23+
statusMessage: 'Device not found',
24+
})
25+
}
26+
27+
const record = {
28+
deviceSlug: device.slug,
29+
timestamp: new Date(),
30+
}
31+
32+
await db.collection('electricity_tracker_alive').insertOne(record)
33+
34+
return {
35+
success: true,
36+
}
37+
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { defineEventHandler, createError, useNitroApp } from '#imports'
2+
import type { ElectricityTracker } from '~~/shared/types/electricity_device'
3+
4+
export default defineEventHandler(async (event) => {
5+
const user = event.context.user
6+
if (!user) {
7+
throw createError({
8+
statusCode: 401,
9+
statusMessage: 'Unauthorized',
10+
})
11+
}
12+
13+
const db = useNitroApp().db
14+
15+
const trackers = await db
16+
.collection('electricity_trackers')
17+
.find({ userId: user.userId })
18+
.sort({ createdAt: -1 })
19+
.toArray()
20+
21+
const trackerSlugs = trackers.map((t: any) => t.slug)
22+
const aliveDocs = await db
23+
.collection('electricity_tracker_alive')
24+
.aggregate([
25+
{ $match: { deviceSlug: { $in: trackerSlugs } } },
26+
{ $sort: { timestamp: -1 } },
27+
{
28+
$group: {
29+
_id: '$deviceSlug',
30+
lastAlive: { $first: '$timestamp' }
31+
}
32+
}
33+
])
34+
.toArray()
35+
36+
const lastAliveMap: Record<string, Date> = {}
37+
for (const doc of aliveDocs) {
38+
lastAliveMap[doc._id] = doc.lastAlive
39+
}
40+
41+
42+
return {
43+
trackers: trackers.map((t: any): ElectricityTracker => ({
44+
name: t.name,
45+
slug: t.slug,
46+
createdAt: t.createdAt,
47+
lastAlive: lastAliveMap[t.slug],
48+
}))
49+
}
50+
})
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createError, defineEventHandler, readBody, useNitroApp, useRuntimeConfig } from '#imports'
2+
import type { CreateElectricityTrackerRequest } from '~~/shared/types/create_electricity_tracker'
3+
import { createSlugFromName } from '~~/shared/utils'
4+
import jwt from 'jsonwebtoken'
5+
import { randomUUID } from 'crypto'
6+
7+
export default defineEventHandler(async (event) => {
8+
const user = event.context.user
9+
if (!user) {
10+
throw createError({
11+
statusCode: 401,
12+
statusMessage: 'Unauthorized',
13+
})
14+
}
15+
16+
const body = await readBody<CreateElectricityTrackerRequest>(event)
17+
const { name } = body
18+
19+
const slug = createSlugFromName(name)
20+
const db = useNitroApp().db
21+
22+
const trackerJwtToken = jwt.sign({ trackerSlug: slug }, useRuntimeConfig().jwtSecret)
23+
24+
const doc = {
25+
name: name.trim(),
26+
slug: slug,
27+
createdAt: new Date(),
28+
userId: user.userId,
29+
}
30+
31+
await db.collection('electricity_trackers').insertOne(doc)
32+
33+
return {
34+
tracker: {
35+
name: name.trim(),
36+
slug: slug,
37+
trackerJwtToken: trackerJwtToken,
38+
}
39+
}
40+
})
41+

server/core/discord/data/repository.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export async function getUserByAuthToken(authToken: string): Promise<DiscordMemb
7171
if (!discordResponce.ok) {
7272
useNitroApp().logger.error('Discord user getting failed', {
7373
error: discordResponce.statusText,
74+
body: await discordResponce.json(),
7475
hint: 'Make sure the bot is added to the guild and has SERVER MEMBERS INTENT enabled in Discord Developer Portal'
7576
})
7677
throw createError({

server/plugins/2_mongo.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ async function createIndexes(db: Db, logger: any) {
6060
}
6161
)
6262

63+
await db.collection('electricity_trackers').createIndex(
64+
{ slug: 1 },
65+
{
66+
unique: true,
67+
name: 'unique_electricity_tracker_slug'
68+
}
69+
)
70+
6371
logger.info('Database indexes created successfully')
6472
} catch (error) {
6573
logger.error('Failed to create database indexes:', error)

0 commit comments

Comments
 (0)